1
0
mirror of https://github.com/opencontainers/runc.git synced 2026-10-23 19:49:01 +03:00

Merge pull request #5535 from kolyshkin/kir/no-pivot-host-mntns

libct/cfg/validate: disallow some bad configurations
This commit is contained in:
Sebastiaan van Stijn
2026-10-23 16:07:08 +00:00
committed by GitHub
6 changed files with 91 additions and 26 deletions

View File

@@ -12,6 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
they are always empty since the removal of veth network strategy in runc
1.0.0-rc6. (#5523)
### Changed ###
- A container configuration which uses `--no-pivot` or sets
`linux.rootfsPropagation`, but does not have its own mount namespace, is now
refused. Previously, `--no-pivot` resulted in host's `/proc` and `/sys` being
unmounted, and `rootfsPropagation` changed the propagation of host mounts.
(#5535)
- Using `/` as container rootfs is now refused. Previously, it failed with a
mount namespace, and without one, container mounts (such as `/proc`) were
mounted on top of the host ones. (#5535)
## [1.6.0-rc.1] - 2026-10-06
> Lo bueno, si breve, dos veces bueno.

View File

@@ -21,19 +21,20 @@ type check func(config *configs.Config) error
func Validate(config *configs.Config) error {
checks := []check{
cgroupsCheck,
rootfs,
network,
netdevices,
uts,
security,
namespaces,
sysctl,
intelrdtCheck,
rootlessEUIDCheck,
mountsStrict,
scheduler,
ioPriority,
memoryPolicy,
mountns,
mountsStrict,
namespaces,
netdevices,
network,
rootfs,
rootlessEUIDCheck,
scheduler,
selinuxCheck,
sysctl,
uts,
}
for _, c := range checks {
if err := c(config); err != nil {
@@ -68,6 +69,13 @@ func rootfs(config *configs.Config) error {
if filepath.Clean(config.Rootfs) != cleaned {
return errors.New("invalid rootfs: not an absolute path, or a symlink")
}
// Host's / can't be used as the container rootfs. With a private mount
// namespace, pivot_root (or MS_MOVE with no-pivot) fails. Without it,
// container mounts (such as proc, dev etc.) are mounted on top of the
// host ones.
if cleaned == "/" {
return errors.New("invalid rootfs: / is not allowed")
}
return nil
}
@@ -127,26 +135,41 @@ func uts(config *configs.Config) error {
return nil
}
func security(config *configs.Config) error {
// restrict sys without mount namespace
if (len(config.MaskPaths) > 0 || len(config.ReadonlyPaths) > 0) &&
!config.Namespaces.Contains(configs.NEWNS) {
// mountns checks the configuration which requires a private mount namespace
// (since otherwise it would affect the host).
func mountns(config *configs.Config) error {
if config.Namespaces.Contains(configs.NEWNS) {
return nil
}
// Restricting sys entries is done by mounting over them.
if len(config.MaskPaths) > 0 || len(config.ReadonlyPaths) > 0 {
return errors.New("unable to restrict sys entries without a private MNT namespace")
}
// A read-only rootfs is implemented by remounting / read-only, which can
// only be done in a private mount namespace (otherwise it would affect
// the host).
if config.Readonlyfs && !config.Namespaces.Contains(configs.NEWNS) {
// A read-only rootfs is implemented by remounting / read-only.
if config.Readonlyfs {
return errors.New("unable to make rootfs read-only without a private MNT namespace")
}
// Same for mounts which can only be made read-only by a remount.
if !config.Namespaces.Contains(configs.NEWNS) {
for _, m := range config.Mounts {
if m.IsReadonlyDeferred() {
return fmt.Errorf("unable to make %s read-only without a private MNT namespace", m.Destination)
}
for _, m := range config.Mounts {
if m.IsReadonlyDeferred() {
return fmt.Errorf("unable to make %s read-only without a private MNT namespace", m.Destination)
}
}
// Without pivot_root, the container rootfs is moved on top of /
// (and host's procfs and sysfs mounts are unmounted).
if config.NoPivotRoot {
return errors.New("unable to use no-pivot without a private MNT namespace")
}
// Rootfs propagation is applied to / and to the parent mount of
// the container rootfs.
if config.RootPropagation != 0 {
return errors.New("unable to set rootfs propagation without a private MNT namespace")
}
return nil
}
func selinuxCheck(config *configs.Config) error {
if config.ProcessLabel != "" && !selinux.GetEnabled() {
return errors.New("selinux label is specified in config, but selinux is disabled or not supported")
}

View File

@@ -538,7 +538,7 @@ func TestLinuxCgroupsPathNotSpecified(t *testing.T) {
func TestSpecconvExampleValidate(t *testing.T) {
spec := Example()
spec.Root.Path = "/"
spec.Root.Path = t.TempDir()
opts := &CreateOpts{
CgroupName: "ContainerID",
@@ -563,7 +563,7 @@ func TestSpecconvExampleValidate(t *testing.T) {
func TestSpecconvNoLinuxSection(t *testing.T) {
spec := Example()
spec.Root.Path = "/"
spec.Root.Path = t.TempDir()
spec.Root.Readonly = false // Requires a mount namespace.
spec.Linux = nil
spec.Hostname = ""
@@ -650,7 +650,7 @@ func TestNonZeroEUIDCompatibleSpecconvValidate(t *testing.T) {
}
spec := Example()
spec.Root.Path = "/"
spec.Root.Path = t.TempDir()
ToRootless(spec)
opts := &CreateOpts{

View File

@@ -19,3 +19,14 @@ function teardown() {
run -0 runc run test_shared_rootfs
assert_output "shared"
}
@test "runc run [rootfsPropagation, host mount ns] must fail" {
update_config ' .linux.rootfsPropagation = "rslave"
| .linux.namespaces -= [{"type": "mount"}]
| .linux.maskedPaths = []
| .linux.readonlyPaths = []
| .root.readonly = false'
run -1 runc run test_host_mntns
assert_output --partial "unable to set rootfs propagation without a private MNT namespace"
}

View File

@@ -20,3 +20,17 @@ function teardown() {
run -1 runc run --no-pivot test_no_pivot
assert_output --partial "mount: permission denied"
}
@test "runc run --no-pivot [host mount ns] must fail" {
# Unsafe because, if the bug is present, host's /proc and /sys
# are unmounted, breaking the host.
requires root unsafe
update_config ' .linux.namespaces -= [{"type": "mount"}]
| .linux.maskedPaths = []
| .linux.readonlyPaths = []
| .root.readonly = false'
run -1 runc run --no-pivot test_no_pivot
assert_output --partial "unable to use no-pivot without a private MNT namespace"
}

View File

@@ -211,3 +211,10 @@ EOF
run -0 runc run test_busybox
assert_line --index 0 "/home/tempuser"
}
@test "runc run [rootfs is /] must fail" {
update_config '.root.path = "/"'
run -1 runc run test_hello
assert_output --partial "invalid rootfs: / is not allowed"
}