Skip to content

fix: apply RELAY spec v0.2 update — ErrAlreadyExists standalone - #9

Merged
SoundMatt merged 1 commit into
mainfrom
feat/relay-spec-update
Jun 17, 2026
Merged

SoundMatt merged 1 commit into
mainfrom
feat/relay-spec-update

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

Applies the RELAY spec v0.2 update notified in issue #8.

The RELAY spec maintainer updated §5.4 in response to issues raised during cpp-RCP's RELAY conformance implementation (PR #7). The key change:

ErrAlreadyExists is now a standalone RCP error, not mapped to any relay sentinel.

Previously: ErrAlreadyExists wrapped relay::ErrClosed (semantically inconsistent — a uniqueness-violation error doesn't represent a closed-connection condition).
Updated: ErrAlreadyExists has no relay mapping; it is protocol-specific with no equivalent relay sentinel.

Changes

  • rcp_category()::equivalent(): already_exists case now returns false for all relay conditions
  • tests/test_relay.cpp: new test asserting ErrAlreadyExists != all four relay sentinels

Test plan

  • Local build passes, 24 test cases / 55 assertions in rcp_relay
  • CI matrix

…elay mapping

RELAY spec maintainer updated §5.4 in response to issue #8 (RELAY issue #8):
ErrAlreadyExists is now a standalone RCP error, not mapped to any relay sentinel.

- rcp_category()::equivalent(): remove already_exists → ErrClosed mapping
- test_relay.cpp: add test asserting ErrAlreadyExists != all relay sentinels

Also adds the standalone ErrAlreadyExists test (24 test cases / 55 assertions).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit 5e3c4a9 into main Jun 17, 2026
19 checks passed
@SoundMatt
SoundMatt deleted the feat/relay-spec-update branch June 17, 2026 00:12
SoundMatt added a commit that referenced this pull request Aug 22, 2026
…177)

Bug (HIGH, confirmed against source): rcp::loan::BufferPool::loan()'s
released rcp::Loan captured `this` (the BufferPool*) by raw pointer in its
release closure. rcp::Loan::~Loan() unconditionally invokes that closure
when the Loan is destroyed. BufferPool::~BufferPool() did not track or wait
for outstanding Loans, so a Loan that outlived the BufferPool it was drawn
from would run the release closure against an already-destroyed
BufferPool: locking a destroyed std::mutex and writing through a dangling
pointer. Genuine use-after-free, not previously documented anywhere in the
file as a precondition, and not exercised by any test.

Fix: split BufferPool's shared internal state (the fixed-capacity free
list, its mutex, entries_len_, and the fault_injector_ pointer) into a
private Impl struct owned via std::shared_ptr<Impl>. BufferPool itself is
now a thin wrapper holding that shared_ptr. loan()'s release closure
captures the shared_ptr<Impl> *by value* instead of `this`, so a Loan's own
copy of the shared_ptr keeps Impl's storage alive by refcount for as long
as the Loan exists, independent of the BufferPool wrapper's own lifetime.
Impl's storage is only actually freed once its last owner (the BufferPool
wrapper or the release closure of the last outstanding Loan) releases it.

Public API surface unchanged: same constructors, loan(), close(), ok(),
pooled_count() signatures; new_buffer_pool() unaffected. close()'s
documented idempotent/"safe with outstanding Loans" behavior is preserved
exactly -- it now just sets a flag on Impl rather than on the BufferPool
itself, with identical observable behavior. Updated the file's header/class
doc comments to describe the new, now-safe lifetime contract (a Loan may
safely outlive its BufferPool) instead of only documenting the old hazard.

Test: added "A Loan may safely outlive the BufferPool it was drawn from"
to tests/test_loan.cpp -- the literal audit repro: loan() a buffer from a
BufferPool constructed in a nested scope into a std::unique_ptr<rcp::Loan>
declared in the outer scope, let the BufferPool destruct, then reset the
Loan. Kept the existing close()-with-outstanding-Loans test unchanged to
confirm that documented behavior still holds after this refactor.

Verification:
- Clean rebuild (cmake -DRCP_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug):
  0 errors, 0 warnings under this project's -Wall -Wextra -Wpedantic
  -Wshadow -Wnon-virtual-dtor -Wold-style-cast -Wcast-align -Wunused
  -Woverloaded-virtual flags.
- ctest: 100% tests passed, 58/58 suites (test_loan: 119 assertions in
  10 test cases, including the new regression test).
- Sanitizer (load-bearing check for a UAF fix): reproduced this repo's CI
  asan-ubsan-regmap job configuration exactly -- ubuntu-22.04, clang-14,
  `-fsanitize=address,undefined -fno-omit-frame-pointer -g` /
  `-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"`,
  ASAN_OPTIONS=halt_on_error=1, UBSAN_OPTIONS=halt_on_error=1 -- via Docker,
  and ran test_loan under it. Clean: "All tests passed (119 assertions in
  10 test cases)".
- Mutation-testing sanity check: temporarily reverted to the raw-`this`-
  capture version, rebuilt under the identical sanitizer configuration, and
  ran the new regression test. ASan caught it immediately:

    ==4341==ERROR: AddressSanitizer: stack-use-after-scope on address
    0xffffe8c00558 at pc 0xaaaae9680b88 bp 0xffffe8bffc30 sp 0xffffe8bffc28
    READ of size 8 at 0xffffe8c00558 thread T0
        #0 ... in rcp::loan::BufferPool::loan(...)::'lambda'()::operator()()
           /work/include/rcp/loan.hpp:152:21
        ...
        #5 ... in rcp::Loan::~Loan() /work/include/rcp/rcp.hpp:123:29
        ...
        #9 ... in CATCH2_INTERNAL_TEST_12() /work/tests/test_loan.cpp:133:14
        ...
    Address 0xffffe8c00558 is located in stack of thread T0 at offset 632
    in frame
        #0 ... CATCH2_INTERNAL_TEST_12() /work/tests/test_loan.cpp:113
      This frame has 24 object(s):
        [32, 40) 'loan_out' (line 123)
        [64, 648) 'pool' (line 125) <== Memory access at offset 632 is
                                          inside this variable
        ...
    SUMMARY: AddressSanitizer: stack-use-after-scope
    /work/include/rcp/loan.hpp:152:21 in
    rcp::loan::BufferPool::loan(...)::'lambda'()::operator()()
    ==4341==ABORTING

  Reapplied the fix, rebuilt under the same sanitizer configuration again:
  clean pass, "All tests passed (119 assertions in 10 test cases)".

Closes a finding from the cpp-RCP v3.0.0 deep audit (loan.hpp
BufferPool::loan()/~BufferPool() use-after-free on Loan-outlives-pool).

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant