diff --git a/CLAUDE.md b/CLAUDE.md index 28c151add..86d1a95a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,7 +154,7 @@ git push ### Current counts (update when generator runs) -**947** assigned | **947** UNDF posts | last run: 2026-03-31 +**951** assigned | **951** UNDF posts | last run: 2026-03-31 ### Patch stamp format diff --git a/defects/tiled-0001/patch/tiled-0001.patch b/defects/tiled-0001/patch/tiled-0001.patch new file mode 100644 index 000000000..7edcc1bf2 --- /dev/null +++ b/defects/tiled-0001/patch/tiled-0001.patch @@ -0,0 +1,70 @@ +# UNDF: UNDF-2026-000000949 +--- a/src/tiled/mapdocument.cpp ++++ b/src/tiled/mapdocument.cpp +@@ -1225,6 +1225,9 @@ static QList sortLayers(const Map &map, const QList &layers) + if (layers.size() < 2) + return layers; + ++ const QSet layerSet(layers.begin(), layers.end()); ++ + QList sorted; + sorted.reserve(layers.size()); + + LayerIterator iterator(&map); + while (Layer *layer = iterator.next()) { +- if (layers.contains(layer)) ++ if (layerSet.contains(layer)) + sorted.append(layer); + } + +@@ -1247,6 +1250,9 @@ static QList sortObjects(const Map &map, const QList & + if (objects.size() < 2) + return objects; + ++ const QSet objectSet(objects.begin(), objects.end()); ++ + QList sorted; + sorted.reserve(objects.size()); + + LayerIterator iterator(&map); + while (Layer *layer = iterator.next()) { + if (layer->layerType() != Layer::ObjectGroupType) + continue; + + for (MapObject *mapObject : static_cast(layer)->objects()) { +- if (objects.contains(mapObject)) ++ if (objectSet.contains(mapObject)) + sorted.append(mapObject); + } + } +@@ -686,7 +686,8 @@ void MapDocument::duplicateLayers(const QList &layers) + // Duplicate layers in the right order (groups before their children) ++ const QSet layerSet(layers.begin(), layers.end()); + LayerIterator iterator(mMap.get()); + iterator.toBack(); + while (Layer *layer = iterator.previous()) +- if (layers.contains(layer)) ++ if (layerSet.contains(layer)) + layersToDuplicate.append(layer); + +@@ -808,7 +809,8 @@ void MapDocument::moveLayersUp(const QList &layers) + // Move layers in the right order, and abort if one of the layers can't be + // moved (iterating backwards because when moving layers up we need to + // start moving the top-most layer first) ++ const QSet layerSet(layers.begin(), layers.end()); + LayerIterator iterator(mMap.get()); + iterator.toBack(); + while (Layer *layer = iterator.previous()) { +- if (layers.contains(layer)) { ++ if (layerSet.contains(layer)) { + if (!MoveLayer::canMoveUp(*layer)) + return; + +@@ -837,6 +839,7 @@ void MapDocument::moveLayersDown(const QList &layers) + // Move layers in the right order, and abort if one of the layers can't be moved ++ const QSet layerSet(layers.begin(), layers.end()); + for (Layer *layer : mMap->allLayers()) { +- if (layers.contains(layer)) { ++ if (layerSet.contains(layer)) { + if (!MoveLayer::canMoveDown(*layer)) + return; diff --git a/defects/tiled-0001/test/tiled-0001-test b/defects/tiled-0001/test/tiled-0001-test new file mode 100755 index 000000000..83e5bef83 Binary files /dev/null and b/defects/tiled-0001/test/tiled-0001-test differ diff --git a/defects/tiled-0001/test/tiled-0001-test.cpp b/defects/tiled-0001/test/tiled-0001-test.cpp new file mode 100644 index 000000000..b7ecf2a58 --- /dev/null +++ b/defects/tiled-0001/test/tiled-0001-test.cpp @@ -0,0 +1,83 @@ +// tiled-0001-test.cpp +// CWE-407 unit test: mapdocument.cpp sortObjects/sortLayers/moveLayersUp/moveLayersDown/duplicateLayers +// QList.contains() inside iteration over all map layers/objects = O(AllItems * SelectedItems) +// Fix: QSet lookup O(1) per item +// +// Simulates the pattern: iterate all items, check membership in selected set. + +#include +#include +#include +#include +#include +#include + +// Simulate the defective sortObjects pattern +static QList sortObjects_defective(const QList &allObjects, const QList &selected) { + QList sorted; + sorted.reserve(selected.size()); + for (void *obj : allObjects) { + if (selected.contains(obj)) // O(N) per call + sorted.append(obj); + } + return sorted; +} + +// Fixed version: QSet for O(1) lookup +static QList sortObjects_fixed(const QList &allObjects, const QList &selected) { + const QSet selectedSet(selected.begin(), selected.end()); + QList sorted; + sorted.reserve(selected.size()); + for (void *obj : allObjects) { + if (selectedSet.contains(obj)) // O(1) per call + sorted.append(obj); + } + return sorted; +} + +int main() { + // N = total objects on map, S = selected objects + const int N = 2000; + const int S = 1000; + + QList allObjects; + allObjects.reserve(N); + for (int i = 0; i < N; ++i) + allObjects.append(reinterpret_cast(static_cast(i + 1))); + + // Select every other object + QList selected; + selected.reserve(S); + for (int i = 0; i < S; ++i) + selected.append(allObjects[i * 2]); + + const int ITERS = 200; + + // Benchmark defective + QElapsedTimer timer; + timer.start(); + for (int i = 0; i < ITERS; ++i) { + auto result = sortObjects_defective(allObjects, selected); + assert(result.size() == S); + } + qint64 defective_us = timer.nsecsElapsed() / 1000; + + // Benchmark fixed + timer.restart(); + for (int i = 0; i < ITERS; ++i) { + auto result = sortObjects_fixed(allObjects, selected); + assert(result.size() == S); + } + qint64 fixed_us = timer.nsecsElapsed() / 1000; + + double ratio = (double)defective_us / (double)fixed_us; + + printf("tiled-0001 CWE-407 sortObjects/sortLayers QList.contains in loop\n"); + printf(" N=%d total objects, S=%d selected, %d iterations\n", N, S, ITERS); + printf(" defective: %lld us\n", defective_us); + printf(" fixed: %lld us\n", fixed_us); + printf(" ratio: %.1fx\n", ratio); + printf(" %s\n", ratio >= 2.0 ? "PASS" : "FAIL"); + + return ratio >= 2.0 ? 0 : 1; +} diff --git a/defects/tiled-0002/patch/tiled-0002.patch b/defects/tiled-0002/patch/tiled-0002.patch new file mode 100644 index 000000000..2602fcb48 --- /dev/null +++ b/defects/tiled-0002/patch/tiled-0002.patch @@ -0,0 +1,16 @@ +# UNDF: UNDF-2026-000000950 +--- a/src/tiled/mapobjectmodel.cpp ++++ b/src/tiled/mapobjectmodel.cpp +@@ -493,6 +493,7 @@ void MapObjectModel::classChanged(const QList &objects) + } else if (typeId == Object::TileType) { ++ const QSet objectSet(objects.begin(), objects.end()); + for (const Layer *layer : map()->objectGroups()) { + auto objectGroup = static_cast(layer); + for (MapObject *mapObject : objectGroup->objects()) { + if (mapObject->className().isEmpty()) + if (auto tile = mapObject->cell().tile()) +- if (objects.contains(tile)) ++ if (objectSet.contains(tile)) + affectedObjects.append(mapObject); + } + } diff --git a/defects/tiled-0002/test/tiled-0002-test b/defects/tiled-0002/test/tiled-0002-test new file mode 100755 index 000000000..ef3088ebb Binary files /dev/null and b/defects/tiled-0002/test/tiled-0002-test differ diff --git a/defects/tiled-0002/test/tiled-0002-test.cpp b/defects/tiled-0002/test/tiled-0002-test.cpp new file mode 100644 index 000000000..b26ca1bde --- /dev/null +++ b/defects/tiled-0002/test/tiled-0002-test.cpp @@ -0,0 +1,79 @@ +// tiled-0002-test.cpp +// CWE-407 unit test: mapobjectmodel.cpp classChanged +// QList.contains(tile) inside nested loop over all map objects = O(MapObjects * ChangedTiles) +// Fix: QSet lookup O(1) per item + +#include +#include +#include +#include +#include + +// Simulate: for each map object, check if its tile is in the changed-objects list +static int classChanged_defective(const QList &allMapObjects, + const QList &objectTiles, + const QList &changedObjects) { + int count = 0; + for (int i = 0; i < allMapObjects.size(); ++i) { + if (changedObjects.contains(objectTiles[i])) // O(N) per call + count++; + } + return count; +} + +static int classChanged_fixed(const QList &allMapObjects, + const QList &objectTiles, + const QList &changedObjects) { + const QSet objectSet(changedObjects.begin(), changedObjects.end()); + int count = 0; + for (int i = 0; i < allMapObjects.size(); ++i) { + if (objectSet.contains(objectTiles[i])) // O(1) per call + count++; + } + return count; +} + +int main() { + const int MAP_OBJECTS = 2000; + const int CHANGED_TILES = 500; + + QList tiles; + for (int i = 0; i < CHANGED_TILES; ++i) + tiles.append(reinterpret_cast(static_cast(i + 1))); + + QList allMapObjects; + QList objectTiles; + for (int i = 0; i < MAP_OBJECTS; ++i) { + allMapObjects.append(reinterpret_cast(static_cast(i + 10000))); + // Half the objects reference changed tiles + objectTiles.append(tiles[i % CHANGED_TILES]); + } + + const int ITERS = 500; + + QElapsedTimer timer; + timer.start(); + for (int i = 0; i < ITERS; ++i) { + int r = classChanged_defective(allMapObjects, objectTiles, tiles); + assert(r == MAP_OBJECTS); + } + qint64 defective_us = timer.nsecsElapsed() / 1000; + + timer.restart(); + for (int i = 0; i < ITERS; ++i) { + int r = classChanged_fixed(allMapObjects, objectTiles, tiles); + assert(r == MAP_OBJECTS); + } + qint64 fixed_us = timer.nsecsElapsed() / 1000; + + double ratio = (double)defective_us / (double)fixed_us; + + printf("tiled-0002 CWE-407 classChanged QList.contains in nested loop\n"); + printf(" MAP_OBJECTS=%d, CHANGED_TILES=%d, %d iterations\n", MAP_OBJECTS, CHANGED_TILES, ITERS); + printf(" defective: %lld us\n", defective_us); + printf(" fixed: %lld us\n", fixed_us); + printf(" ratio: %.1fx\n", ratio); + printf(" %s\n", ratio >= 2.0 ? "PASS" : "FAIL"); + + return ratio >= 2.0 ? 0 : 1; +} diff --git a/defects/tiled-0003/patch/tiled-0003.patch b/defects/tiled-0003/patch/tiled-0003.patch new file mode 100644 index 000000000..0d18f7758 --- /dev/null +++ b/defects/tiled-0003/patch/tiled-0003.patch @@ -0,0 +1,30 @@ +# UNDF: UNDF-2026-000000951 +--- a/src/tiled/editpolygontool.cpp ++++ b/src/tiled/editpolygontool.cpp +@@ -383,6 +383,8 @@ void EditPolygonTool::updateHandles() + { + const QList &selection = mapDocument()->selectedObjects(); + ++ const QSet selectionSet(selection.begin(), selection.end()); ++ + auto deleteHandle = [this](PointHandle *handle) { + if (mHoveredHandle == handle) + mHoveredHandle = nullptr; +@@ -399,13 +401,13 @@ void EditPolygonTool::updateHandles() + QMutableHashIterator > i(mHandles); + while (i.hasNext()) { + i.next(); +- if (!selection.contains(i.key())) { ++ if (!selectionSet.contains(i.key())) { + for (PointHandle *handle : std::as_const(i.value())) + deleteHandle(handle); + + i.remove(); + } + } +- if (mHoveredSegment && !selection.contains(mHoveredSegment.object)) ++ if (mHoveredSegment && !selectionSet.contains(mHoveredSegment.object)) + mHoveredSegment.clear(); +- if (mClickedSegment && !selection.contains(mClickedSegment.object)) ++ if (mClickedSegment && !selectionSet.contains(mClickedSegment.object)) + mClickedSegment.clear(); diff --git a/defects/tiled-0003/test/tiled-0003-test b/defects/tiled-0003/test/tiled-0003-test new file mode 100755 index 000000000..a4a6cd73b Binary files /dev/null and b/defects/tiled-0003/test/tiled-0003-test differ diff --git a/defects/tiled-0003/test/tiled-0003-test.cpp b/defects/tiled-0003/test/tiled-0003-test.cpp new file mode 100644 index 000000000..0de1280f3 --- /dev/null +++ b/defects/tiled-0003/test/tiled-0003-test.cpp @@ -0,0 +1,75 @@ +// tiled-0003-test.cpp +// CWE-407 unit test: editpolygontool.cpp updateHandles +// QList.contains() inside QHash iteration = O(HandleEntries * SelectionSize) +// Fix: QSet for O(1) lookup + +#include +#include +#include +#include +#include +#include + +// Simulate: iterate hash entries, check if key is in selection list +static int updateHandles_defective(const QHash &handles, + const QList &selection) { + int removed = 0; + for (auto it = handles.begin(); it != handles.end(); ++it) { + if (!selection.contains(it.key())) // O(N) per call + removed++; + } + return removed; +} + +static int updateHandles_fixed(const QHash &handles, + const QList &selection) { + const QSet selectionSet(selection.begin(), selection.end()); + int removed = 0; + for (auto it = handles.begin(); it != handles.end(); ++it) { + if (!selectionSet.contains(it.key())) // O(1) per call + removed++; + } + return removed; +} + +int main() { + const int HANDLES = 1000; // polygon objects with handles + const int SELECTION = 500; // selected objects + + QHash handles; + QList selection; + + for (int i = 0; i < HANDLES; ++i) + handles.insert(reinterpret_cast(static_cast(i + 1)), i); + + for (int i = 0; i < SELECTION; ++i) + selection.append(reinterpret_cast(static_cast(i + 1))); + + const int ITERS = 500; + + QElapsedTimer timer; + timer.start(); + for (int i = 0; i < ITERS; ++i) { + int r = updateHandles_defective(handles, selection); + assert(r == HANDLES - SELECTION); + } + qint64 defective_us = timer.nsecsElapsed() / 1000; + + timer.restart(); + for (int i = 0; i < ITERS; ++i) { + int r = updateHandles_fixed(handles, selection); + assert(r == HANDLES - SELECTION); + } + qint64 fixed_us = timer.nsecsElapsed() / 1000; + + double ratio = (double)defective_us / (double)fixed_us; + + printf("tiled-0003 CWE-407 updateHandles QList.contains in hash iteration\n"); + printf(" HANDLES=%d, SELECTION=%d, %d iterations\n", HANDLES, SELECTION, ITERS); + printf(" defective: %lld us\n", defective_us); + printf(" fixed: %lld us\n", fixed_us); + printf(" ratio: %.1fx\n", ratio); + printf(" %s\n", ratio >= 2.0 ? "PASS" : "FAIL"); + + return ratio >= 2.0 ? 0 : 1; +}