Repository navigation
fix: enforce regmap size invariant to close OOB writes (#64, #65) - #90
Merged
Merged
Conversation
Ep0's per-endpoint access checks (check_read_access, check_write_access, set_endpoint_owner) all bound a target endpoint against endpoint_owner_, which is sized from RegisterMap::endpoint_count at construction. The actual writes in write_generic_config/write_functional_config land in regs_.generic_configs/regs_.functional_configs instead, which were never required to be the same length as endpoint_count. A RegisterMap with endpoint_count larger than those vectors let an in-range access check be followed by an out-of-bounds vector write (cpp-RCP-N2-01, #64). write_whole_map compounded this: it could install a replacement map with a different endpoint_count without resizing endpoint_owner_ to match, so a stale bounds check from the old map size could pass and then index out of bounds on the new, differently-sized vectors, or new endpoints could never get an owner assigned (cpp-RCP-N2-02, #65). Both are fixed via a shared size_invariant_holds() check (generic_configs .size() == functional_configs.size() == endpoint_count): the Ep0 constructor fails closed (sizes endpoint_owner_ to 0, rejecting all per-endpoint access) when a RegisterMap violates it, and write_whole_map now rejects an inconsistent replacement map outright and resizes endpoint_owner_ to match a valid one. write_generic_config/ write_functional_config additionally bounds-check against the actual vector being indexed as a second, independent layer of defense. Verified against AddressSanitizer/UndefinedBehaviorSanitizer: standalone repros reproduced a real SEGV/heap-buffer-overflow on the pre-fix code for both issues, and are clean after the fix. Added regression tests covering both fail-closed construction and write_whole_map resize/reject behavior, and a new "ASan/UBSan (regmap)" CI job that builds and runs test_regmap under -fsanitize=address,undefined on every PR. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
5 of 6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two memory-safety issues in
rcp::regmap::Ep0(include/rcp/regmap.hpp):Ep0's per-endpoint access checks (check_read_access,check_write_access,set_endpoint_owner) all bound a target endpoint againstendpoint_owner_, which is sized fromRegisterMap::endpoint_countat construction. The actual writes inwrite_generic_config/write_functional_configland inregs_.generic_configs/regs_.functional_configsinstead — two vectors never required to be the same length asendpoint_count. ARegisterMapconstructed withendpoint_countlarger than those vectors (including the default-constructed empty-vector case) let an in-range access check pass and the subsequent write go out of bounds.write_whole_maplet the root client move-assign an entirely newRegisterMapin with no check that itsgeneric_configs/functional_configssizes matched its ownendpoint_count, and never resizedendpoint_owner_to match. A smaller replacement leftendpoint_owner_stale-and-too-large (OOB write on subsequent per-endpoint write); a larger replacement left it too small (new endpoints could never get an owner — silent DoS).Fix
Both share one root invariant:
generic_configs.size() == functional_configs.size() == endpoint_count. Added a shared privatesize_invariant_holds()check:Ep0constructor now fails closed — if the incomingRegisterMapviolates the invariant,endpoint_owner_is sized to 0 instead of the untrustworthyendpoint_count, so every non-EP0 access check returnsINVALID_PARAMETERinstead of a false in-range verdict. A newis_endpoint_table_consistent()accessor lets callers detect this degraded state.write_whole_mapnow validates the replacement map's invariant up front and rejects an inconsistent one withINVALID_PARAMETER(leaving the live map untouched), and resizes/resetsendpoint_owner_to match the newendpoint_counton acceptance — prior per-endpoint owner assignments are discarded since they describe endpoints in the map being replaced, not necessarily the same endpoints in the new one.write_generic_config/write_functional_configadditionally bounds-check against the actual vector being indexed (not justcheck_write_access'sendpoint_owner_-based verdict) as a second, independent layer of defense.No exceptions introduced — everything still returns
std::error_code/RegMapErrc::invalid_parameter, matching the rest of this class.Sanitizer verification (per the "prove it first" requirement)
I could not add a sanitizer build to a
RCP_BUILD_TESTSCMake target and get a crash out of the existingtest_regmap.cppsuite as it stood pre-fix — every existing test already buildsRegisterMaps with consistent vector sizes viamake_map(), so the existing suite never exercised the mismatched-size path at all (that's the bug: nothing enforced it). So, per the instructions, I wrote small standalone repros compiled directly against the unfixed header withclang++ -fsanitize=address,undefined, ran them, and confirmed a real crash before touching any code, then re-ran them after the fix.Before fix — heap-buffer-overflow (issue #64, undersized
generic_configs)(A second variant with a fully empty/default-constructed
generic_configs/functional_configs— the more literal reading of "endpoint_count nonzero, config vectors default-constructed" from the issue — hit a null-pointer SEGV instead, also caught by ASan/UBSan:AddressSanitizer: SEGV ... in std::vector<unsigned short>::__vdeallocate(), plus several UBSan "member call on null pointer" reports leading up to it.)Before fix — heap-buffer-overflow (issue #65,
write_whole_mapdesync)Repro:
Ep0constructed against a consistent 4-endpoint map, root client installs a smaller (1-endpoint, consistent) replacement viawrite_whole_map;endpoint_owner_stayed at its stale size of 4 pre-fix, so a subsequentwrite_generic_config(target=4)passed the stale bounds check and indexed 3 slots past the new 1-elementgeneric_configs.After fix — all three repros are ASan/UBSan-clean
I then also built the real
test_regmapbinary (including the new regression tests below) via CMake with-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"(matching the new CI job below) and ran it directly:No ASan/UBSan reports, clean exit.
Regression tests (
tests/test_regmap.cpp)RegisterMapwithendpoint_count=4and emptygeneric_configs/functional_configsnow fails closed:is_endpoint_table_consistent()is false, andset_endpoint_owner/check_read_access/write_generic_config/write_functional_configall returnINVALID_PARAMETERinstead of an in-range verdict.endpoint_count=4, vectors resized to 1).write_whole_maprejects a replacement map whose owngeneric_configs/functional_configsdisagree with itsendpoint_count, leaving the live map untouched.write_whole_mapwith a valid replacement correctly resizesendpoint_owner_: growing (2→4 endpoints) discards stale owner assignments and lets the root client write the newly-valid endpoint 4; shrinking (4→1) makes endpoint 4INVALID_PARAMETERagain rather than leaving a stale in-range verdict.All new tests are tagged with existing requirement IDs (
REQ-REGMAP-005,REQ-REGMAP-013) since they reinforce those existing invariants more strictly rather than introducing new requirements —.fusa-reqs.jsonwas intentionally left untouched, per this task's file-scope restriction toinclude/rcp/regmap.hpp,tests/test_regmap.cpp, and (additively).github/workflows/ci.yml.CI
Added a new, gating "ASan/UBSan (regmap)" job to
.github/workflows/ci.yml(no|| true): configures a separatebuild-asanCMake dir with-fsanitize=address,undefined, builds onlytest_regmap, and runs it withhalt_on_error=1. Purely additive — no existing job touched or removed. Scoped totest_regmapper the task's "minimal/fast" guidance rather than sanitizer-ifying the whole suite.Verification checklist
cmake -B build -DCMAKE_BUILD_TYPE=Debug -G Ninja && cmake --build build --parallelsucceedsctest --test-dir build --output-on-failure— 53/53 tests pass (including 4 new regression tests intest_regmap)test_regmapbuilt and run under-fsanitize=address,undefinedvia CMake — clean, 132 assertions / 23 test casesclang-tidydiff-checked against the unfixed header with an explicit broad check set (the project has no.clang-tidyand CI's own clang-tidy step is non-gating via|| true, so I verified manually): identical set of warning categories before/after, no new category introduced by this diffLeft unresolved / out of scope
.fusa-reqs.jsonwas not touched (file-scope restriction for this task); the new tests trace to existingREQ-REGMAP-005/REQ-REGMAP-013instead of new requirement IDs.regmap.hppper the assigned issues.Closes #64, closes #65.