Skip to content

linux: keep the mount point manifests where the user id says, first under /var/tmp - #615

Merged
ronleizrowice-ant merged 8 commits into
fix/linux-placeholder-liveness-lockfrom
fix/linux-manifest-directory-from-uid
Oct 20, 2026
Merged

ronleizrowice-ant merged 8 commits into
fix/linux-placeholder-liveness-lockfrom
fix/linux-manifest-directory-from-uid

Conversation

@ronleizrowice-ant

@ronleizrowice-ant ronleizrowice-ant commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #584 (retarget to main once merged). Ships with the first release that writes manifests: none may write elsewhere.

Why

  • The manifest directory followed XDG_RUNTIME_DIR and TMPDIR. Processes with different environments did not read each other's: 27 of 40 denied writes landed.
  • /run/user/<uid> goes at the last logout: a dropped SSH connection left 12 mount points for good.

What changes

  • Places, in write order: /var/tmp/srt-mount-points-<uid>; /run/user/<uid>/srt-mount-points, where /run/user/<uid> is the user's (alone, to be written under: a chmod from inside makes nobody read less); /tmp/srt-mount-points-<uid>; then the environment's two names. Checks unchanged.
  • Believed: the first three whenever they pass, writable or not; the last two only when none of the three can be written; the one settled on.
  • Out of the command's reach: every wrap that restricts writes makes, binds read-only and pins every place.
  • One pass over all: claim in each, read each again, decide; list each before every removal. Doubt in one is doubt. What a read-only one names is kept.
  • Ends: a manifest records the boot id, and one of another boot is finished, on file systems only this kernel reaches (an allowlist). Elsewhere only an equal boot id makes it this kernel's; else seven days. One of another PID namespace is live for seven days (docker restart). What is no JSON at a manifest's name (a crash) counts half a second, not an hour.

Seen

Same runs: 0 of 40 writes; nothing left after the logout. #584's end-to-end runs: unchanged.

Known limits (README)

A missing parent that the command may make; links inside a write root; another user taking a name; processes that share a project and none of the three places; over seven days in another PID namespace.

Tests

No suite reaches the real places (one did on #584): a seam, wrapper modules for child processes, a private mount namespace for three runs at the real names. 63 mutations each fail a test, on both runtimes.

… environment

Two processes of one user keep each other's mount points only when
each reads what the other writes. Where the manifests went followed
XDG_RUNTIME_DIR and TMPDIR, which differ between a login shell and what
a service, an IDE or cron starts: such processes did not see each
other, the second took the first's mount point for the user's own
file, and the first's clean-up removed it from under the second
sandbox.

The candidates are now /run/user/<uid>/srt-mount-points, where
/run/user/<uid> is a real directory of the user's alone, then
/tmp/srt-mount-points-<uid>, and only then the two names the
environment gives. A process writes to the first it can use.

It believes the manifests under the first two, and under the
environment's names only when it can use neither of the first two,
which is when it writes there, and in the directory it has settled on.
What the environment names is kept out of a sandbox's reach only by
processes with that environment, so a manifest found there is not
taken on trust by a process that has somewhere else to look.

Every candidate is still made when missing, bound read-only and pinned
by every wrap that restricts writes, whether or not the process writes
there: a command that may write /tmp would otherwise make the
directory itself and be believed. A clean-up makes one pass per
directory under that directory's lock, and a live manifest in any of
them keeps a path.
…touches the user's own manifest directories

Real bubblewrap runs with two real processes, in a mount namespace
where a private directory stands at /tmp and at /run/user: the deny
holds in a second sandbox after the first has ended, with different
XDG_RUNTIME_DIR and with different TMPDIR; a command that may write
/tmp can neither make, write nor move aside the directory under it; a
manifest forged under one process's TMPDIR by the sandbox of another
gets nothing removed.

bunfig.toml preloads stand-ins for the four places before any test
file is loaded, and children are handed a module or a program that
does the same, so no suite reads, writes or collects where the user's
own sandboxes keep their manifests. Several did.
@ronleizrowice-ant ronleizrowice-ant mentioned this pull request Sep 27, 2026
Comments only: the JavaScript emitted with comments removed is
byte-identical for every file, and the test names are unchanged. The
same standard as on the branch this one is stacked on, applied to the
comments this branch rewrites and adds.
… end

The manifest directory was taken from XDG_RUNTIME_DIR or TMPDIR, so two
processes of one user with different environments did not see each other,
and a logout, which empties /run/user/<uid>, left a killed process's
mount points in the project for good.

A process now writes in the first usable of /var/tmp/srt-mount-points-<uid>,
/run/user/<uid>/srt-mount-points and /tmp/srt-mount-points-<uid>, and reads
all three; what the environment names counts only when none of them can be
written. Every place is made, bound read-only and pinned by every wrap, so
that the sandboxed command cannot bring one into being. Not the home
directory: a network home caches listings, which the claim by rename
relies on.

Ends. A manifest carries the boot id: of another boot, it is finished. Of
another PID namespace, it is live for seven days. What can be a manifest
of no version (zero bytes, not JSON) is spent after the grace. The boot id
counts only on kinds of file system that one kernel owns, by an allowlist;
elsewhere a manifest is this kernel's only when both boot ids can be read
and are equal, since two hosts' PID namespaces can be named alike, and any
other is live for the seven days.

The permission bits of /run/user/<uid> count for writing under it, not for
reading: where it is a write root the command can chmod it, and must not
make other processes read less by that.
@ronleizrowice-ant ronleizrowice-ant changed the title linux: the manifest directory is worked out from the user id, not the environment linux: keep the mount point manifests where the user id says, first under /var/tmp Sep 29, 2026
@ronleizrowice-ant
ronleizrowice-ant merged commit db4ac8c into fix/linux-placeholder-liveness-lock Oct 20, 2026
8 checks passed
@ronleizrowice-ant
ronleizrowice-ant deleted the fix/linux-manifest-directory-from-uid branch October 20, 2026 06:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant