Skip to content

[AIT-1204] Add RTLO4e10 and RTO10c1b1: the root object must never be tombstoned or GC-evicted#509

Merged
sacOO7 merged 2 commits into
mainfrom
fix/rtlo4e10-root-tombstone-guard
Jul 23, 2026
Merged

[AIT-1204] Add RTLO4e10 and RTO10c1b1: the root object must never be tombstoned or GC-evicted#509
sacOO7 merged 2 commits into
mainfrom
fix/rtlo4e10-root-tombstone-guard

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes https://ably.atlassian.net/browse/AIT-1204

Problem

Raised in the ably-js PR review: ably/ably-js#2219 (discussion) — the GC sweep deletes any tombstoned object past the grace period with no protection for the root object.

Investigating it surfaced a spec-level inconsistency:

  • RTO3b states the ObjectsPool "must always contain" the root InternalLiveMap.
  • RTO5c2a already protects root during sync cleanup.
  • RTO10c1b, however, mandates removing any tombstoned object past the grace period — with no root exclusion. A spec-compliant SDK was therefore compelled to violate RTO3b.

And the deeper gap: nothing prevented the root object from being tombstoned in the first place. Per RTLO4e the realtime system only tombstones orphaned/uninitialized objects — root is by definition neither — but no client-side guard existed. If a faulty OBJECT_DELETE (or a tombstoned ObjectState) ever targeted root:

  1. Root's data is cleared and all its subscriptions are deregistered.
  2. Tombstone is a terminal state — no re-sync or re-attach ever resurrects it. The channel's objects are permanently dead from that moment.
  3. After the grace period, GC evicts root from the pool; root cannot be re-created as a zero-value object ("root" is not parseable as an object id per RTO6b1), so every subsequent objects operation on the channel fails unrecoverably.

Changes

Feature spec (objects-features.md)

Clause Content
RTLO4e10 (new) LiveObject.tombstone must reject the root object: log a warning, return a noop LiveObjectUpdate, perform no state change. Placed in the shared tombstone method so one guard covers both the RTLO5 operation path and the RTLC6f/RTLM6f sync paths.
RTO10c1b1 (new) The GC sweep must never remove the root object from the ObjectsPool (mirrors RTO5c2a). Defense-in-depth for the RTO3b invariant — root can never become tombstoned per RTLO4e10, so this is a safeguard.

UTS test specs

  • internal_live_map.md
    • New tests: RTLO4e10/object-delete-root-noop-0 (operation path) and RTLO4e10/replace-data-tombstone-root-noop-0 (sync path) — root is not tombstoned, data untouched, noop update returned.
    • The three existing tombstone-mechanics tests (RTLO5, RTLM6f, RTLO4e9) are repointed from objectId: "root" to a tombstoneable non-root map (map:test@1000). Their stimuli would noop under the new guard; the assertions themselves are unchanged. This also reconciles the spec with the non-root ids the ably-js derived tests already used.
  • realtime_object.md
    • New channel-level test RTO10c1b1/gc-root-never-removed-0: a rogue OBJECT_DELETE targeting root leaves it untouched, and after the GC grace period elapses root remains in the pool and still applies subsequent operations.

Severity note

Exposure requires the server to send something the protocol says it never sends (clients must never publish OBJECT_DELETE, and the system never targets root), so this is defensive hardening — but the blast radius is terminal and unrecoverable, and RTO5c2a already set the precedent of defending root against exactly this class of anomaly.

SDK implementations

@sacOO7

sacOO7 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the LiveObjects specification and UTS coverage to harden the “root” object invariant: the root object must never be tombstoned and must never be GC-evicted from the ObjectsPool, even in the presence of faulty inbound messages.

Changes:

  • Adds spec clause RTLO4e10 to reject any attempt to tombstone root (log warning, return noop update, no state change).
  • Adds spec clause RTO10c1b1 to ensure the GC sweep never removes root from ObjectsPool.
  • Updates/extends UTS specs to (a) add root-rejection tests and (b) repoint existing tombstone-mechanics tests away from root to a tombstoneable non-root object.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
specifications/objects-features.md Adds RTLO4e10 (reject tombstoning root) and RTO10c1b1 (GC must never remove root).
uts/objects/unit/internal_live_map.md Adds RTLO4e10 unit tests and updates existing tombstone tests to use a non-root map id.
uts/objects/unit/realtime_object.md Adds a channel-level test asserting a rogue OBJECT_DELETE targeting root does not break root functionality after the GC grace period.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread uts/objects/unit/realtime_object.md
sacOO7 added a commit that referenced this pull request Jul 23, 2026
…in isolation

RTLO4e10 makes a tombstoned root unreachable in a conforming
implementation, so the channel-level test verifies the composed
behaviour and the GC-side exclusion remains defense-in-depth verified
by its spec tag rather than by fabricating an impossible state.
Addresses review feedback on #509.
@github-actions
github-actions Bot temporarily deployed to staging/pull/509 July 23, 2026 03:56 Inactive
@sacOO7 sacOO7 changed the title Add RTLO4e10 and RTO10c1b1: the root object must never be tombstoned or GC-evicted [AIT-1204] Add RTLO4e10 and RTO10c1b1: the root object must never be tombstoned or GC-evicted Jul 23, 2026

@ttypic ttypic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

sacOO7 added 2 commits July 23, 2026 14:42
…or GC-evicted

RTO10c1b as written compelled a compliant SDK to violate RTO3b: an
OBJECT_DELETE (or a tombstoned ObjectState) targeting the root object
would tombstone it - clearing its data, deregistering all listeners,
and (since tombstone is a terminal state) making the channel's objects
permanently unrecoverable - after which the GC sweep would evict root
from the ObjectsPool entirely.

- RTLO4e10: LiveObject.tombstone must reject the root object with a
  warning and a noop LiveObjectUpdate. Placed in the shared tombstone
  method so it covers both the RTLO5 operation path and the
  RTLC6f/RTLM6f sync paths.
- RTO10c1b1: the GC sweep must never remove the root object from the
  ObjectsPool (mirrors RTO5c2a), as a safeguard for the RTO3b invariant.

UTS test specs:
- internal_live_map.md: new RTLO4e10 tests for the operation and sync
  paths; the three existing tombstone-mechanics tests (RTLO5, RTLM6f,
  RTLO4e9) are repointed from objectId "root" to a tombstoneable
  non-root map, reconciling the spec with the ids the ably-js derived
  tests already used.
- realtime_object.md: new channel-level RTO10c1b1 test verifying root
  survives a GC sweep after a rogue OBJECT_DELETE and stays functional.

Raised in ably/ably-js#2219 review discussion r3558197916.
…in isolation

RTLO4e10 makes a tombstoned root unreachable in a conforming
implementation, so the channel-level test verifies the composed
behaviour and the GC-side exclusion remains defense-in-depth verified
by its spec tag rather than by fabricating an impossible state.
Addresses review feedback on #509.
@sacOO7
sacOO7 force-pushed the fix/rtlo4e10-root-tombstone-guard branch from 688a792 to e1c9a95 Compare July 23, 2026 09:17
@sacOO7
sacOO7 merged commit 7becd91 into main Jul 23, 2026
2 checks passed
@sacOO7
sacOO7 deleted the fix/rtlo4e10-root-tombstone-guard branch July 23, 2026 09:19
ttypic pushed a commit to w3lld1/ably-js that referenced this pull request Jul 24, 2026
A faulty OBJECT_DELETE operation (or a tombstoned object state) targeting
the root object would tombstone it: data cleared, all subscriptions
deregistered, and - tombstone being a terminal state - no re-sync could
ever resurrect it. After the GC grace period the sweep would then evict
root from the ObjectsPool entirely, violating RTO3b; root cannot be
re-created as a zero-value object ('root' is not a parseable object id),
so every subsequent objects operation on the channel fails unrecoverably.

Per RTLO4e the realtime system only tombstones orphaned or uninitialized
objects - root is by definition neither - so such a message indicates a
fault and is now rejected client-side, consistent with the existing
warn-and-skip handling for other invalid inbound operations.

- LiveObject.tombstone() rejects the root object with a warning and a
  noop update (RTLO4e10); single choke point covering both the RTLO5
  operation path and the RTLC6f/RTLM6f sync paths
- ObjectsPool._onGCInterval() never evicts root (RTO10c1b1, mirroring
  the existing RTO5c2a guard in deleteExtraObjectIds); root's tombstoned
  map entries still get RTLM19 entry-level GC
- overrideWithObjectState() in LiveMap/LiveCounter returns the tombstone
  branch early since tombstone() can now return a noop update

Derived UTS tests: RTLO4e10 operation/sync path tests in live_map.test.ts,
channel-level RTO10c1b1 GC test in realtime_object.test.ts.

Spec twin: ably/specification#509
Raised in ably#2219 review discussion r3558197916
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants