Build1 publisher3 min readPublished
systemd's mstack mounts a single-layer stack writable by default
systemd's mstack mounted a single-layer directory writable by default in a dev.to test, and one written line overwrote a read-only file with exit code 0. Anyone shipping a one-layer stack in a unit file has to prove read-only with a test write.
The Engineer · Build desk

What happened
- mstack landed in systemd 260 in March, systemd-nspawn gained more support for it in 261 in June, and 261.3 ships in the current Arch Linux image.
- On 261.3, a one-line write into a single-layer mstack mount permanently replaced a file marked read-only, and the tool reported no error, no warning and exit code 0.
- Inside a default Docker container, whose root sits on overlay2, the same mount fails with a '(layerfd)' error and 'Invalid argument'.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure A service given a one-layer RootMStack= could modify the directory it was meant only to read, and the Docker-only tests leave that service path unverified.
- constraint Deployment checks that trust the exit status cannot tell a read-only stack from a writable one; only a write probe plus inspection of the source directory separates them.
- decision Evaluating mstack means moving the test off a default Docker filesystem onto tmpfs or a bind-mounted host directory, or the overlay never gets built.
- capability Teams get mount stacks they can inspect as symlinks and share as directories, for about a millisecond of extra mount time on the author's two-layer test.
The overlayfs model behind mstack has two kinds of layer: read-only lowers and one writable top [1]. In the directory format, the lowers are symlinks named layer@0, layer@1 and so on, and the writable top is an rw/ subdirectory [5]. In a correctly built stack, writes go to rw/ and the lowers stay untouched [1][5]. For a stack with a single layer, the dev.to post reports that mstack mounts it writable by default [4]. The overwrite was permanent. So the write did not stop in a scratch upper directory. It reached the file the layer pointed at [1].
For automation, the exit status is the bigger problem. The mount returned 0, with no error and no warning [3]. A provisioning script that checks $? sees success. The only check that catches the write-through is a test write followed by a look at the source directory.
The service paths matter because nspawn and RootMStack= read the same directory [6]. The author ran every test in Docker against 261.3, because the sandbox had no systemd running as PID 1 [7]. With no systemd as PID 1, no service using RootMStack= could have run, and every command shown in the post is systemd-mstack [2]. The published portion of the post does not show which option or layout keeps a one-layer stack read-only. Until that is documented, I think the safe assumption for a unit file is that a single-layer stack writes through.
Docker is also where the failure path goes wrong. The hand-written equivalent, mount -t overlay, fails there with "overlay: filesystem on upper not supported as upperdir" [9]. The kernel will not build an overlay on directories that already sit on overlayfs, and that limit predates 261 [9]. The author wrote that mstack explains the failure worse, because its message names an internal function, (layerfd), instead of the actual constraint [13]. Most people's first instinct is to test inside a plain Docker container, the author wrote, so most evaluators will get the confusing version first [14]. Putting the layers on tmpfs or on a bind-mounted host directory avoids the problem. An ext-family host directory mounted cleanly with exit 0 [10].
The rest of the design is careful work. The docs order layers by version sort so that layer@10 does not land before layer@2. The author's test with layer@1 to layer@3 and layer@10 to layer@12 came back in numeric order [11]. Over five mounts each of a two-layer stack on tmpfs, mstack ran roughly a millisecond slower than the hand-written command [12]. The author attributed the gap to resolving symlinks and building the option string [12]. The number applies to stacks shaped like the test: two layers on tmpfs. I'd expect the cost to rise with each extra symlink to resolve, and the post measured two layers.
What to watch
- Whether a 261.x point release or systemd 262 changes the single-layer default or documents a switch that mounts a lone layer read-only.
- A test of RootMStack= in a real unit under systemd as PID 1 showing whether the service path inherits the writable default.
- Whether systemd-mstack replaces the '(layerfd)' message with the kernel's overlay-on-overlay upperdir constraint.