Skip to content

Attachment lifecycle bookkeeping ignores updates — a file_id re-point orphans the old sys_file with no tombstone (retention leak) #10171

Description

@os-zhuang

Found while implementing #10091 (the sys_attachmentbeforeUpdate authorization guard). Filed rather than fixed: that card is about authorization; this is lifecycle bookkeeping in packages/services/service-storage/src/attachment-lifecycle.ts.

What is there and what is not

installAttachmentLifecycleHooks registers exactly three handlers, all delete/insert-side:

  • beforeDelete: stash the doomed rows' file_ids;
  • afterDelete: tombstone any stashed attachments-scope committed sys_file whose last join row is gone (status: 'deleted', so the reap guard can reclaim the bytes);
  • afterInsert: re-attaching a tombstoned file inside the grace window revives it.

There is no update-side handler. An UPDATE that changes a join row's file_id therefore:

  1. Orphans the old file silently — if that row was the file's last reference, no tombstone is ever written, so the reap guard never reclaims the bytes. Consistent with the module's stated bias ("fail toward retention, not data loss") but as a permanent leak, not a missed sweep: nothing later re-examines the file.
  2. Never revives the new file — re-pointing a row at a tombstoned (grace-window) sys_file does not bring it back the way afterInsert would; the reap guard may then reclaim bytes a live join row references (worth verifying against the reap guard's sweep-time re-verification — if that re-check resolves current references, only leg 1 stands).

On today's origin/main any member can issue such an update (no beforeUpdate authorization at all); after #10091 the writers narrow to uploaders/parent editors, but an authorizedfile_id re-point still skips both legs.

Suggested shape

beforeUpdate/afterUpdate twins of the existing pair, firing only when the payload carries file_id: stash the prior file_ids before the write, run the afterDelete-style orphan check on them after it, and the afterInsert-style revival on the new value. Same best-effort posture (never block the user's write).

Backlinks: #10091 (where found), #2755 (the attachment kit's card, where the tombstone flow landed), #5180 (the delete-cascade design card for this object family — adjacent but a different mechanism: that one is about the PARENT record dying, this one about the join row being rewritten).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions