Skip to content

fix(ec2): close the gaps in the IPAM internet-registry operations - #2533

Merged
vieiralucas merged 2 commits into
mainfrom
fix/ipam-registry-findings
Sep 14, 2026
Merged

vieiralucas merged 2 commits into
mainfrom
fix/ipam-registry-findings

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

A review of the 15 IPAM internet-registry / routing-policy-registration operations that shipped in #2511 found them accepting requests they then failed to honour. Each defect is fixed here with the test that proves it.

Wire correctness

  • Child request XML escaped: a ServiceUri containing &, or a handle containing ", produced a document the RIR cannot parse (200 to the caller, breakage discovered later).
  • Batch ASNs written as JSON numbers were dropped, registering a CIDR that authorizes nobody and self-reports unknown/permissive.

Requests accepted but not honoured

  • A batch entry that could not be applied was skipped after the document was recorded as published; the document is now validated first and a bad entry fails the request.
  • Batch entries bypassed single-op validation, storing and emitting maxLength=200 for a member the model bounds to 0..48. One shared validation; a batch add on an existing CIDR is an upsert per the op's docs.
  • Modify rebuilt the record, silently clearing MaxLength / Description / PermitMoreSpecificAnnouncements when omitted. Now a partial update.
  • ClientToken was modeled on all five mutating ops and ignored: a retried timed-out create got a conflict instead of a replay.
  • The eight paginated reads validated MaxResults then returned everything with no nextToken, and discarded modeled Filters.

Preconditions

  • DryRun returned success before the checks it exists to surface (existing CIDR, nonexistent IPAM, unregistered CIDR). Dry runs now reach the same verdict as the real call.
  • pending-enable was documented but unenforced, leaving EnableIpamInternetRegistryAssociation decorative.
  • DeleteIpamInternetRegistryAssociation deleted an association out from under its registrations; now DependencyViolation until they are gone.

Adjacent, same class

  • A conformance ordering assertion compared Option<usize>, so a dropped delta passed vacuously (None < Some).
  • The bounded container-CLI helper left its reader thread blocked on a pipe a wrapper script's grandchild still held -- a leaked thread per timed-out call on the wedged-daemon path the bound exists for. Now runs the CLI in its own process group and kills the group; collection bounded by a drain grace. fakecloud-testkit carried a second leaky copy and now shares core's, and detect_bridge_gateway (still unbounded) goes through them.

Local gate: workspace build + clippy -D warnings clean, fmt clean, 22 IPAM unit tests, 9 bounded-CLI tests, 9 testkit tests, 69 EC2 IPAM conformance tests green.


Summary by cubic

Fixes the 15 IPAM internet-registry operations that accepted requests they didn’t honor. All operations now follow their modeled contracts: wire documents are valid, batch and single-operation behavior match, mutating operations are idempotent and respect preconditions, and reads paginate and filter.

Bug Fixes

  • Escapes ServiceUri, handles, RPKI version, and parent BPKI TA in child request XML; & and quotes no longer produce a document the RIR can’t parse.
  • Accepts batch ASNs written as JSON numbers; previously they were dropped, registering a CIDR that authorized no one.
  • Validates a batch document against the state it produces before recording any of it; an invalid entry fails the request instead of being skipped after success.
  • Applies one shared validation to batch and single-operation registrations; batch add on an existing CIDR acts as an upsert.
  • Runs Modify as a partial update, preserving omitted members.
  • Clears MaxLength, Description, and PermitMoreSpecificAnnouncements when sent as an empty value or JSON null, while omitted still means unchanged.
  • Records ClientToken with a fingerprint of the request parameters, so retries replay the original result; reuse with different parameters returns IdempotentParameterMismatch.
  • Ages token records out after 24 hours and caps them oldest-first, so create/delete loops no longer grow state forever.
  • Enforces pending-enable for registration writes; removal is not gated, so an association restored from an older snapshot can still be emptied and deleted.
  • Returns DependencyViolation when deleting an association that still has registrations.
  • Pages the eight read operations honoring MaxResults, Filters, and nextToken; reverse paging uses delta ids so new deltas between pages aren’t repeated, and non-numeric MaxResults is rejected instead of returning everything.
  • Makes dry runs reach the same verdict as real calls.
  • Fixes a conformance ordering assertion that compared Option<usize> and passed vacuously when a delta was dropped.
  • Runs the bounded container-CLI helper in its own process group with /dev/null stdin, so a wedged daemon no longer leaks a reader thread; fakecloud-testkit shares core’s helpers and detect_bridge_gateway goes through them too.

Written for commit 96e7bb6. Summary will update on new commits.

Review in cubic

A review of the 15 operations that shipped in #2511 found them accepting
requests they then failed to honour. Each one is fixed here with the test that
proves it.

- The child request document interpolated `ServiceUri`, the handles, the RPKI
  version and the parent BPKI TA straight into XML, so an ordinary URI carrying
  `&` produced a document the RIR cannot parse, and a handle carrying a quote
  closed an attribute early. All five are escaped.
- BatchModifyIpamRoutingPolicyRegistrations read ASNs only as JSON strings, so
  a document writing them as numbers registered a CIDR that authorized nobody
  and reported itself unknown/permissive. Numbers are accepted.
- A batch entry that could not be applied was skipped after the whole document
  had already been recorded as published, so nine of ten entries applying still
  reported success. The document is validated before anything is recorded, and
  a bad entry fails the request.
- Batch entries bypassed the validation the single-op path runs, so a
  `maxLength` of 200 was stored and emitted for a member the model bounds to
  0..48. Both paths share one validation now, and a batch `add` for a CIDR that
  is already registered is an upsert, matching the operation's own docs.
- ModifyIpamRoutingPolicyRegistration rebuilt the record from the request, so
  `MaxLength`, `Description` and `PermitMoreSpecificAnnouncements` vanished
  whenever the request omitted them. Modify is a partial update.
- DryRun returned success before the checks it exists to surface: a dry-run
  create against an existing CIDR or a nonexistent IPAM, and a dry-run modify
  or delete of a CIDR that is not registered, all reported a success the real
  call then refused. Every dry run now reaches the same verdict as the real
  call.
- The create path parked an association in `pending-enable` and documented that
  it cannot publish until enabled, but nothing enforced it, leaving
  EnableIpamInternetRegistryAssociation decorative. Registration writes require
  an enabled association.
- ClientToken was modeled on all five mutating operations and ignored by all of
  them, so an SDK retrying a timed-out create got a conflict instead of a
  replay. Tokens are recorded per action and replay their original result.
- The eight paginated reads validated MaxResults and then returned everything,
  never emitting nextToken, and discarded the modeled Filters. They paginate
  and filter.
- DeleteIpamInternetRegistryAssociation deleted an association out from under
  its registrations; it now refuses with DependencyViolation until they are
  gone.

Also: a conformance ordering assertion compared `Option<usize>`, so a dropped
delta passed vacuously (`None < Some`), and the bounded container-CLI helper
left its reader thread blocked on a pipe a wrapper script's grandchild still
held, leaking a thread per timed-out call on exactly the wedged-daemon path the
bound exists for. The helper now runs the CLI in its own process group and
kills the group, so the read end closes and the reader exits; collection is
bounded by a drain grace. fakecloud-testkit carried a second copy of those
helpers with the same leak and now shares core's, and `detect_bridge_gateway`,
which still called the CLI unbounded, goes through them too.
A review of the previous commit found the fixes themselves incomplete.

- ClientToken replay keyed only on the token, so a token reused with different
  parameters discarded the new request and returned the old result with a 200.
  Deleting 192.0.2.0/24 and then retrying the same token for 198.51.100.0/24
  reported a published delete for a CIDR that is still registered, and the
  caller then hit the new DependencyViolation on an association it had been
  told was empty. Every mutating operation now records a fingerprint of the
  request parameters beside the token, replays only on an exact match, and
  answers a divergent reuse with IdempotentParameterMismatch.
- The token store grew forever. ClientToken is an idempotency token, so every
  SDK fills a fresh one on every call, not just on retries: a create/delete
  loop left one permanent entry per call in state and in every snapshot.
  Records now age out after 24 hours and are capped oldest-first.
- A batch that removed a CIDR the same document added was rejected, because
  removals were checked against the state before the batch while additions are
  applied first. The document is now validated against the state it would
  itself produce.
- Turning Modify into a partial update left no way to clear MaxLength,
  Description or PermitMoreSpecificAnnouncements: an explicit empty value was
  dropped as absent, and a JSON null in a delta document was too. Omitted still
  means unchanged; present-but-empty and null now clear.
- GetIpamRoutingPolicyRegistrationDeltas paged ChronologicalOrder=reverse by
  offset into a reversed list, so a delta appended between two page requests
  shifted every later index and the second page repeated entries. Reverse now
  pages by delta id.
- MaxResults that was not a number disabled paging and returned everything,
  while a non-numeric NextToken was rejected; both are rejected now.
- The batch path's registration check could never fail (its write variant fell
  through to Ok), so the validation it claimed to share was a no-op; it is
  replaced by the projection above. upsert_registration no longer decides
  create-vs-modify by string-comparing the action name. Publishing is gated on
  an enabled association and removal never is, so a snapshot restored from
  before the gate can still be emptied and deleted.

Also from the same review: the bounded container-CLI helper left stdin
inherited while putting the child in its own process group, so a CLI that read
the terminal took SIGTTIN and stopped, and the WNOHANG wait never saw it, which
burned the whole deadline; stdin is /dev/null now. The dedup left wait_bounded
and its KillScope enum with no callers, and bounded_status still took &[String]
where its sibling took &[&str], allocating per container in the sweep.
@vieiralucas
vieiralucas merged commit 2409d69 into main Sep 14, 2026
159 checks passed
@vieiralucas
vieiralucas deleted the fix/ipam-registry-findings branch September 14, 2026 23:08
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