diff --git a/defects/caprice32-0001/patch/caprice32-0001.patch b/defects/caprice32-0001/patch/caprice32-0001.patch new file mode 100644 index 000000000..0100822d5 --- /dev/null +++ b/defects/caprice32-0001/patch/caprice32-0001.patch @@ -0,0 +1,39 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/z80.cpp ++++ b/src/z80.cpp +@@ -160,6 +160,7 @@ static void* const ed_jumpTable[256] = { + #include + #include ++#include + + t_z80regs z80; + std::vector breakpoints; ++// Fast O(1) address lookup set kept in sync with breakpoints vector. ++std::unordered_set breakpoint_addresses; + std::vector watchpoints; +@@ -164,16 +165,24 @@ std::vector watchpoints; + // All callers that mutate breakpoints must also mutate breakpoint_addresses. + // API helpers to keep them in sync: ++void add_breakpoint(Breakpoint bp) { ++ breakpoints.push_back(bp); ++ breakpoint_addresses.insert(bp.address); ++} ++ ++void remove_breakpoint(std::size_t idx) { ++ breakpoint_addresses.erase(breakpoints[idx].address); ++ breakpoints.erase(breakpoints.begin() + idx); ++} ++ ++void remove_breakpoints_if(std::function pred) { ++ for (const auto& bp : breakpoints) { ++ if (pred(bp)) breakpoint_addresses.erase(bp.address); ++ } ++ breakpoints.erase(std::remove_if(breakpoints.begin(), breakpoints.end(), pred), breakpoints.end()); ++} +--- a/src/z80.cpp ++++ b/src/z80.cpp +@@ -1098,7 +1098,7 @@ dword z80_execute(dword iCycleCountInit) + if (!breakpoints.empty()) { +- if ((z80.breakpoint_reached = std::any_of(breakpoints.begin(), breakpoints.end(), [&](const auto& b) { return b.address == _PC; }))) break; ++ if ((z80.breakpoint_reached = (breakpoint_addresses.count(_PC) != 0))) break; + } diff --git a/defects/caprice32-0001/test/test_caprice32_0001 b/defects/caprice32-0001/test/test_caprice32_0001 new file mode 100755 index 000000000..23a229fac Binary files /dev/null and b/defects/caprice32-0001/test/test_caprice32_0001 differ diff --git a/defects/caprice32-0001/test/test_caprice32_0001.cpp b/defects/caprice32-0001/test/test_caprice32_0001.cpp new file mode 100644 index 000000000..a52322831 --- /dev/null +++ b/defects/caprice32-0001/test/test_caprice32_0001.cpp @@ -0,0 +1,194 @@ +// Unit test for caprice32-0001: CWE-407 breakpoint linear scan O(B) per Z80 instruction +// +// In src/z80.cpp the inner emulation loop checks breakpoints via: +// std::any_of(breakpoints.begin(), breakpoints.end(), [&](const auto& b) { return b.address == _PC; }) +// This is O(B) per instruction. With B breakpoints loaded and the PC never hitting any, +// every instruction scans all B entries. At ~4 MHz Z80 clock, that is ~4 million O(B) +// scans per second -- a multiplier of B applied to our emulation speed. +// +// Fix: maintain a parallel std::unordered_set breakpoint_addresses kept in sync +// with the breakpoints vector. Inner loop becomes: +// breakpoint_addresses.count(_PC) != 0 +// which is O(1) average. +// +// Compile: g++ -std=c++17 -O2 -o test_caprice32_0001 test_caprice32_0001.cpp +// Run: ./test_caprice32_0001 + +#include +#include +#include +#include +#include +#include +#include + +typedef uint32_t dword; +typedef uint16_t word; + +enum BreakpointType { NORMAL = 0, EPHEMERAL = 1 }; + +struct Breakpoint { + Breakpoint(word val, BreakpointType t = NORMAL) : address(val), type(t) {} + dword address; + BreakpointType type; +}; + +// --- DEFECT: O(B) std::any_of scan per instruction --- +struct DefectLoop { + std::vector breakpoints; + + void add(Breakpoint bp) { breakpoints.push_back(bp); } + + // Returns true if hit; scanned_out = number of entries examined. + bool check(word pc, int& scanned_out) { + scanned_out = 0; + for (const auto& b : breakpoints) { + ++scanned_out; + if (b.address == pc) return true; + } + return false; + } +}; + +// --- FIX: O(1) unordered_set lookup --- +struct FixedLoop { + std::vector breakpoints; + std::unordered_set breakpoint_addresses; + + void add(Breakpoint bp) { + breakpoints.push_back(bp); + breakpoint_addresses.insert(bp.address); + } + + void remove_if(std::function pred) { + for (const auto& bp : breakpoints) { + if (pred(bp)) breakpoint_addresses.erase(bp.address); + } + breakpoints.erase( + std::remove_if(breakpoints.begin(), breakpoints.end(), pred), + breakpoints.end()); + } + + void remove_at(std::size_t idx) { + breakpoint_addresses.erase(breakpoints[idx].address); + breakpoints.erase(breakpoints.begin() + idx); + } + + // Returns true if hit; scanned_out = 1 (hash probe) or 0 (fast no-match). + bool check(word pc, int& scanned_out) { + if (breakpoints.empty()) { scanned_out = 0; return false; } + bool hit = (breakpoint_addresses.count(pc) != 0); + scanned_out = 1; // one hash probe regardless of B + return hit; + } +}; + +static void test_defect_scans_all_on_miss() { + const int B = 500; + DefectLoop d; + for (int i = 0; i < B; ++i) + d.add(Breakpoint(word(0x1000 + i))); + + int scanned = 0; + bool hit = d.check(0x0000, scanned); + + assert(!hit); + assert(scanned == B && "defect: all B entries scanned for a miss"); + printf("PASS defect: %d breakpoints, PC miss -> scanned %d entries\n", B, scanned); +} + +static void test_fix_constant_work_on_miss() { + const int B = 500; + FixedLoop f; + for (int i = 0; i < B; ++i) + f.add(Breakpoint(word(0x1000 + i))); + + int scanned = 0; + bool hit = f.check(0x0000, scanned); + + assert(!hit); + assert(scanned <= 1 && "fix: O(1) probe regardless of B"); + printf("PASS fix: %d breakpoints, PC miss -> scanned %d entries (O(1))\n", B, scanned); +} + +static void test_fix_detects_hit() { + FixedLoop f; + f.add(Breakpoint(0x0400)); + f.add(Breakpoint(0x0800)); + f.add(Breakpoint(0x0C00)); + + int scanned = 0; + bool hit = f.check(0x0800, scanned); + + assert(hit && "fix: should detect breakpoint at 0x0800"); + printf("PASS fix: breakpoint at 0x0800 detected in %d probe(s)\n", scanned); +} + +static void test_fix_remove_at_updates_set() { + FixedLoop f; + f.add(Breakpoint(0x0300)); + f.add(Breakpoint(0x0600)); + + // Remove first breakpoint (index 0, address 0x0300). + f.remove_at(0); + + int scanned = 0; + bool hit = f.check(0x0300, scanned); + assert(!hit && "fix: removed breakpoint should not fire"); + printf("PASS fix: after remove, 0x0300 no longer hits\n"); + + hit = f.check(0x0600, scanned); + assert(hit && "fix: remaining breakpoint should still fire"); + printf("PASS fix: remaining breakpoint 0x0600 still hits\n"); +} + +static void test_fix_ephemeral_remove() { + FixedLoop f; + f.add(Breakpoint(0x0200, EPHEMERAL)); + f.add(Breakpoint(0x0400, NORMAL)); + + // Remove all ephemeral breakpoints (simulating RemoveEphemeralBreakpoints). + f.remove_if([](const Breakpoint& bp) { return bp.type == EPHEMERAL; }); + + int scanned = 0; + bool hit = f.check(0x0200, scanned); + assert(!hit && "fix: ephemeral breakpoint removed from set"); + + hit = f.check(0x0400, scanned); + assert(hit && "fix: normal breakpoint still present"); + printf("PASS fix: ephemeral removal leaves normal breakpoint intact\n"); +} + +static void test_speedup_ratio() { + const int B = 500; + DefectLoop d; + FixedLoop f; + for (int i = 0; i < B; ++i) { + word addr = word(0x8000 + i); + d.add(Breakpoint(addr)); + f.add(Breakpoint(addr)); + } + + int defect_scanned = 0, fix_scanned = 0; + d.check(0x0000, defect_scanned); + f.check(0x0000, fix_scanned); + + // Observed speedup ratio per-check. + int ratio = defect_scanned; // fix_scanned is 1, so ratio ~= defect_scanned + printf("PASS speedup: defect=%d scans/check, fix=%d scan/check, ratio ~%dx\n", + defect_scanned, fix_scanned, ratio); + assert(defect_scanned == B); + assert(fix_scanned <= 1); +} + +int main() { + printf("--- caprice32-0001 CWE-407 breakpoint O(B) scan per Z80 instruction ---\n"); + test_defect_scans_all_on_miss(); + test_fix_constant_work_on_miss(); + test_fix_detects_hit(); + test_fix_remove_at_updates_set(); + test_fix_ephemeral_remove(); + test_speedup_ratio(); + printf("ALL PASS\n"); + return 0; +} diff --git a/defects/caprice32-0002/patch/caprice32-0002.patch b/defects/caprice32-0002/patch/caprice32-0002.patch new file mode 100644 index 000000000..4fe626ea8 --- /dev/null +++ b/defects/caprice32-0002/patch/caprice32-0002.patch @@ -0,0 +1,57 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/z80.cpp ++++ b/src/z80.cpp +@@ -162,6 +162,9 @@ t_z80regs z80; + std::vector breakpoints; + std::unordered_set breakpoint_addresses; + std::vector watchpoints; ++// Fast O(1) lookup sets for watchpoints, separated by access type. ++std::unordered_set watchpoint_reads; // addresses watched on READ ++std::unordered_set watchpoint_writes; // addresses watched on WRITE +--- a/src/z80.cpp ++++ b/src/z80.cpp +@@ -340,11 +340,11 @@ inline byte read_mem(word addr) { + if (!watchpoints.empty()) { +- if (std::any_of(watchpoints.begin(), watchpoints.end(), [&](const auto& w) { +- return w.address == addr && (w.type & READ); +- })) { ++ if (watchpoint_reads.count(addr)) { + z80.watchpoint_reached = 1; + } + } + return read_mem_no_watchpoint(addr); + } +@@ -355,11 +355,11 @@ inline void write_mem(word addr, byte val) { + if (!watchpoints.empty()) { +- if (std::any_of(watchpoints.begin(), watchpoints.end(), [&](const auto& w) { +- return w.address == addr && (w.type & WRITE); +- })) { ++ if (watchpoint_writes.count(addr)) { + z80.watchpoint_reached = 1; + } + } +--- a/src/gui/src/CapriceDevTools.cpp ++++ b/src/gui/src/CapriceDevTools.cpp +@@ -1353,9 +1353,19 @@ void CapriceDevTools::AddWatchpoint(word address, WatchpointType type) { +- watchpoints.emplace_back(address, type); ++ watchpoints.emplace_back(address, type); ++ if (type & READ) watchpoint_reads.insert(address); ++ if (type & WRITE) watchpoint_writes.insert(address); + } + + void CapriceDevTools::RemoveWatchpoint(std::size_t idx) { ++ const auto& wp = watchpoints[idx]; ++ // Only erase from sets if no other watchpoint covers this address+type. ++ bool other_read = false; ++ bool other_write = false; ++ for (std::size_t i = 0; i < watchpoints.size(); i++) { ++ if (i == idx) continue; ++ if (watchpoints[i].address == wp.address) { ++ if (watchpoints[i].type & READ) other_read = true; ++ if (watchpoints[i].type & WRITE) other_write = true; ++ } ++ } ++ if (!other_read && (wp.type & READ)) watchpoint_reads.erase(wp.address); ++ if (!other_write && (wp.type & WRITE)) watchpoint_writes.erase(wp.address); + watchpoints.erase(watchpoints.begin() + idx); + } diff --git a/defects/caprice32-0002/test/test_caprice32_0002 b/defects/caprice32-0002/test/test_caprice32_0002 new file mode 100755 index 000000000..e99316bf9 Binary files /dev/null and b/defects/caprice32-0002/test/test_caprice32_0002 differ diff --git a/defects/caprice32-0002/test/test_caprice32_0002.cpp b/defects/caprice32-0002/test/test_caprice32_0002.cpp new file mode 100644 index 000000000..7eedb6557 --- /dev/null +++ b/defects/caprice32-0002/test/test_caprice32_0002.cpp @@ -0,0 +1,225 @@ +// Unit test for caprice32-0002: CWE-407 watchpoint linear scan O(W) per memory access +// +// In src/z80.cpp, read_mem() and write_mem() -- called for every Z80 memory +// access -- scan the watchpoints vector with std::any_of: +// std::any_of(watchpoints.begin(), watchpoints.end(), +// [&](const auto& w) { return w.address == addr && (w.type & READ); }) +// +// This is O(W) per memory access. A typical Z80 instruction performs 1-4 memory +// accesses, so with W watchpoints we pay O(4W) per instruction. Combined with +// ~4 MHz Z80 clock, this is ~16 million O(W) probes per second. +// +// Fix: maintain two std::unordered_set -- watchpoint_reads and +// watchpoint_writes -- kept in sync with the watchpoints vector. +// Inner loop becomes: watchpoint_reads.count(addr) or watchpoint_writes.count(addr) +// which is O(1) average. +// +// Compile: g++ -std=c++17 -O2 -o test_caprice32_0002 test_caprice32_0002.cpp +// Run: ./test_caprice32_0002 + +#include +#include +#include +#include +#include + +typedef uint32_t dword; +typedef uint16_t word; + +enum WatchpointType { READ = 1, WRITE = 2, READWRITE = 3 }; + +struct Watchpoint { + Watchpoint(word val, WatchpointType t) : address(val), type(t) {} + dword address; + WatchpointType type; +}; + +// --- DEFECT: O(W) std::any_of per memory access --- +struct DefectMemory { + std::vector watchpoints; + int last_read_scanned = 0; + int last_write_scanned = 0; + + void add(Watchpoint wp) { watchpoints.push_back(wp); } + + bool read_mem(word addr) { + last_read_scanned = 0; + if (!watchpoints.empty()) { + for (const auto& w : watchpoints) { + ++last_read_scanned; + if (w.address == addr && (w.type & READ)) return true; + } + } + return false; + } + + bool write_mem(word addr) { + last_write_scanned = 0; + if (!watchpoints.empty()) { + for (const auto& w : watchpoints) { + ++last_write_scanned; + if (w.address == addr && (w.type & WRITE)) return true; + } + } + return false; + } +}; + +// --- FIX: O(1) unordered_set lookup --- +struct FixedMemory { + std::vector watchpoints; + std::unordered_set watchpoint_reads; + std::unordered_set watchpoint_writes; + int last_read_scanned = 0; + int last_write_scanned = 0; + + void add(Watchpoint wp) { + watchpoints.push_back(wp); + if (wp.type & READ) watchpoint_reads.insert(wp.address); + if (wp.type & WRITE) watchpoint_writes.insert(wp.address); + } + + void remove_at(std::size_t idx) { + const auto& wp = watchpoints[idx]; + // Only erase from sets if no other watchpoint covers this address+type. + bool other_read = false, other_write = false; + for (std::size_t i = 0; i < watchpoints.size(); i++) { + if (i == idx) continue; + if (watchpoints[i].address == wp.address) { + if (watchpoints[i].type & READ) other_read = true; + if (watchpoints[i].type & WRITE) other_write = true; + } + } + if (!other_read && (wp.type & READ)) watchpoint_reads.erase(wp.address); + if (!other_write && (wp.type & WRITE)) watchpoint_writes.erase(wp.address); + watchpoints.erase(watchpoints.begin() + idx); + } + + bool read_mem(word addr) { + if (!watchpoints.empty()) { + if (watchpoint_reads.count(addr)) { last_read_scanned = 1; return true; } + } + last_read_scanned = 0; + return false; + } + + bool write_mem(word addr) { + if (!watchpoints.empty()) { + if (watchpoint_writes.count(addr)) { last_write_scanned = 1; return true; } + } + last_write_scanned = 0; + return false; + } +}; + +static void test_defect_scans_all_on_miss() { + const int W = 500; + DefectMemory d; + for (int i = 0; i < W; ++i) + d.add(Watchpoint(word(0x4000 + i), READWRITE)); + + bool hit = d.read_mem(0x0000); + assert(!hit); + assert(d.last_read_scanned == W && "defect: all W watchpoints scanned for miss"); + printf("PASS defect: %d watchpoints, read miss -> scanned %d\n", W, d.last_read_scanned); + + hit = d.write_mem(0x0000); + assert(!hit); + assert(d.last_write_scanned == W && "defect: all W watchpoints scanned for write miss"); + printf("PASS defect: %d watchpoints, write miss -> scanned %d\n", W, d.last_write_scanned); +} + +static void test_fix_constant_work_on_miss() { + const int W = 500; + FixedMemory f; + for (int i = 0; i < W; ++i) + f.add(Watchpoint(word(0x4000 + i), READWRITE)); + + bool hit = f.read_mem(0x0000); + assert(!hit); + assert(f.last_read_scanned == 0 && "fix: O(1) hash miss costs 0 recorded scans"); + printf("PASS fix: %d watchpoints, read miss -> scanned %d (O(1))\n", W, f.last_read_scanned); + + hit = f.write_mem(0x0000); + assert(!hit); + assert(f.last_write_scanned == 0); + printf("PASS fix: %d watchpoints, write miss -> scanned %d (O(1))\n", W, f.last_write_scanned); +} + +static void test_fix_detects_read_watchpoint() { + FixedMemory f; + f.add(Watchpoint(0x2000, READ)); + f.add(Watchpoint(0x3000, WRITE)); + + assert(f.read_mem(0x2000) && "fix: READ watchpoint fires on read"); + assert(!f.read_mem(0x3000) && "fix: WRITE-only watchpoint does not fire on read"); + assert(f.write_mem(0x3000) && "fix: WRITE watchpoint fires on write"); + assert(!f.write_mem(0x2000) && "fix: READ-only watchpoint does not fire on write"); + printf("PASS fix: watchpoint type separation (READ/WRITE) correct\n"); +} + +static void test_fix_readwrite_watchpoint() { + FixedMemory f; + f.add(Watchpoint(0x1000, READWRITE)); + + assert(f.read_mem(0x1000) && "fix: READWRITE watchpoint fires on read"); + assert(f.write_mem(0x1000) && "fix: READWRITE watchpoint fires on write"); + printf("PASS fix: READWRITE watchpoint fires on both read and write\n"); +} + +static void test_fix_remove_at_cleans_sets() { + FixedMemory f; + f.add(Watchpoint(0x5000, READ)); + f.add(Watchpoint(0x6000, WRITE)); + + f.remove_at(0); // remove 0x5000/READ + + assert(!f.read_mem(0x5000) && "fix: removed watchpoint no longer fires on read"); + assert(f.write_mem(0x6000) && "fix: remaining watchpoint still fires"); + printf("PASS fix: remove_at cleans sets correctly\n"); +} + +static void test_fix_remove_shared_address_preserved() { + // Two watchpoints at same address, different types. Remove one, other survives. + FixedMemory f; + f.add(Watchpoint(0x7000, READ)); + f.add(Watchpoint(0x7000, WRITE)); + + f.remove_at(0); // remove READ watchpoint; WRITE should remain + + assert(!f.read_mem(0x7000) && "fix: READ watchpoint removed, no read hit"); + assert(f.write_mem(0x7000) && "fix: WRITE watchpoint survives after sibling removed"); + printf("PASS fix: shared-address partial remove keeps other type intact\n"); +} + +static void test_speedup_ratio() { + const int W = 500; + DefectMemory d; + FixedMemory f; + for (int i = 0; i < W; ++i) { + word addr = word(0xC000 + i); + d.add(Watchpoint(addr, READWRITE)); + f.add(Watchpoint(addr, READWRITE)); + } + + d.read_mem(0x0000); + f.read_mem(0x0000); + + printf("PASS speedup: defect=%d scans/read, fix=%d scans/read, ratio ~%dx\n", + d.last_read_scanned, f.last_read_scanned, d.last_read_scanned); + assert(d.last_read_scanned == W); + assert(f.last_read_scanned == 0); +} + +int main() { + printf("--- caprice32-0002 CWE-407 watchpoint O(W) scan per memory access ---\n"); + test_defect_scans_all_on_miss(); + test_fix_constant_work_on_miss(); + test_fix_detects_read_watchpoint(); + test_fix_readwrite_watchpoint(); + test_fix_remove_at_cleans_sets(); + test_fix_remove_shared_address_preserved(); + test_speedup_ratio(); + printf("ALL PASS\n"); + return 0; +}