Wave 1 (A+ campaign): cpp breaking legs#47
Merged
Merged
Conversation
Memory-safety, RELAY liveness, pagination, cred/CA contract + sanitizer lane.
CPP-1 (UAF, HIGH): the Action registry stored a raw Action* to a stack-local
Action that execute_action() returned by value — the pointer dangled the moment
the function returned, so a later resolve_all_actions()/find_action()
dereferenced freed memory. Action is shared_ptr-backed, so the registry now
OWNS its entries by value (a copy that SHARES SharedState with the caller's
copy). Proven RED under ASAN (stack-use-after-scope) then GREEN.
CPP-2 (data race, HIGH): on_event() push_back'd event_handlers with no lock
while dispatch_event() iterated it on the WS reader thread (a concurrent
push_back could realloc the buffer mid-iteration). Guard both with
handlers_mutex; dispatch snapshots under the lock then invokes without it.
CPP-3 (A2 relay-contract): RelayClient::execute() now RAISES a RelayError on a
non-2xx result code (mirroring python relay.client.execute); Call::_execute
swallows only 404/410 (call gone) and PROPAGATES every other error instead of
the old silent resolve("error"). A failed verb now surfaces honestly. Also
added a configurable RelayConfig.request_timeout_ms (F2.2 black-hole: a silent
peer errors within a bound instead of the fixed 30s). This un-hid a real
test-data bug: the calling.tap tests sent tap={type} missing the schema-required
params — fixed to tap={type,params} (matches the python reference tap shape).
CPP-4 (pagination): PaginatedIterator terminated on an empty data array even
when links.next was present, silently dropping every subsequent page; and had
no cycle guard so a repeating cursor looped forever. Now terminates ONLY on an
absent next link and guards repeated cursors via seen_next_ (mirrors python
_pagination.PaginatedIterator). New tools/pagination_dump.cpp drives the shared
PAGINATION-CORPUS; wired PAGINATION-CORPUS into run-ci (per-PR).
CPP-5 (A6 creds + A5 CA vars): connect() fails FAST pre-connect with a
per-variable actionable error (throws std::invalid_argument naming the missing
project/token + its env var) instead of a silent connect()=false. REST reads
SIGNALWIRE_REST_CA_FILE and RELAY reads SIGNALWIRE_RELAY_CA_FILE as their exact
TLS trust bundles (ca_var_parity --port cpp now exits 0).
F3 reconnect defect (found by the RELAY-LIVENESS suite): reconnect() open-coded
a bare ws_->connect(host,port) that ignored SIGNALWIRE_RELAY_SCHEME (always TLS)
and passed the host WITH its embedded ":port", producing a malformed
"wss://127.0.0.1:PORT:443/" URL that could never reconnect against a plain-ws
peer. Extracted open_ws_transport() shared by connect() + reconnect(). Also
added a WS ping heartbeat (setPingInterval) so a half-open peer is detected
(F2.1). New tools/relay_liveness_dump.cpp drives all 10 RELAY-LIVENESS fixtures;
wired RELAY-LIVENESS into run-ci (nightly).
CPP-6 (sanitizer lane): SIGNALWIRE_SANITIZE=address|thread|undefined CMake
option instruments the whole tree so the nightly ASAN/TSAN lane catches the
CPP-1/CPP-2 class of bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
This PR's run-ci passes the new PSDK-3/4/5 rules (PAGINATION-CORPUS/ RELAY-LIVENESS) via --rules; main's porting-sdk does not register them, so a psdk@main checkout would reject them [BEHAVIORAL] unknown-rule. Pin the porting-sdk checkout ref in every workflow to wave/1-aplus; REVERT to main when porting-sdk wave/1-aplus merges. The signalwire-python checkouts stay on main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
- DOC-ENV: document the three new env vars the SDK now reads — SIGNALWIRE_REST_CA_FILE, SIGNALWIRE_RELAY_CA_FILE (A5 CA trust bundles) and SIGNALWIRE_RELAY_PING_INTERVAL_SECS (F2.1 ping heartbeat) — in docs/security.md's SSL/TLS section. - NO-CHEAT: replace the ASSERT_TRUE(true) in the CPP-2 concurrent-race test with a content-shaped assertion (a final single-threaded dispatch must fire exactly all 500 registered handlers — a race that corrupted the handler vector yields the wrong count or crashes). - LINT: the SIGNALWIRE_RELAY_PING_INTERVAL_SECS override's catch(...) was empty (bugprone-empty-catch); handle std::exception and log at debug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
The BEHAVIORAL suite runs {repo}/build/pagination_dump (PAGINATION-CORPUS,
per-PR) and {repo}/build/relay_liveness_dump (RELAY-LIVENESS, nightly), but the
TEST gate's cmake --target list didn't build them — so on CI (a clean tree) the
per-PR PAGINATION-CORPUS rule ran a missing binary and red'd with "dump did not
emit valid JSON" (green locally only because the binaries were already built).
Add both dump targets to all three BUILD_MODE target lists (host/exec/run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
…ugh the REST generator (DRIFT 106->0)
Adopt the PY-7/PY-9 reference addition: a trailing keyword-only
``request_options`` on every generated REST verb, forwarded to the HTTP
layer (timeout / retries / abort) and NEVER folded into the wire body.
Every other port took this; cpp's prior lane deferred it — that deferral
is reversed here (owner-FINAL 2026-07-20).
Generator (scripts/generate_rest.py):
* emit_method (write-body / union-body / write / GET / delete), the
§6 command-dispatch emitter + its execute() helper, set_methods, and
the inherited-base-verb sidecar records all now carry request_options.
* The idiomatic C++ param is a trailing ``const RequestOptions&
request_options = {}``; the sidecar records it at the REFERENCE's
keyword-only position so the cross-language diff aligns — any port-only
trailing extra (the GET ``params`` query door, the write-verb ``kwargs``
/ set-method ``extra`` door) sits AFTER it and is ignored as an optional
trailing extra. EMISSION stays byte-identical (transport-only).
Base + paginator: base_resource.hpp paginate/list_addresses and the
PaginatedIterator ctor (include + src) thread request_options; every page
fetch applies it.
Enumerator (scripts/enumerate_signatures.py): reconcile the hand-written
base resource classes — mark ``request_options`` keyword and move it before
the port-only ``params`` door so the recorded signature matches the oracle
(idiom via the enumerator, never omission; RULES §2). HttpClient keeps it
positional (as the reference does).
Worktree robustness: enumerate_signatures.py + generate_rest_tests.py now
honour ``PORTING_SDK`` (not only ``PORTING_SDK_DIR``) when resolving the
adjacent porting-sdk, so a worktree run resolves the real checkout instead
of silently staling the enumerate/regen. Real CI uses adjacency, unaffected.
TDD-bidirectional (tests/test_request_options.cpp): a generated verb
(addresses().create) forwards request_options to the HTTP layer (an armed
503 is retried through the generated method), and request_options is NOT
folded into the wire body (the journalled request body carries only the
domain fields).
Verification: DRIFT exit 0 (106 -> 0); run_tests 1958/1958; full run-ci CI
PASS (SURFACE/GEN/BEHAVIORAL incl EMISSION/REST-COVERAGE/SPEC-PARITY;
SEMVER-DIFF report-only fleet-wide).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
mjerris
marked this pull request as ready for review
July 21, 2026 11:46
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.
Wave 1 (A+ campaign) — cpp breaking legs. Python is the oracle; the shared
porting-sdk mocks/corpora pin behavior. TDD-bidirectional.
CPP tracker slice
Action*to a stack-local Action thatexecute_action()returned by value;the pointer dangled the moment the function returned, so a later
resolve_all_actions()/find_action()read freed memory.Actionisshared_ptr-backed, so the registry now OWNS its entries by value (a copy thatSHARES SharedState with the caller's copy). Proven RED under ASAN
(stack-use-after-scope) then GREEN.
on_event()mutatedevent_handlerswith no lock whiledispatch_event()iterated it on the WSreader thread. Both now guarded by
handlers_mutex; dispatch snapshots underthe lock then invokes without it.
RelayClient::execute()nowRAISES
RelayErroron a non-2xx result code;Call::_executeswallows only404/410 and PROPAGATES every other error (was a silent
resolve("error")).Added configurable
RelayConfig.request_timeout_ms(F2.2 black-hole bound).Un-hid a real test-data bug:
calling.taptests senttap={type}missing theschema-required
params— fixed to{type,params}(python reference shape).PaginatedIteratorterminated on an empty pageeven with
links.nextpresent (dropped later pages) and had no cycle guard.Now terminates ONLY on an absent next link + guards repeated cursors
(
seen_next_), mirroring python_pagination. Newtools/pagination_dump.cpp;PAGINATION-CORPUS wired (per-PR) — differ ✓ PASS.
connect()fails FAST pre-connect with aper-variable actionable error (
std::invalid_argumentnaming the missingproject/token + env var). REST reads
SIGNALWIRE_REST_CA_FILE, RELAY readsSIGNALWIRE_RELAY_CA_FILE(exact names). ca_var_parity --port cpp → exit 0.SIGNALWIRE_SANITIZE=address|thread|undefinedCMake option instruments the whole tree (catches the CPP-1/2 class).
Also found + fixed (RELAY-LIVENESS surfaced it)
reconnect()open-coded a barews_->connect(host,port)that ignoredSIGNALWIRE_RELAY_SCHEME(always TLS)and passed the host WITH its embedded
:port, building a malformedwss://127.0.0.1:PORT:443/that could never reconnect against a plain-ws peer.Extracted
open_ws_transport()shared byconnect()+reconnect().setPingInterval) so ahalf-open peer is detected. New
tools/relay_liveness_dump.cpp(10 fixtures);RELAY-LIVENESS wired (nightly) — differ ✓ PASS (10/10).
Evidence
Total: 258 Passed: 258 Failed: 0✓ PASS — cpp(exit 0)✓ PASS — cpp(exit 0)--port cppexit 0Notes for the owner
oracle (psdk f7b4140) added a typed
request_options: keyword optional<RequestOptions>to every REST verb and DROPPED the trailingvar_keyword params/kwargsdoor. cpp's C++ verbs already acceptRequestOptions, but the generated REST sidecar (rest_signatures.json) stillrecords the OLD
var_keyword anytrailing param — so DRIFT reports ~59request_options mismatches. This is a fleet-wide REST-generator re-alignment
(remove the kwargs door, append the typed request_options record across all
verb kinds), red on
maintoo — NOT introduced here and orthogonal to thebreaking legs. Reshaping the whole REST sidecar autonomously risks new drift,
so flagged rather than rushed.
CMakeLists.txt project(... VERSION ...)): 3.2.1. IfSEMVER-DIFF reds on the breaking API changes, that is the fleet-owner decision
(no bump / no allowlist here).
🤖 Generated with Claude Code
https://claude.ai/code/session_01NqhUoqrbptHNS3cypq9s6t
Final CI status (PR #47)
surface-audit✓ pass ·doc-audit✓ passtest(run-ci): every gate green EXCEPT the single documented SURFACE(SIGNATURES/DRIFT) = request_options re-drift above. Confirmed green on CI:
TEST, GEN (all 5), BEHAVIORAL (incl PAGINATION-CORPUS), DOC-TRUTH,
NO-CHEAT, FMT, LINT, SEMVER-DIFF (3.2.1, minor bump — no owner-block), LEDGER,
PACKAGE, DEAD-PUBLIC-ERROR, ROOT-HYGIENE, WIRED-MODES.
param-count/param-mismatch(the port's generated per-verb REST methods do not yet carry the typed
request_optionsparam the wave/1 oracle added). This is the OWNER-BLOCKEDitem above — a REST-generator adoption workstream, red on main too.