Skip to content

fix(init): deep-copy security_options in rmw_init_options_copy [P1] - #6

Merged
benaliabderrahmane merged 1 commit into
mainfrom
fix/p1-3-init-options-double-free
Jun 5, 2026
Merged

benaliabderrahmane merged 1 commit into
mainfrom
fix/p1-3-init-options-double-free

Conversation

@benaliabderrahmane

Copy link
Copy Markdown
Owner

Severity: P1 — found by a full multi-agent code audit (method + findings in AUDIT_FINAL.md).

Problem

rmw_init_options_copy() did *dst = *src and only deep-copied enclave. rmw_security_options_t::security_root_path is a heap-owned char *, so after the shallow copy src and dst alias the same pointer. rmw_init_options_fini() calls rmw_security_options_fini(), so finalizing both copies (what rcl / init-options lifecycle code does) frees security_root_path twice → heap corruption. Latent until a security-enabled (sros2) deployment sets a non-null root.

Fix

After *dst = *src, zero dst->security_options and deep-copy via rmw_security_options_copy(...), returning the error on failure.

File: rmw_unix_socket_cpp/src/rmw_init.cpp
Note: the analogous discovery_options aliasing (P2-4) is addressed in the full-stack PR, not here.
Build: clean.

@benaliabderrahmane

Copy link
Copy Markdown
Owner Author

Reviewed the diff (+9/-0, single file src/rmw_init.cpp), verified the bug against main, the rmw_security_options_copy/init_options contracts, and CI.

The core fix is correct, minimal, and well-scoped: security_options.security_root_path is a heap-owned char*, so the previous shallow *dst = *src left src/dst aliasing the same pointer and double-freeing in rmw_init_options_fini. Deep-copying via rmw_security_options_copy after zero-initializing dst's security_options is the right fix, matches existing style, and the PR correctly defers the discovery_options/P2-4 fix to the full-stack PR. All three CI jobs (jazzy/kilted/rolling) pass. Approving.

Two non-blocking follow-ups:

  1. (P3) Error-path leaves dst partially initialized -- src/rmw_init.cpp:54-62. At line 54 *dst = *src already sets dst->implementation_identifier (non-null) and aliases dst->enclave = src->enclave. The new early-return at lines 60-62 fires BEFORE the enclave deep-copy, so on RMW_RET_BAD_ALLOC (reachable only when src->security_options.security_root_path is non-null) dst is left non-zero, violating the rmw_init_options_copy postcondition ('If copy fails, destination init options will remain zero initialized') and reintroducing the same double-free class -- now on enclave -- that this PR fixes for security_root_path. main had no failure path at all, so this edge case is introduced by this diff. Suggest *dst = rmw_get_zero_initialized_init_options(); before the early return.

  2. (CLAUDE.md §4) No reproducing test. This PR adds no test, and the existing InitOptionsLifecycle (test/test_rmw_init.cpp:21-36) only ever exercises a null security_root_path (seeded by rmw_get_zero_initialized_security_options), so it passes identically with or without the fix and won't catch a revert. A single low-cost change closes both items: extend InitOptionsLifecycle to set a non-null root via rmw_security_options_set_root_path, copy, then fini both copies under ASan, and reset dst on the error path.

Neither blocks merge -- the shipped code is a correct, focused fix. Recommend both as fast-follow.

@benaliabderrahmane
benaliabderrahmane merged commit 3de7b77 into main Jun 5, 2026
3 checks passed
@benaliabderrahmane
benaliabderrahmane deleted the fix/p1-3-init-options-double-free branch June 17, 2026 08:47
benaliabderrahmane added a commit that referenced this pull request Jul 10, 2026
Addresses the review findings on the durable-shm TRANSIENT_LOCAL change:

- Return code (finding #1): the TL publish path ignored send_to's result and
  always returned RMW_RET_OK, even when the live send of the current message
  was rejected by the kernel size cap (EMSGSIZE) — a lying success, and
  inconsistent with the non-TL path. Extract the TL cache+replay+send logic
  into transient_local_publish(), which now surfaces ConfigError as
  RMW_RET_ERROR. (ENOBUFS/oversize on a raised-buffer machine stays a soft
  drop in BOTH paths, unchanged — a shared, pre-existing limitation.)

- Serialized TL gap (completeness): rmw_publish_serialized_message did not feed
  the replay cache at all, so a large latched payload published through it was
  sent inline once and dropped for late joiners. It now routes TL through the
  same transient_local_publish() helper (durable staging + replay) and gained
  the same known-subscriber prune-on-refresh as rmw_publish.

- Fallback coverage (finding #6): add a cold-path test seam (env var
  RMW_UDS_TEST_FORCE_SHM_FAILURE, checked only on large latched staging) so a
  test can force the shm-unavailable path and assert the payload is cached
  inline (no durable segment, no flag) and still delivered.

Tests: TransientLocalPublishReturnsErrorOnEMSGSIZE and
TransientLocalSerializedLargeMessageLateJoiner both confirmed to FAIL on the
pre-change publisher and pass here; TransientLocalLargeMessageInlineFallbackWhenShmUnavailable
covers the fallback. Full suite: 95 tests, clean build.
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