Summary
ruff check --add-noqa (and presumably --fix / ruff format) rewrites
source files in place without an atomic-rename step. If the process is
interrupted (SIGINT / crash) while a write is in flight, the affected files
are left truncated to 0 bytes — neither the old content nor the new content
survives.
The cache path was hardened against this in #9981 ("Use atomic write when
persisting cache"). The same pattern hasn't been applied to source-file
writes.
Repro
- Run
ruff check --add-noqa (or ruff format, or ruff check --fix)
against a large tree where many files will be rewritten.
- SIGINT (Ctrl-C) it mid-run, while ruff is rewriting files in parallel.
find <tree> -name '*.py' -empty — a handful of files that previously
had content are now 0 bytes. Their mtimes cluster tightly around the
SIGINT.
Expected
An interrupted in-place rewrite should leave the original file intact (or
the fully-written new file), never an empty / partially-written file.
Standard pattern: write to <file>.tmp, fsync, then rename over the
original — rename(2) is atomic on POSIX, so a crash anywhere before the
rename leaves the original untouched.
Prior art in-repo
#9981 introduced atomic-rename for cache writes for exactly this reason.
The same fix shape (write-tmp + rename) should apply to the source-write
path used by --add-noqa, --fix, and ruff format.
Version
ruff 0.14.10, Linux (Docker, ext4).
Summary
ruff check --add-noqa(and presumably--fix/ruff format) rewritessource files in place without an atomic-rename step. If the process is
interrupted (SIGINT / crash) while a write is in flight, the affected files
are left truncated to 0 bytes — neither the old content nor the new content
survives.
The cache path was hardened against this in #9981 ("Use atomic write when
persisting cache"). The same pattern hasn't been applied to source-file
writes.
Repro
ruff check --add-noqa(orruff format, orruff check --fix)against a large tree where many files will be rewritten.
find <tree> -name '*.py' -empty— a handful of files that previouslyhad content are now 0 bytes. Their mtimes cluster tightly around the
SIGINT.
Expected
An interrupted in-place rewrite should leave the original file intact (or
the fully-written new file), never an empty / partially-written file.
Standard pattern: write to
<file>.tmp,fsync, thenrenameover theoriginal —
rename(2)is atomic on POSIX, so a crash anywhere before therename leaves the original untouched.
Prior art in-repo
#9981 introduced atomic-rename for cache writes for exactly this reason.
The same fix shape (write-tmp + rename) should apply to the source-write
path used by
--add-noqa,--fix, andruff format.Version
ruff 0.14.10, Linux (Docker, ext4).