Skip to content

deferredwork: append_decision/append_entry bare write_text can truncate the ledger to zero bytes #328

Description

@pbean

Summary

mark_done_many writes the ledger through atomic_write_text (deferredwork.py:354) with a
documented rationale — a bare tmp+replace resets the file mode and turns a symlinked ledger into a
regular file. Its two sibling writers do not:

  • append_decisiondeferredwork.py:389: path.write_text(text, encoding="utf-8")
  • append_entrydeferredwork.py:492: path.write_text(text + sep + block, encoding="utf-8")

Path.write_text opens with mode 'w' (truncate) and then encodes. Any failure between those two
points — an unencodable value, ENOSPC, EIO — leaves a zero-byte ledger. Every deferred-work
entry is gone.

Reproduced during review: an append_decision call that raised mid-write left a 35-byte ledger at
0 bytes, while the same value through mark_done left it intact.

Why the asymmetry matters

mark_done_many's docstring already argues the case for this exact file — "swapping a fresh inode
over the ledger otherwise resets its mode (a 0600 ledger silently becoming world-readable) and
turns a symlinked ledger into a regular file". That reasoning applies unchanged to the other two
writers; the protection just was not extended to them.

An asymmetric guard between siblings is itself the finding: a reader cannot tell whether the
difference is deliberate.

Fix shape

Route append_decision and append_entry through atomic_write_text too. Note append_entry
creates the file when absent (path.parent.mkdir(...)), so the helper needs to keep working for a
not-yet-existing target.

Found during the #305 review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:deferred-workdeferred-work.md ledger and sweepbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions