From e46e2364bedafecadb573f2c1f9d074331990c7d Mon Sep 17 00:00:00 2001 From: Steele Ray Desmond Date: Fri, 19 Dec 2025 16:58:26 -0800 Subject: [PATCH 1/2] Implement best-effort mount cleanup with host mount namespace Signed-off-by: Steele Ray Desmond --- libcontainer/state_linux.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go index 1f758e2a301..df912921cd8 100644 --- a/libcontainer/state_linux.go +++ b/libcontainer/state_linux.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/opencontainers/cgroups" + "github.com/sirupsen/logrus" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" @@ -61,11 +62,36 @@ func destroy(c *Container) error { return fmt.Errorf("unable to remove container state dir: %w", err) } c.initProcess = nil + + if !c.config.Namespaces.IsPrivate(configs.NEWNS) { + unmountMounts(c) + } + err := runPoststopHooks(c) c.state = &stoppedState{c: c} return err } +func unmountMounts(c *Container) { + // Unmount recursive + if err := unix.Unmount(c.config.Rootfs, unix.MNT_DETACH); err == nil { + return + } + + // If recursive unmount fails, try best-effort unmount + // We iterate in reverse to unmount children before parents + for i := len(c.config.Mounts) - 1; i >= 0; i-- { + m := c.config.Mounts[i] + mountpoint := filepath.Join(c.config.Rootfs, m.Destination) + if err := unmount(mountpoint, unix.MNT_DETACH); err != nil { + logrus.Warn(err) + } + } + if err := unmount(c.config.Rootfs, unix.MNT_DETACH); err != nil { + logrus.Warn(err) + } +} + func runPoststopHooks(c *Container) error { hooks := c.config.Hooks if hooks == nil { From 879ab448b95239de148da7a59c20998a638bd648 Mon Sep 17 00:00:00 2001 From: Steele Ray Desmond Date: Fri, 19 Dec 2025 16:58:30 -0800 Subject: [PATCH 2/2] libcontainer: add best-effort mount cleanup for host mount namespace Signed-off-by: Steele Ray Desmond --- libcontainer/state_linux.go | 2 +- tests/integration/mounts.bats | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go index df912921cd8..7e37bef51e8 100644 --- a/libcontainer/state_linux.go +++ b/libcontainer/state_linux.go @@ -7,9 +7,9 @@ import ( "path/filepath" "github.com/opencontainers/cgroups" - "github.com/sirupsen/logrus" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) diff --git a/tests/integration/mounts.bats b/tests/integration/mounts.bats index dfaf4d7d341..e6e74f6e56f 100644 --- a/tests/integration/mounts.bats +++ b/tests/integration/mounts.bats @@ -384,3 +384,33 @@ test_mount_target() { rm -rf rootfs/tmp/hosts test_mount_target . /etc/hosts /tmp/hosts } + +# https://github.com/opencontainers/runc/pull/3118 +@test "runc delete [cleanup host mount namespace]" { + requires root + + # Remove 'mount' namespace and clear masked/readonly paths + update_config ' .linux.namespaces |= map(select(.type != "mount")) + | .linux.maskedPaths = [] + | .linux.readonlyPaths = [] ' + + runc run -d --console-socket "$CONSOLE_SOCKET" test_host_mnt + [ "$status" -eq 0 ] + + runc state test_host_mnt + [ "$status" -eq 0 ] + [[ "$output" == *"running"* ]] + + # Verify the rootfs is a mount point on the host. + ROOTFS_DIR="$PWD/rootfs" + mountpoint -q "$ROOTFS_DIR" + [ "$status" -eq 0 ] + + runc kill test_host_mnt KILL + runc delete test_host_mnt + [ "$status" -eq 0 ] + + # Verify the mount is gone. + run mountpoint -q "$ROOTFS_DIR" + [ "$status" -ne 0 ] +}