Skip to content

Wave 1 (A+ campaign): cpp breaking legs#47

Merged
mjerris merged 5 commits into
mainfrom
wave/1-aplus
Jul 21, 2026
Merged

Wave 1 (A+ campaign): cpp breaking legs#47
mjerris merged 5 commits into
mainfrom
wave/1-aplus

Conversation

@mjerris

@mjerris mjerris commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Wave 1 (A+ campaign) — cpp breaking legs. Python is the oracle; the shared
porting-sdk mocks/corpora pin behavior. TDD-bidirectional.

CPP tracker slice

  • CPP-1 dangling Action* registry UAF (HIGH) — the 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() read 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 handler-registration race (HIGH)on_event() mutated
    event_handlers with no lock while dispatch_event() iterated it on the WS
    reader thread. Both now guarded by handlers_mutex; dispatch snapshots under
    the lock then invokes without it.
  • CPP-3 A2 relay-contract / honest errorsRelayClient::execute() now
    RAISES RelayError on a non-2xx result code; Call::_execute swallows only
    404/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.tap tests sent tap={type} missing the
    schema-required params — fixed to {type,params} (python reference shape).
  • CPP-4 pagination corpusPaginatedIterator terminated on an empty page
    even with links.next present (dropped later pages) and had no cycle guard.
    Now terminates ONLY on an absent next link + guards repeated cursors
    (seen_next_), mirroring python _pagination. New tools/pagination_dump.cpp;
    PAGINATION-CORPUS wired (per-PR) — differ ✓ PASS.
  • CPP-5 A6 creds + A5 CA varsconnect() fails FAST pre-connect with a
    per-variable actionable error (std::invalid_argument naming the missing
    project/token + env var). REST reads SIGNALWIRE_REST_CA_FILE, RELAY reads
    SIGNALWIRE_RELAY_CA_FILE (exact names). ca_var_parity --port cpp → exit 0.
  • CPP-6 nightly ASAN/TSAN laneSIGNALWIRE_SANITIZE=address|thread|undefined
    CMake option instruments the whole tree (catches the CPP-1/2 class).

Also found + fixed (RELAY-LIVENESS surfaced it)

  • F3 reconnect URL defectreconnect() open-coded a bare
    ws_->connect(host,port) that ignored SIGNALWIRE_RELAY_SCHEME (always TLS)
    and passed the host WITH its embedded :port, building a malformed
    wss://127.0.0.1:PORT:443/ that could never reconnect against a plain-ws peer.
    Extracted open_ws_transport() shared by connect() + reconnect().
  • F2.1 dead-peer — added a WS ping heartbeat (setPingInterval) so a
    half-open peer is detected. New tools/relay_liveness_dump.cpp (10 fixtures);
    RELAY-LIVENESS wired (nightly) — differ ✓ PASS (10/10).

Evidence

  • relay suite: Total: 258 Passed: 258 Failed: 0
  • PAGINATION-CORPUS differ: ✓ PASS — cpp (exit 0)
  • RELAY-LIVENESS differ: ✓ PASS — cpp (exit 0)
  • CPP-1 teeth: ASAN aborts on the pre-fix tree (stack-use-after-scope), clean with the fix
  • ca_var_parity --port cpp exit 0

Notes for the owner

  • request_options (PY-9 re-drift): pre-existing, OWNER-BLOCKED. The wave/1
    oracle (psdk f7b4140) added a typed request_options: keyword optional<RequestOptions> to every REST verb and DROPPED the trailing
    var_keyword params/kwargs door. cpp's C++ verbs already accept
    RequestOptions, but the generated REST sidecar (rest_signatures.json) still
    records the OLD var_keyword any trailing param — so DRIFT reports ~59
    request_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 main too — NOT introduced here and orthogonal to the
    breaking legs. Reshaping the whole REST sidecar autonomously risks new drift,
    so flagged rather than rushed.
  • cpp version (CMakeLists.txt project(... VERSION ...)): 3.2.1. If
    SEMVER-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 ✓ pass
  • test (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.
  • The 106 SURFACE drifts are 100% request_options param-count/param-mismatch
    (the port's generated per-verb REST methods do not yet carry the typed
    request_options param the wave/1 oracle added). This is the OWNER-BLOCKED
    item above — a REST-generator adoption workstream, red on main too.

mjerris and others added 5 commits July 20, 2026 20:04
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
mjerris marked this pull request as ready for review July 21, 2026 11:46
@mjerris
mjerris merged commit 23cf93e into main Jul 21, 2026
3 checks passed
@mjerris
mjerris deleted the wave/1-aplus branch July 21, 2026 11:48
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