Skip to content

DiskBlobAdapter.putAttachment writes non-atomically; dedup makes a torn blob permanent #121

Description

@cuibonobo

DiskBlobAdapter.putAttachment writes bytes straight to their final content-addressed path (blob-adapter-disk/src/index.ts:29-35):

const fileId = createHash('sha256').update(data).digest('hex');
if (!existsSync(join(this.dir, fileId))) {
  await writeFile(join(this.dir, fileId), data);
}

A crash mid-writeFile leaves a partial file at its final, hash-named path. Because dedup keys on existence of that path, every future upload of the same bytes sees the file exists and skips writing (existsSync → true) — so the torn file is never repaired, and reads serve bytes that don't match their own fileId. A silent content-integrity failure, permanent by construction.

Fix

Write to a temp file, then atomic rename() to the final path — the exact idiom the spec already prescribes for the planned adapter-json (§Concurrency & storage ownership) and that #72 assumes for the streaming blob path. A rename is atomic on a local filesystem, so a crash leaves either the complete file or no file, never a torn one at the final name. (Optional defense-in-depth: verify the hash on dedup-skip, but temp+rename is the core fix.)

Tests

  • Simulate a partial write at the final path, then upload the real bytes: the stored file matches the fileId (repair happens / torn bytes never served)
  • Concurrent uploads of the same bytes don't corrupt each other

Refs

#45 (atomic temp-file-and-rename idiom), #72 (streaming blob path assumes this), #106 (adjacent attachment-atomicity — a different window: bytes-vs-metadata, not the byte write itself). From docs/design-assessment-2026-07.md §F8.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions