Skip to content

A live attachment pointing at a grace-window tombstone 404s for up to 30 days, then silently starts working — the reap guard only revives files it is asked about, and it is never asked inside the window #10246

Description

@os-warren

Found and measured while implementing #10171 (PR #10241). Deliberately not fixed there, and filed unassigned rather than ridden along: the fix would be Clause-②: yes (it turns a currently-404 download into a 200), and the services lane has no contract-review tier available right now. The #10171 dev stopped and reported it rather than implementing or filing unilaterally; this card is the PM recording it.

The shape

Re-pointing a sys_attachment row onto a file that is inside its 30-day grace-window tombstone is byte-safe — that half is already owned by createSysFileReapGuard, whose sweep-time re-verification resolves current references, un-tombstones the file and vetoes the reap. #10241 measured exactly that and therefore added no revival leg, correctly.

But the guard only ever sees rows the sweep nominates, and the sweep nominates a tombstone only after its ttl { field: 'deleted_at', expireAfter: '30d' } has expired. Measured: candidates within the window = [].

So inside the window the file is not a sweep candidate at all, and simply stays status='deleted'. Meanwhile storage-routes.ts:606 and :653 refuse any file whose status !== 'committed' with 404 FILE_NOT_FOUND.

Net: a live attachment row can point at a file that 404s for up to 30 days, and then silently starts working once the sweep finally runs and un-tombstones it. Nothing is lost, and nothing tells the operator why.

Why this is worth a card rather than a comment

The revival mechanism is correct and the bytes are safe — what is wrong is when it runs. "Eventually correct, on a 30-day timer, with a 404 in between" is a defensible retention posture and an indefensible download posture, and today the system has no way to tell those two apart because one timer serves both.

⚠️ #10241 enlarges this population. Before it, only the DELETE verb minted tombstones; after it, an UPDATE that re-points file_id mints them too. The scenario is pre-existing, but the set of ways to reach it just grew — which is why it is being recorded now rather than left as a curiosity.

Why the fix is Clause-② and needs the contract-review tier

Any fix makes a request that is refused today succeed — an endpoint's accept set widens. That is categorically different from #10241's own change, which applied an already-declared rule (an attachments-scope file with zero join rows is tombstoned, enforced on the delete verb since #2755) to the verb that was missing it, minting no new refusal category and no new accepted input.

⛔ So this must not be folded into a lower-tier card.

Directions (not a recommendation — this needs triage/maintainer grading)

  1. Revive at re-point time, not at sweep time: when an update points a row at a tombstoned file, un-tombstone it immediately. Closes the window entirely; costs a second implementation of the reference-resolution question the guard already answers — the duplicate-mechanism hazard fix(service-storage): tombstone the prior file when an attachment re-points file_id #10241 deliberately avoided.
  2. Let the download path resolve it: storage-routes.ts treats a tombstoned file with at least one live join row as downloadable. Keeps one revival mechanism, moves the judgement to the read side; the tombstone stays until the sweep tidies it.
  3. Accept and document: state that a re-attached file may 404 until the next sweep. Cheapest, and honest only if someone actually wants a 30-day 404 in a live attachment.

Refs: #10171 (the update-verb detach leg), PR #10241, #2755 (the orphan rule), createSysFileReapGuard, storage-routes.ts:606 / :653, system-file.object.ts (the ttl / retention lifecycle declaration).

⚠️ domain:services assigned by the discovering lane; retriage if it belongs elsewhere.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions