A shared temporary directory is a namespace other users may also write to, not a guarantee that every file inside is public. File permissions still control content access. The dangerous assumption is that choosing a pathname gives your process ownership of whatever already occupies it. Create a new object atomically and keep using the returned handle instead of trusting a predictable name.
Name ownership and content access are different
Linux commonly gives /tmp mode 1777: broad directory write/search permissions plus the sticky bit. Sticky restricts removal and renaming by other users; it does not make existing files confidential or reserve names before creation. File ownership, directory ownership, capabilities and other access controls still matter. Inspect the actual environment rather than assuming every operating system uses the same path, mount or permissions.
An ordinary write-open can follow a pre-existing symbolic link. O_CREAT|O_EXCL rejects an existing final pathname, including a symlink. O_NOFOLLOW rejects a symlink in the final component, but alone does not reject an existing regular file or secure every ancestor. These are different contracts. Use a trusted parent directory and a suitable temporary-file API rather than treating one flag as universal pathname protection.
The creation mode also applies only when a new file is created. Opening an existing file with O_CREAT and mode 0600 does not tighten that file’s previous permissions. Random names reduce collisions, but the security property is exclusive creation, not merely an unpredictable-looking string. A name-only API leaves an interval in which another process can claim the name.
Location does not establish isolation
Python’s tempfile.mkstemp securely creates and opens a file; the caller owns closing and deletion. TemporaryDirectory provides a restricted directory and normal-exit cleanup. TMPDIR influences location selection, but does not itself create a private directory or verify an isolation policy. Python caches its default-directory choice, so pass dir= explicitly when the operation requires a particular location.
systemd’s PrivateTmp= can give a service a separate temporary-file namespace. It changes sharing behavior and has platform and unit prerequisites; it is not equivalent to setting an environment variable. Do not depend on cross-service /tmp paths when those services have separate namespaces. This article’s local exercise does not enable or verify systemd isolation.
A tmpfs mount grows with use and can use swap when available and permitted. Its default size limit is half physical RAM when no size is specified; that is a ceiling, not memory reserved at mount time. Allocation and inode limits can independently prevent new work. /tmp is not necessarily tmpfs, so base capacity monitoring on the actual mount and workload.
When it breaks
Predictable names can redirect writes or reuse an object with unexpected permissions. Cleanup on only the success path leaks files after ordinary exceptions. Context managers and finally help those paths, but cannot promise execution after SIGKILL or a machine crash. Plan stale-file cleanup without deleting another process’s live work, and never rely on temporary storage for the only durable copy.
A shared capacity limit can make one writer disrupt another. Creation and later writes can fail at different stages; creating an empty file does not reserve room for its eventual contents. Bound scratch usage and choose a filesystem appropriate to its lifetime and size. Persistent application-state directories are not a blanket destination for disposable build output.
Check it quickly
Save this as temp_contract.py and run python3 temp_contract.py on Linux with Python 3.11 or newer, a trusted temporary parent and assertions enabled. Do not use -O or PYTHONOPTIMIZE: optimization removes the os.write call inside an assertion. The fixture creates its own files under one identity and does not change existing permissions.
from pathlib import Path
from tempfile import TemporaryDirectory, mkstemp
import errno
import os
import stat
with TemporaryDirectory(prefix="temp-contract-") as directory:
root = Path(directory)
target = root / "target"
target.write_text("original")
predictable = root / "predictable"
predictable.symlink_to(target)
with predictable.open("w") as stream:
stream.write("followed")
assert target.read_text() == "followed"
for flags, expected in [
(os.O_WRONLY | os.O_CREAT | os.O_EXCL, errno.EEXIST),
(os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, errno.ELOOP),
]:
try:
fd = os.open(predictable, flags, 0o600)
except OSError as error:
assert error.errno == expected, error
else:
os.close(fd)
raise AssertionError("pre-existing symlink was accepted")
assert target.read_text() == "followed"
before = stat.S_IMODE(target.stat().st_mode)
fd = os.open(target, os.O_WRONLY | os.O_CREAT, 0o600)
try:
assert stat.S_IMODE(os.fstat(fd).st_mode) == before
finally:
os.close(fd)
fd, name = mkstemp(prefix="owned-", dir=root)
try:
mode = stat.S_IMODE(os.fstat(fd).st_mode)
assert mode & 0o077 == 0
assert os.write(fd, b"synthetic data") == 14
os.lseek(fd, 0, os.SEEK_SET)
assert os.read(fd, 14) == b"synthetic data"
finally:
os.close(fd)
os.unlink(name)
assert not Path(name).exists()
print("ordinary open followed link; EXCL and NOFOLLOW rejected it")
print("existing mode preserved:", oct(before), "; temporary mode:", oct(mode))
assert not root.exists()
print("descriptor round trip and normal-exit cleanup passed")
The ordinary open changes the symlink target. Both guarded opens then fail with their specific expected errors without changing its contents. Reopening the existing target preserves its measured mode; the test intentionally does not assume a particular process umask. The secure temporary file has no group/other permission bits, and its returned descriptor supports the complete write/read round trip.
The earlier article reports Python 3.12.14 success with modes 0644 and 0600, but no matching retained execution record was established here. This review did not run filesystem examples. The code checks ordinary cleanup, not cross-user attacks, concurrent races, sudden termination, tmpfs exhaustion, network filesystems or namespace isolation. Printed summaries alone cannot replace those separate tests.
Sources
- Linux open(2), O_CREAT mode application, O_EXCL and final-component O_NOFOLLOW semantics, checked 2026-10-15.
- Python 3.12 tempfile, mkstemp, TemporaryDirectory, cached default selection and cleanup limits, checked 2026-10-15.
- Linux tmpfs documentation, dynamic allocation, swap and independent size/inode limits, checked 2026-10-15.
- systemd.exec(5), PrivateTmp= sharing boundaries and implementation prerequisites, checked 2026-10-15.
- Linux unlink(2), directory write/search requirements and sticky ownership restrictions, checked 2026-10-15.
- Python assert statement, optimization removes assertions and their expressions, checked 2026-10-18.