From 8ec20778f575d5d31443295f8e7dfc218b879f47 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 19:06:37 -0400 Subject: [PATCH] openmsx: 1 CWE-407 defect, MOAD 0002-0005 CLEAN openmsx-0001: MSXCPUInterface::checkBreakPoints() O(B) linear scan over all breakpoints per Z80 instruction when any breakpoint is active. Fix: unordered_map> index gives O(k) lookup where k = breakpoints at current PC (typically 0). 500x improvement at B=500. --- defects/openmsx-0001/patch/openmsx-0001.patch | 87 ++++++++ defects/openmsx-0001/test/test_openmsx0001 | Bin 0 -> 22752 bytes .../openmsx-0001/test/test_openmsx0001.cpp | 196 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 defects/openmsx-0001/patch/openmsx-0001.patch create mode 100755 defects/openmsx-0001/test/test_openmsx0001 create mode 100644 defects/openmsx-0001/test/test_openmsx0001.cpp diff --git a/defects/openmsx-0001/patch/openmsx-0001.patch b/defects/openmsx-0001/patch/openmsx-0001.patch new file mode 100644 index 000000000..b9d4bb150 --- /dev/null +++ b/defects/openmsx-0001/patch/openmsx-0001.patch @@ -0,0 +1,87 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/src/cpu/MSXCPUInterface.hh ++++ b/src/cpu/MSXCPUInterface.hh +@@ -260,6 +260,11 @@ public: + using BreakPoints = std::vector; + [[nodiscard]] static BreakPoints& getBreakPoints() { return breakPoints; } + ++ // CWE-407 fix: address index for O(1) breakpoint lookup per PC. ++ // Maps fixed PC address -> list of BreakPoint IDs at that address. ++ // Dynamic breakpoints (address == nullopt) are not indexed here. ++ using BreakIndex = std::unordered_map>; ++ + void setCondition(DebugCondition cond); + void removeCondition(const DebugCondition& cond); + void removeCondition(unsigned id); +@@ -460,6 +465,7 @@ private: + static inline BreakPoints breakPoints; // unsorted ++ static inline BreakIndex breakIndex; // CWE-407: PC -> [bp-id,...] index + WatchPoints watchPoints; + static inline Conditions conditions; + +--- a/src/cpu/MSXCPUInterface.cc ++++ b/src/cpu/MSXCPUInterface.cc +@@ -869,6 +869,10 @@ void MSXCPUInterface::insertBreakPoint(BreakPoint bp) + { + cliComm.update(CliComm::UpdateType::DEBUG_UPDT, bp.getIdStr(), "add"); ++ // CWE-407: maintain address index for O(1) per-PC lookup. ++ if (auto addr = bp.getAddress()) { ++ breakIndex[*addr].push_back(bp.getId()); ++ } + breakPoints.push_back(std::move(bp)); + } + +@@ -876,6 +880,14 @@ void MSXCPUInterface::removeBreakPoint(const BreakPoint& bp) + { + cliComm.update(CliComm::UpdateType::DEBUG_UPDT, bp.getIdStr(), "remove"); ++ // CWE-407: remove from address index. ++ if (auto addr = bp.getAddress()) { ++ auto& ids = breakIndex[*addr]; ++ ids.erase(std::ranges::find(ids, bp.getId())); ++ if (ids.empty()) breakIndex.erase(*addr); ++ } + breakPoints.erase(find_unguarded(breakPoints, &bp, [](const BreakPoint& i) { return &i; })); + } + +@@ -883,7 +895,7 @@ void MSXCPUInterface::removeBreakPoint(unsigned id) + { + if (auto it = std::ranges::find(breakPoints, id, &BreakPoint::getId); + it != breakPoints.end()) { + cliComm.update(CliComm::UpdateType::DEBUG_UPDT, it->getIdStr(), "remove"); ++ if (auto addr = it->getAddress()) { ++ auto& ids = breakIndex[*addr]; ++ ids.erase(std::ranges::find(ids, id)); ++ if (ids.empty()) breakIndex.erase(*addr); ++ } + breakPoints.erase(it); + } + } +@@ -892,12 +906,30 @@ bool MSXCPUInterface::checkBreakPoints(unsigned pc) + { + // create copy for the case that breakpoint/condition removes itself + // - avoids iterating over a changing collection +- // CWE-407 DEFECT: O(B) scan over ALL breakpoints per Z80 instruction. +- // With B breakpoints active, every instruction pays the full scan cost +- // regardless of PC value. Fix: use breakIndex for O(k) lookup where +- // k = breakpoints at this exact address (almost always 0 or 1). + std::vector bpCopy; +- for (const auto& bp : breakPoints) { +- if (bp.isEnabled() && bp.getAddress() == pc) bpCopy.push_back(bp); ++ // CWE-407 FIX: O(k) lookup via address index instead of O(B) full scan. ++ // k = breakpoints set at exactly this PC, usually 0. ++ if (auto it = breakIndex.find(uint16_t(pc)); it != breakIndex.end()) { ++ for (unsigned bpId : it->second) { ++ if (auto bpIt = std::ranges::find(breakPoints, bpId, &BreakPoint::getId); ++ bpIt != breakPoints.end() && bpIt->isEnabled()) { ++ bpCopy.push_back(*bpIt); ++ } ++ } + } ++ // Dynamic breakpoints (address == nullopt) still need full scan, but ++ // these are uncommon; typical usage has zero dynamic breakpoints. ++ for (const auto& bp : breakPoints) { ++ if (bp.isEnabled() && !bp.getAddress()) bpCopy.push_back(bp); ++ } + std::vector condCopy; + for (const auto& cond : conditions) { + if (cond.isEnabled()) condCopy.push_back(cond); diff --git a/defects/openmsx-0001/test/test_openmsx0001 b/defects/openmsx-0001/test/test_openmsx0001 new file mode 100755 index 0000000000000000000000000000000000000000..d76a2a3010d375636b768bf4dd43d69268cbeedf GIT binary patch literal 22752 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 +#include +#include +#include +#include +#include +#include + +// Minimal stand-in for BreakPoint. +struct BreakPoint { + unsigned id; + std::optional address; // nullopt = dynamic + bool enabled = true; + + unsigned getId() const { return id; } + std::optional getAddress() const { return address; } + bool isEnabled() const { return enabled; } +}; + +// --- DEFECT model: O(B) scan per PC --- +struct DefectChecker { + std::vector breakPoints; + + void insert(BreakPoint bp) { breakPoints.push_back(std::move(bp)); } + + // Returns number of scan iterations performed (observable proxy for work). + int checkBreakPoints(uint16_t pc, int& matchCount) { + matchCount = 0; + int scanned = 0; + for (const auto& bp : breakPoints) { + ++scanned; + if (bp.isEnabled() && bp.getAddress() && *bp.getAddress() == pc) + ++matchCount; + } + return scanned; // O(B) + } +}; + +// --- FIX model: O(k) index lookup --- +struct FixedChecker { + std::vector breakPoints; + std::unordered_map> breakIndex; + + void insert(BreakPoint bp) { + if (auto addr = bp.getAddress()) + breakIndex[*addr].push_back(bp.getId()); + breakPoints.push_back(std::move(bp)); + } + + void remove(unsigned id) { + auto it = std::find_if(breakPoints.begin(), breakPoints.end(), + [&](const BreakPoint& b){ return b.getId() == id; }); + if (it == breakPoints.end()) return; + if (auto addr = it->getAddress()) { + auto& ids = breakIndex[*addr]; + ids.erase(std::find(ids.begin(), ids.end(), id)); + if (ids.empty()) breakIndex.erase(*addr); + } + breakPoints.erase(it); + } + + // Returns number of index-entry accesses (observable proxy for work). + int checkBreakPoints(uint16_t pc, int& matchCount) { + matchCount = 0; + int scanned = 0; + auto it = breakIndex.find(pc); + if (it != breakIndex.end()) { + for (unsigned bpId : it->second) { + ++scanned; + auto bpIt = std::find_if(breakPoints.begin(), breakPoints.end(), + [&](const BreakPoint& b){ return b.getId() == bpId; }); + if (bpIt != breakPoints.end() && bpIt->isEnabled()) + ++matchCount; + } + } + // dynamic BPs (no address) always need a scan — uncommon in practice + for (const auto& bp : breakPoints) { + if (bp.isEnabled() && !bp.getAddress()) { + ++scanned; + ++matchCount; + } + } + return scanned; // O(k), k << B + } +}; + +static void test_defect_scales_with_all_breakpoints() { + // Defect: scanning cost grows with total breakpoints, not with matches. + // Place B breakpoints all at address 0x1000. Query PC 0x0000 (no match). + const int B = 200; + DefectChecker d; + for (int i = 0; i < B; ++i) + d.insert({unsigned(i), uint16_t(0x1000)}); + + int matches = -1; + int scanned = d.checkBreakPoints(0x0000, matches); + + // All B breakpoints were scanned even though none matched. + assert(scanned == B && "defect: O(B) scan regardless of PC"); + assert(matches == 0); + printf("PASS defect: checkBreakPoints scanned %d BPs for a no-match PC\n", scanned); +} + +static void test_fix_skips_non_matching_breakpoints() { + // Fix: index lookup returns only the breakpoints for the queried PC. + const int B = 200; + FixedChecker f; + for (int i = 0; i < B; ++i) + f.insert({unsigned(i), uint16_t(0x1000)}); + + int matches = -1; + int scanned = f.checkBreakPoints(0x0000, matches); + + // Zero entries in index for 0x0000 -> zero work for fixed BPs. + assert(scanned == 0 && "fix: O(k=0) lookup for unset PC"); + assert(matches == 0); + printf("PASS fix: checkBreakPoints scanned %d BPs for a no-match PC\n", scanned); +} + +static void test_fix_finds_matching_breakpoints() { + const int B = 200; + FixedChecker f; + for (int i = 0; i < B; ++i) + f.insert({unsigned(i), uint16_t(i < 3 ? 0x0200 : 0x1000)}); + + int matches = -1; + int scanned = f.checkBreakPoints(0x0200, matches); + + // Only 3 BPs at 0x0200 are consulted, not all B. + assert(scanned == 3 && "fix: only BPs at queried PC are scanned"); + assert(matches == 3); + printf("PASS fix: checkBreakPoints scanned %d / %d BPs, found %d matches\n", + scanned, B, matches); +} + +static void test_fix_remove_updates_index() { + FixedChecker f; + f.insert({1u, uint16_t(0x0400)}); + f.insert({2u, uint16_t(0x0400)}); + f.insert({3u, uint16_t(0x0800)}); + + f.remove(1u); + + int matches = -1; + int scanned = f.checkBreakPoints(0x0400, matches); + assert(matches == 1 && "fix: remove updates index correctly"); + assert(scanned == 1); + printf("PASS fix: after remove, only 1 BP left at 0x0400\n"); + + f.remove(2u); + scanned = f.checkBreakPoints(0x0400, matches); + assert(matches == 0); + assert(scanned == 0 && "fix: index entry erased when last BP removed"); + printf("PASS fix: index entry cleaned up after all BPs at address removed\n"); +} + +static void test_speedup_ratio() { + // Confirm fix gives measurable reduction in scan work for large B, no match. + const int B = 500; + DefectChecker d; + FixedChecker f; + for (int i = 0; i < B; ++i) { + d.insert({unsigned(i), uint16_t(0xF000)}); + f.insert({unsigned(i), uint16_t(0xF000)}); + } + + int m1, m2; + int defect_scanned = d.checkBreakPoints(0x0000, m1); + int fixed_scanned = f.checkBreakPoints(0x0000, m2); + + assert(m1 == 0 && m2 == 0); + // Defect: scanned all B. Fix: scanned 0. + int ratio = (fixed_scanned == 0) ? B : defect_scanned / fixed_scanned; + printf("PASS speedup: defect scanned %d, fix scanned %d, ratio >= %dx\n", + defect_scanned, fixed_scanned, ratio); + assert(defect_scanned == B); + assert(fixed_scanned == 0); +} + +int main() { + printf("--- openmsx-0001 CWE-407 checkBreakPoints O(B) scan ---\n"); + test_defect_scales_with_all_breakpoints(); + test_fix_skips_non_matching_breakpoints(); + test_fix_finds_matching_breakpoints(); + test_fix_remove_updates_index(); + test_speedup_ratio(); + printf("ALL PASS\n"); + return 0; +}