Skip to content

fix(engine): a carried unknown set is never truncated, so a co-writer can push an owner's own record past the block ceiling #1327

Description

@FSM1

blueprint/core.md states the rule twice, in the two places it matters:

An unwalkable remainder is truncated, never refused: the carried set is attacker-influenced, so failing the cut would let a committed write-grantee block the rotation that revokes them.

A re-seal handed an over-length link drops it rather than failing, so the produce side can never emit a body its own decoder refuses.

The engine implements neither for the envelope's carried unknown fields. crates/engine/src/net/author.rs copies carried_unknown and carried_epoch_tag_unknown verbatim into the re-authored envelope, and the MAX_RESOLVED_RECORD_BYTES check in encode runs after that copy. The two fields come straight off a resolved record — Drain::load_child_node, load_child_folder and publish_node in crates/engine/src/sync/drain.rs all thread them through — with no truncation anywhere in the crate.

A resolved record may itself be up to MAX_RESOLVED_RECORD_BYTES (read_block's own ceiling). So a co-member holding the scope write seed for a shared folder, or a compromised sibling device of the same account, can publish a near-ceiling record at a node's name and make every subsequent re-author of that node exceed the ceiling. That is the attacker-influenced produce-side refusal the blueprint forbids: the owner's publishes at that node stop, and the only exit is the attacker shrinking their own record.

PR #1320 (#1308) bounds the damage — the halt is charged and the op dead-letters with its version preserved rather than spinning forever — but it does not remove the trigger, and says so.

Two adjacent gaps found alongside it

Ordinary folder growth reaches the same ceiling with no attacker involved. There is no folder child-count limit and no name-length limit in crates/core or crates/engine. A folder record inlines a ChildRef per child, so somewhere around 15-20k children in one folder the folder's own record crosses 2 MiB and every write into it dead-letters. #1301 bounds the write body's total encoded size; this is the read-body/folder side of the same question, and it wants a member-facing answer ("split this folder"), not only a bound.

Command::Create accepts an arbitrary-length name. crates/engine/tests/write_plane.rs now relies on that to build the over-ceiling case, which is a fair test lever but a poor command contract: a single name can be made larger than any record that could carry it.

Acceptance criteria

  • The carried unknown sets are truncated on the re-author path rather than carried into a record that then fails encode, per blueprint/core.md.
  • A name length bound at the command boundary, refused as a command error rather than five ticks later as a dead letter.
  • A decision recorded on the folder child-count ceiling: a bound, a member-facing warning as the folder approaches it, or both.
  • A distinct DeadLetterReason for a size refusal. Today it is indistinguishable from a transport outage — both surface as AttemptsExhausted — so a member whose folder crossed the ceiling is told the same thing as one whose network was down, and neither learns the remedy.

Found by the /security-review gate on PR #1320.

Part of #655

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp:enginecrates/engine — the stateful client brainv2-buildv2 rewrite build slice

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions