/tmp is a shared namespace of names, not a private scratch area. Any local user can create the name your process is about to use, so a name is not a claim on a file, and hier(7) says the contents may be deleted with no notice.
The sticky bit makes the directory safe to share, not safe to use predictably. The fix is therefore not a permission change but the shape of the syscall: one call that names and creates the file, with nothing in between for anyone else to take.
1777 is what makes sharing possible at all
Mode 1777 is world-writable plus the sticky bit, and on this host that is not hypothetical: /tmp is a symlink to private/tmp, which is drwxrwxrwt. Without the sticky bit, any writer could unlink or rename anyone else’s file; with it, only the file’s owner, the directory’s owner or root can. That single bit is why /tmp is usable by many mutually untrusting users at once.
What the sticky bit does not do is protect the name before the file exists. open("/tmp/svc.log", "w") follows a symlink that is already there, so the write lands wherever the link points. The fixture verified that with a real open(2), then verified the two refusals: O_CREAT|O_EXCL fails with EEXIST because the name is taken, and O_CREAT|O_NOFOLLOW fails with ELOOP because the final component is a symlink.
The mode of a file you did not create is not yours
The second half of the same bug is subtler: no symlink, no visible race. O_CREAT applies its mode only when it creates the file, so a pre-created world-readable file keeps 0666 when you open it with mode 0600, and the secret written into it stays world-readable. The fixture reproduced that with a real stat(2).
The fix is one call that names and creates atomically. mkstemp(3) documents the contract: the last six characters of the template are XXXXXX, the file is created with permissions 0600, and it is opened with O_EXCL, guaranteeing the caller created it. mktemp(1) does the same from the shell, honouring TMPDIR and defaulting the template to tmp.XXXXXXXXXX. Its -u flag prints a name without creating anything, and the manual labels it “(unsafe)”; the fixture confirmed the name was still free afterwards, which is exactly the window mkstemp closes. For a directory rather than a file, mkdtemp(3) applies the same rule with mode 0700 in one call.
Isolation, and what it costs
TMPDIR is the cheapest isolation available: the fixture showed tempfile.gettempdir() follows it, and this host already gives each user a private temp directory. systemd’s PrivateTmp= does the same per unit, setting up a filesystem namespace whose /tmp and /var/tmp are not shared with processes outside it and whose files are removed when the service stops. The same page is honest about the price: it makes sharing via /tmp or /var/tmp impossible, and the implementation can be unavailable, so a unit should not rely on it alone for security.
A tmpfs /tmp adds an unrelated failure. tmpfs defaults to half of RAM, and those pages are RAM, so a runaway build cache that fills /tmp makes the next service’s 4 KiB mkstemp fail with ENOSPC even though its own data directory is empty. The service that fails is not the one that filled the directory, and every byte parked in /tmp is memory the rest of the machine cannot have.
When it breaks
The first failure is the predictable name, presenting as a file that turns out to be a symlink, or as a secret that ends up world-readable. The audit is mechanical: look for fixed paths under /tmp, and for tmpnam/tempnam, which return a name before any file exists. Replace them with mkstemp/mkdtemp, or tempfile.mkstemp(prefix=..., dir=...), and remember that a crashed process leaves its file behind, so clean up in a finally block rather than on the happy path.
The second failure is the shared /tmp that takes down a bystander. Because a tmpfs /tmp has a size cap unrelated to the disk, one service’s abandoned temp file makes another service’s mkstemp fail with ENOSPC while its own storage is empty. That blast radius makes it worth monitoring as a service-level dependency, and writers capable of producing large files belong under a real path or $XDG_STATE_HOME.
Check it quickly
stat -c '%A %a %n' /tmp
ls -ld "$TMPDIR"
python3 -c 'import tempfile,os;fd,p=tempfile.mkstemp(prefix="probe-");print(p,oct(os.stat(p).st_mode & 0o777));os.close(fd)'
mktemp; mktemp -u
grep -rn "tmpnam\|tempnam\|/tmp/[a-z]" --include='*.py' --include='*.c' --include='*.sh' . | head
The stat line should show drwxrwxrwt with mode 1777; a mode without the sticky bit is a shared directory where any user can delete files. The mkstemp line should print a random name and 0o600, so a 0o644 or a fixed name means the code is not using the atomic API. mktemp creates a file while mktemp -u only prints a name, so finding -u in a script is finding the race. The grep audits fixed temp paths.
Sources
- mktemp(1), the default template, TMPDIR handling, and the -u dry run that the manual labels unsafe, checked 2026-09-16.
- mkstemp(3), the XXXXXX template, the 0600 creation mode, and the O_EXCL guarantee, checked 2026-09-16.
- hier(7), /tmp as temporary files that may be deleted with no notice, checked 2026-09-16.
- systemd.exec(5), PrivateTmp= namespaces and the sharing it makes impossible, checked 2026-09-16.