Skip to content

Build1 publisher2 min readPublished

Leaving mkstemp without a dir argument turned an atomic write into Errno 18

A developer's field log records 48 hours spent chasing OSError Errno 18 after a coding assistant's atomic JSON writer created its tempfile in the default temp directory while the target lived on another mount.

The Engineer · Build desk

Illustration accompanying Leaving mkstemp without a dir argument turned an atomic write into Errno 18

What happened

  • A developer asked a coding assistant for an atomic replace helper, pasted it into a small module, and did not question where the tempfile was being created.
  • Local tests wrote the file and read it back, and killing the process with SIGKILL mid-write still left either the old file or the new one.
  • Rerun on a second machine, the same script raised OSError: [Errno 18] Invalid cross-device link, because the tempfile sat in the default temp directory and the target on another mount.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Atomicity from os.replace stops at the mount boundary, so a helper that chooses its own temp directory can only promise it on hosts where that directory shares a device with the target.
  • decision Reviewing a generated file writer means reading the tempfile call for a dir argument, not only the write-and-fsync loop that the comments are attached to.
  • exposure The break surfaces on the deployment host, where the thing that raises is a boot-time config write and the service waiting on the file gets nothing.

The constraint the helper walked into is stated in the note itself: "os.replace can overwrite on the same filesystem, but it cannot teleport an inode across devices" [12]. On the second machine the call raised `OSError: [Errno 18] Invalid cross-device link` [8]. The except branch unlinked the tempfile and re-raised [7]. The destination kept its old contents and the process died with a traceback. The traceback is the better failure, given why he wanted atomicity: a service reads the file on boot and treats truncated JSON as a fatal parse error [16].

The local check tested the invariant he cared about, and it tested it on one disk. The target was a throwaway file under his home directory [17], and on that laptop the rename stayed inside one filesystem [20]. Nothing in the test varied `st_dev`, because nothing in the test could.

The line that decides the whole thing is the tempfile call:

```python fd, tmp_name = tempfile.mkstemp(suffix=".json") ```

The call passes `suffix` and no `dir` [5]. The file landed in the default temp directory while the destination sat on another mount [11]. Everything after it does careful work on a file in the wrong place: `os.fdopen`, `json.dump` with `indent=2` and `sort_keys=True`, a flush, then `os.fsync(handle.fileno())` [6].

Before the probe he went through permissions, SELinux, a stale `.pyc`, the JSON encoder, and a workspace directory he had not created by hand [9]. Five theories [10], and the note says none of them survived two lines of Python [9]. Those two lines are two `os.stat` calls: print `tempfile.gettempdir()` and the cwd, then compare `os.stat(tmp).st_dev` with `os.stat(target_dir).st_dev` [13]. From the shell he ran `df -P` and `stat -c '%d %n'` on both paths [13].

This is one developer's account of one system, and he calls it "a field log, not a victory lap" [19]. Two things have to be true for it to reach anyone else's code: the helper picks its own temp directory, and that directory sits on a different device from the file being replaced. The second machine here was MonkeyCode's free server option, and the note discloses it was prepared as part of MonkeyCode's product outreach [14].

In my view the review question on any generated file writer is short: does the tempfile call name a directory? If it does not, the word atomic in the comment above it is scoped to whatever mounts the author's laptop happened to have. The note as published breaks off before the corrected helper, at the sentence beginning "When those two device IDs differ" [18].

What to watch

  • Whether the author publishes the corrected helper, and whether it passes dir= to mkstemp or falls back to copying bytes.
  • Any measurement past one laptop and one remote box: how often the default temp directory and application data directories sit on different devices in common images.
  • Whether coding assistants start emitting a dir argument by default in their atomic-write snippets.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories