Skip to content

Refuse read-only rootfs and mounts without a mount namespace - #5479

Open
kolyshkin wants to merge 4 commits into
opencontainers:mainfrom
kolyshkin:readonlyfs-no-mntns
Open

kolyshkin wants to merge 4 commits into
opencontainers:mainfrom
kolyshkin:readonlyfs-no-mntns

Conversation

@kolyshkin

Copy link
Copy Markdown
Contributor

A read-only rootfs (root.readonly), as well as read-only tmpfs mounts and a
read-only /dev, are all implemented by remounting them read-only from
finalizeRootfs, which is only called for containers having their own mount
namespace (doing it otherwise would affect the host mounts).

As a result, such a configuration without a mount namespace was silently
started with those filesystems left writable -- no error, no warning --
leaving whoever relies on that hardening unprotected. Meanwhile,
maskedPaths and readonlyPaths, which have the very same requirement, are
properly refused.

Fail closed in those cases, too. Note that all other read-only mounts get
MS_RDONLY set right away, and thus are not affected.

While at it, fix process.user.umask being ignored (with not even the default
umask being set) for a container without a mount namespace -- setting the
umask has nothing to do with the rootfs setup, so it does not belong to
finalizeRootfs.

Fixes #5371.

A read-only rootfs (root.readonly in the OCI spec, config.Readonlyfs in
libcontainer) is implemented by remounting / read-only from finalizeRootfs,
which is only called for containers having their own mount namespace (doing
it otherwise would remount the host's / read-only).

As a result, a container configured with root.readonly: true and no mount
namespace was silently started with a writable rootfs -- no error, no
warning -- leaving whoever relies on that hardening unprotected.

Fail closed instead, the same way we already do for maskedPaths and
readonlyPaths, which have the very same requirement.

Note this requires amending two tests which use a configuration with a
read-only rootfs and no mount namespace (in the same manner as they already
amend hostname, maskedPaths and readonlyPaths, for the very same reason).

Fixes opencontainers#5371

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Setting the umask has nothing to do with the rootfs setup, yet it was done
in finalizeRootfs, which is only called when the container has its own mount
namespace. Because of that, a container without a mount namespace ignored
process.user.umask from the spec, and did not even set the default 0o022.

Move it to standard_init, keeping the original ordering (i.e. after the
rootfs is finalized), so that it is applied unconditionally.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Read-only tmpfs mounts, as well as a read-only /dev, are first mounted
read-write (as we need to modify their contents afterwards), and are only
made read-only later, by remounting them from finalizeRootfs. Same as with a
read-only rootfs, this is only done for containers having their own mount
namespace, so such mounts were silently left writable otherwise.

Refuse those configurations, too. Note that all other read-only mounts get
MS_RDONLY set right away, and thus are not affected.

To avoid the condition drifting apart between the two (now three) places
which need it, add configs.Mount.IsReadonlyDeferred and use it everywhere.
Note that filepath.Clean is used instead of pathrs.LexicallyCleanPath, as
the two only differ for relative paths (which can never be equal to "/dev"
either way), and libcontainer/configs should not depend on internal/pathrs.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
// Note that such a remount is only possible in a private mount namespace.
func (m *Mount) IsReadonlyDeferred() bool {
return m.Flags&unix.MS_RDONLY == unix.MS_RDONLY &&
(m.Device == "tmpfs" || filepath.Clean(m.Destination) == "/dev")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See commit description as to why filepath.Clean and pathrs.LexicallyCleanPath are equivalent in this case. In short, when we expect an absolute path, the input should also be absolute path, and in this case they are equivalent.

@kolyshkin
kolyshkin requested review from AkihiroSuda, lifubang and rata and removed request for rata September 17, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

root.readonly is silently ignored (rootfs left writable) when the container has no mount namespace

1 participant