Repository navigation
fix: v2.23.0 — §13.1 generic-config privilege escalation + fault-injection use-after-free - #101
Merged
Conversation
…ction use-after-free Two unrelated critical defects, both found by an independent audit of this repository, fixed together because neither is large. 1. Privilege escalation on the generic config block (cpp-RCP-D2) §13.1 grants an RC Client other than the ROOT_CLIENT write access only to the functional config (EP_FUNC_config) of the endpoints allocated to it; §13.2 frames the counterpart block as the generic part of the endpoint register map, owned by the RC Server. Ep0::check_write_access collapsed that distinction: it applied one identical owner-based authorization test to both ConfigBlock::Generic and ConfigBlock::Functional, with only the *lock* branch — not the *authorization* branch — differing by block. write_generic_config therefore succeeded for any non-root client that merely owned the target endpoint. That is an escalation rather than a cosmetic gap because of what the generic block holds: hw_pin_indices (which physical pins the endpoint claims), request_queue_size/response_queue_size, and the per-endpoint ep_req_crc_enable/ep_ack_crc_enable/ep_response_crc_enable E2E-CRC toggles added at v2.6.0. A non-root client could repoint an owned endpoint's hardware pin mapping or silently switch off a safety mechanism — data the specification reserves to the RC Server and the root client. Authorization now depends on the targeted block: generic-block writes require the root client, functional-block writes accept root-or-owner. `block` keeps defaulting to ConfigBlock::Generic, which is now the stricter of the two, so a call site that forgets to name a block fails closed. write_functional_config, write_whole_map, check_read_access and the cpp-RCP-N2-01/N2-02 size invariants are untouched. tests/test_regmap.cpp's "A non-root client may only write the endpoint config it owns" was asserting the bug as correct behavior — it wrote the *generic* block as a non-root owner and required success. It is split in two: the owner grant is retested against the functional block where it belongs, and a new case pins the generic block as root-only, exercising write_generic_config and check_write_access directly (including the default-argument path) and asserting the refused write left the config untouched. The generic-block lock test now claims the root client first so the lock, not the new authorization check, is still what refuses that write. 2. Use-after-free in faultinject::Interceptor::pick_rule (cpp-RCP-D6) pick_rule returned a Rule* into its own std::vector<Rule> and erased that very element before returning whenever the firing exhausted the rule's count. send() then dereferenced the erased element to read type and latency. Erasing a trailing element does not reallocate on common libstdc++/libc++ builds, so unsanitized builds read stale-but-mapped memory and appeared to work — which is why the existing count-expiry test never failed. The same pointer was also a data race: rules_ is guarded only for the duration of pick_rule(), so send() read into the container with no lock held while another thread could be reallocating it via add_rule, emptying it via clear_rules, or expiring rules of its own — in a class that holds a std::mutex precisely because it is meant to be used concurrently. pick_rule now returns std::optional<Rule>, copied out while the lock is still held and before any erase; send() acts on that value. Rule is four scalars, so the copy is free. Verification Full local build and ctest (53/53 suites) pass. Both fixes were mutation- checked. Reverting the regmap authorization to the pre-fix owner-only test fails the new generic-block case. Building tests/test_faultinject with -fsanitize=address,undefined against the pre-fix header aborts with "AddressSanitizer: container-overflow" at the switch (rule->type) in send() — reached from the pre-existing "Slow rule adds latency" case, from the new single-shot expiry case, and from a worker thread in the new concurrent case — and passes clean against the fixed header. That the existing suite already reproduced the abort yet CI stayed green is itself a finding: the ASan/UBSan job was scoped to test_regmap alone. It now builds and runs test_faultinject under the same flags, so this class of defect is gated rather than fixed once. - regmap.hpp: block-dependent authorization in check_write_access; header and Ep0 class comments corrected (they described the collapsed rule). - faultinject.hpp: pick_rule returns std::optional<Rule>; <optional> included. - test_regmap.cpp: owner-grant case rebased onto the functional block; new generic-block root-only case; generic-lock case claims root first. - test_faultinject.cpp: new same-call-expiry case (a Slow rule with count=1 must still apply its configured latency) and a new 8-thread concurrent case retiring 256 single-shot rules. - ci.yml: ASan/UBSan job extended to test_faultinject. - .fusa-reqs.json: REQ-REGMAP-005 restated with the Generic/Functional asymmetry (it encoded the buggy rule verbatim); REQ-REGMAP-006 no longer implies an owner may write the generic block; REQ-FI-005 states the copy-under-lock contract. - README.md/ROADMAP.md: milestone 67 added, GA renumbered to 68. No wire-format change and no public API signature change; pick_rule is private. The one observable behavior change is the intended one: a non-root client's generic-config write that previously succeeded now returns UNAUTHORIZED_ACCESS. kVersion/CMakeLists.txt bump 2.22.0 -> 2.23.0. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
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.
Two unrelated critical defects, both found by an independent audit of this repository, fixed together because neither is large.
1. Privilege escalation on the generic config block (cpp-RCP-D2)
§13.1 grants an RC Client other than the ROOT_CLIENT write access only to the functional config (
EP_FUNC_config) of the endpoints allocated to it; §13.2 frames the counterpart block as the generic part of the endpoint register map, owned by the RC Server.Ep0::check_write_accesscollapsed that distinction. It applied one identical owner-based authorization test to bothConfigBlock::GenericandConfigBlock::Functional— only the lock branch, not the authorization branch, differed by block:So
write_generic_configsucceeded for any non-root client that merely owned the target endpoint.That is an escalation rather than a cosmetic conformance gap because of what the generic block holds:
hw_pin_indices(which physical pins the endpoint claims),request_queue_size/response_queue_size, and the per-endpointep_req_crc_enable/ep_ack_crc_enable/ep_response_crc_enableE2E-CRC toggles added at v2.6.0. A non-root client could repoint an owned endpoint's hardware pin mapping or silently switch off a safety mechanism — data the specification reserves to the RC Server and the root client.Fix. Authorization now depends on the targeted block: generic-block writes require the root client, functional-block writes accept root-or-owner.
blockkeeps defaulting toConfigBlock::Generic, which is now the stricter of the two, so a call site that forgets to name a block fails closed.write_functional_config,write_whole_map,check_read_accessand the cpp-RCP-N2-01/N2-02 size invariants are untouched.The existing test certified the bug.
tests/test_regmap.cpp's "A non-root client may only write the endpoint config it owns" wrote the generic block as a non-root owner and required success. It is split in two: the owner grant is retested against the functional block where it belongs, and a new case pins the generic block as root-only — exercisingwrite_generic_configandcheck_write_accessdirectly (including the default-argument path), and asserting the refused write left the config untouched. The generic-block lock test now claims the root client first, so the lock rather than the new authorization check is still what refuses that write.2. Use-after-free in
faultinject::Interceptor::pick_rule(cpp-RCP-D6)pick_rulereturned aRule*into its ownstd::vector<Rule>and erased that very element before returning whenever the firing exhausted the rule's count:send()then dereferences it immediately (rule->type,rule->latency). Erasing a trailing element does not reallocate on common libstdc++/libc++ builds, so unsanitized builds read stale-but-mapped memory and appeared to work — which is why the existing count-expiry test never failed.The same pointer was also a data race:
rules_is guarded only for the duration ofpick_rule(), sosend()read into the container with no lock held while another thread could be reallocating it viaadd_rule, emptying it viaclear_rules, or expiring rules of its own — in a class that holds astd::mutexprecisely because it is meant to be used concurrently.Fix.
pick_rulereturnsstd::optional<Rule>, copied out while the lock is still held and before any erase;send()acts on that value.Ruleis four scalars, so the copy is free.Verification
Full local build and
ctestpass, 53/53 suites. Both fixes were mutation-checked.cpp-RCP-D2. Reverting the authorization to the pre-fix owner-only test (
is_root_client(client) || owns) fails the new generic-block case:cpp-RCP-D6. Building
tests/test_faultinjectwith-fsanitize=address,undefinedagainst the pre-fix header aborts:Line 88 is the
switch (rule->type). The abort reproduces from three independent call sites: the pre-existing "Slow rule adds latency" case (count = 1), the new single-shot expiry case, and — on a worker thread (thread T5) — the new 8-thread concurrent case. Against the fixed header, bothtest_faultinjectandtest_regmappass clean under ASan/UBSan withhalt_on_error=1.That the existing suite already reproduced the abort yet CI stayed green is itself a finding: the ASan/UBSan job was scoped to
test_regmapalone. It now builds and runstest_faultinjectunder the same flags, so this class of defect is gated rather than fixed once.Changes
include/rcp/regmap.hpp— block-dependent authorization incheck_write_access; header andEp0class comments corrected (both described the collapsed rule).include/rcp/faultinject.hpp—pick_rulereturnsstd::optional<Rule>;<optional>included.tests/test_regmap.cpp— owner-grant case rebased onto the functional block; new generic-block root-only case; generic-lock case claims root first.tests/test_faultinject.cpp— new same-call-expiry case (aSlowrule withcount = 1must still apply its configured latency after being retired) and a new 8-thread case retiring 256 single-shot rules..github/workflows/ci.yml— ASan/UBSan job extended totest_faultinject..fusa-reqs.json— REQ-REGMAP-005 restated with the Generic/Functional asymmetry (it encoded the buggy rule verbatim); REQ-REGMAP-006 no longer implies an owner may write the generic block; REQ-FI-005 states the copy-under-lock contract.README.md/ROADMAP.md— milestone 67 added, GA renumbered to 68.Compatibility
No wire-format change and no public API signature change;
pick_ruleis private, so its return type is an implementation detail. The one observable behavior change is the intended one: a non-root client's generic-config write that previously succeeded now returnsUNAUTHORIZED_ACCESS.kVersion/CMakeLists.txtbump 2.22.0 → 2.23.0, the same minor-bump convention milestones 63/64/65/66 used during this pre-GA v2.x line.