Research notes / Filesystem interfaces
Unnamed files: publishing is not replacing
17 October 2026 / API review and native Linux observation
Preparing a file, assigning its first name, replacing an existing name and exchanging two names are different operations. None alone establishes a durable application commit.
Choose the namespace operation explicitly
O_TMPFILE creates an unnamed regular file in the chosen directory's filesystem. Supply a writable access mode and permission mode. Filesystem support is required. Adding O_EXCL prevents later linking; it does not request no-overwrite publication. An unnamed file is not inherently confidential. [1]
linkat() can publish that file, but it does not replace an occupied destination. The documented unprivileged route follows /proc/self/fd/N with AT_SYMLINK_FOLLOW. It needs accessible procfs and a live descriptor; the alternative AT_EMPTY_PATH route needs CAP_DAC_READ_SEARCH. Cross-mount linking is not allowed. [2]
A zero link count is not sufficient to identify a publishable temporary file. An ordinary file whose last name was removed can remain readable through an open descriptor, yet Linux rejects an attempt to relink it through procfs. O_TMPFILE without O_EXCL is a specific exception, not a general undelete mechanism. [2]
For an already named source, RENAME_NOREPLACE refuses an occupied destination. RENAME_EXCHANGE swaps two existing names; the flags cannot be combined. Support depends on the filesystem. An existence check followed by ordinary rename is not a substitute for no-replace semantics. [3]
Hesela's design implication: a workflow that links an unnamed file to a staging name and then renames it has two namespace steps. Only the final rename is the replacement; the staging entry can be visible. Use a trusted directory descriptor and controlled relative names, not an assumption that descriptor-relative operations provide a sandbox.
Ten measured outcomes, three repetitions
The native C probe ran as UID 1001 on Linux 6.8.0-146-generic, x86_64, ext4, with cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0. Each repetition used a fresh private directory. All ten asserted outcomes agreed across the three runs; these repetitions are consistency checks, not a statistical sample of devices.
Before exchange
moved names file A
right names file B
After exchange
moved names file B
right names file A
left-data; the descriptor opened for B still read right-data.| Case | Observed invariant | Return / errno |
|---|---|---|
| Stage without a name | Link count 0; no published entry. | 0 / 0 |
| Publish an absent name | Link count 1; name and descriptor identify the same inode and payload. | 0 / 0 |
| Link onto an existing name | EEXIST; existing inode and payload unchanged; candidate remains unnamed. | -1 / 17 |
| Publish O_TMPFILE | O_EXCL | ENOENT; no destination entry created. | -1 / 2 |
| Relink an ordinary file after unlink | ENOENT; link count stays 0, no name is recreated, and the open descriptor still reads the original payload. | -1 / 2 |
| NOREPLACE collision | EEXIST; both input identities and payloads preserved. | -1 / 17 |
| NOREPLACE to an absent name | Source name removed; destination identifies the original source inode. | 0 / 0 |
| Exchange existing names | Names swap inode identities; open descriptors retain their original payloads. | 0 / 0 |
| Exchange with missing destination | ENOENT; destination remains absent. | -1 / 2 |
| Combine EXCHANGE and NOREPLACE | EINVAL; the previously exchanged bindings remain in place. | -1 / 22 |
The initial row records a successful state check, not the numeric descriptor returned by openat(). Other rows record the namespace-call return and its error number; zero in the error column is the probe's success marker, not a claim about libc's residual errno. The source contains the assertions behind every row.
The exchange diagram does not promise a reader snapshot: two separate lookups can occur on opposite sides of an exchange. No concurrent observer ran in this experiment. Nor does one ext4 run demonstrate support on another filesystem or a network mount.
A visible name is not a recovery guarantee
File synchronization and directory synchronization cover different state. File fsync() alone does not ensure that a new directory entry is durable. [4] The probe checked successful file sync before publication and directory sync afterward, but never interrupted power or remounted a filesystem.
Pillai and colleagues separate persistence ordering from atomicity in their OSDI 2014 study. BOB reorders recorded block I/O within its model and checks recovered states; ALICE derives possible crash states from application traces and a specified persistence model. Neither is equivalent to checking successful syscall returns. [5] This historical methodology motivates an explicit limit here: our sequential probe does not reproduce their crash analysis or establish present-day filesystem guarantees.
A protocol must distinguish at least three outcomes: preparation failed; publication succeeded but its directory sync failed; publication and required synchronization succeeded. The second state can leave a visible file with unconfirmed post-crash persistence. Reporting it as an unchanged namespace would conceal uncertainty. This is Hesela's synthesis of the separate operation contracts, not a crash-test result.
Do not classify every failure as lack of feature support. Permission failures, invalid arguments and unsupported operations require different handling. NFS can report link or rename failure after server-side execution, so local error observations must not become universal rollback claims. [2] [3] Continue with atomic rename and durable commit boundaries and synchronization failure handling.
Reproduce the observation
C probe,Python runner andraw JSON with source hashes. Run the two source files together in a directory you control:
python3 observe.py --output observation.json --directory /path/on/test/filesystemThe runner requires Linux, a C compiler, procfs and the findmnt utility. It creates and cleans private test directories. Unexpected or unsupported operations abort without writing a passing result. An existing output from an earlier run must not be mistaken for fresh evidence after failure.
Sequential local observation, not a concurrency proof, network-filesystem test or power-loss experiment. Runtime support, permissions and error handling remain deployment-specific. Neither this probe nor its successful sync calls certify a storage device, mount configuration or application transaction.
Primary sources
- Linux man-pages: open(2), O_TMPFILE
- Linux man-pages: link(2), linkat flags and errors
- Linux man-pages: rename(2), renameat2 flags
- Linux man-pages: fsync(2), file and directory persistence
- Pillai et al., All File Systems Are Not Created Equal, OSDI 2014, sections 2.2 and 3.2
Accessed 17 October 2026. API descriptions are paraphrased; the local observation and its limitations are Hesela's work.