fix(containermanager): handle podman+runc scenario (#51)

When using Podman on a system that uses runc, we must manually add root
mounts to ensure they are mounted as read only volumes
This commit is contained in:
Emanuele De Cupis
2026-06-14 13:04:27 +02:00
committed by Alessio Biancalana
parent b7e48d7069
commit 02fd08ab89
2 changed files with 91 additions and 1 deletions
@@ -77,6 +77,7 @@ func (p *Podman) Create(
}
cmd := p.makeCreateCommand(
ctx,
opts.ContainerName,
opts.ContainerImage,
opts.AdditionalFlags,
@@ -112,6 +113,7 @@ func (p *Podman) Create(
//
//nolint:gocognit,funlen // ignore cognitive complexity here, the function is mostly imperative option appending
func (p *Podman) makeCreateCommand(
ctx context.Context,
containerName string,
containerImage string,
containerAdditionalFlags []string,
@@ -202,7 +204,18 @@ func (p *Podman) makeCreateCommand(
fmt.Sprintf("%s:%s", distroboxHostexecPath, "/usr/bin/distrobox-host-exec:ro"),
)
options = append(options, "--volume", fmt.Sprintf("%s:%s:rslave", containerUserHome, containerUserHome))
options = append(options, "--volume", "/:/run/host/:rslave")
// Due to breaking change in https://github.com/opencontainers/runc/commit/d4b670fca6d0ac606777376440ffe49686ce15f4
// now we cannot mount /:/run/host as before, as it will try to mount RO partitions as RW thus breaking things.
// This will ensure we will mount directories one-by-one thus avoiding this problem.
//
// This happens ONLY with podman+runc, docker and lilipod are unaffected,
// so let's do this only if we have podman AND runc.
if p.usesRunc(ctx) {
options = append(options, hostRootMountsForRunc(ctx)...)
} else {
options = append(options, "--volume", "/:/run/host/:rslave")
}
if !unshareDevsys {
options = append(options, "--volume", "/dev:/dev:rslave")
@@ -579,6 +592,74 @@ func commandExists(cmd string) bool {
return err == nil
}
// usesRunc detects whether podman is configured to use runc as the OCI runtime.
// Equivalent to shell: podman info 2>/dev/null | grep -q runc.
func (p *Podman) usesRunc(ctx context.Context) bool {
out, err := p.run(ctx, []string{"info"}, runOptions{})
if err != nil {
return false
}
return strings.Contains(out, "runc")
}
// hostRootMountsForRunc returns per-directory volume mounts for /run/host,
// working around a runc breaking change that prevents mounting / as a whole.
func hostRootMountsForRunc(ctx context.Context) []string {
var mounts []string
entries, err := os.ReadDir("/")
if err != nil {
return mounts
}
for _, entry := range entries {
// Skip hidden directories (shell glob /* doesn't match dotfiles)
if strings.HasPrefix(entry.Name(), ".") {
continue
}
rootdir := "/" + entry.Name()
// Skip symlinks
info, err := os.Lstat(rootdir)
if err != nil || info.Mode()&os.ModeSymlink != 0 {
continue
}
target := fmt.Sprintf("%s:/run/host%s", rootdir, rootdir)
if isMountReadOnly(ctx, rootdir) {
mounts = append(mounts, "--volume", target+":ro,rslave")
} else {
mounts = append(mounts, "--volume", target+":rslave")
}
}
return mounts
}
// isMountReadOnly checks if the given path resides on a read-only mount
// by parsing findmnt output. Equivalent to shell:
// findmnt --notruncate --noheadings --list --output OPTIONS --target "$path" | tr ',' '\n' | grep -q "^ro$".
func isMountReadOnly(ctx context.Context, path string) bool {
out, err := exec.CommandContext(
ctx,
"findmnt", "--notruncate", "--noheadings", "--list",
"--output", "OPTIONS", "--target", path,
).Output()
if err != nil {
return false
}
for _, opt := range strings.Split(string(out), ",") {
if strings.TrimSpace(opt) == "ro" {
return true
}
}
return false
}
func (p *Podman) Commit(ctx context.Context, containerID string, tag string) error {
_, err := p.run(ctx, []string{"container", "commit", containerID, tag}, runOptions{})
return err
@@ -25,6 +25,7 @@ func TestPodman_makeCreateCommand(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container", // containerName
"my-image", // containerImage
containerAdditionalFlags, // containerAdditionalFlags
@@ -112,6 +113,7 @@ func TestPodman_makeCreateCommandRootful(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -165,6 +167,7 @@ func TestPodman_makeCreateCommandNoInit(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -220,6 +223,7 @@ func TestPodman_makeCreateCommandWithCrun(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -273,6 +277,7 @@ func TestPodman_makeCreateCommandWithPlatform(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -316,6 +321,7 @@ func TestPodman_makeCreateCommandWithCustomHome(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -380,6 +386,7 @@ func TestPodman_makeCreateCommandWithAdditionalFlags(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
additionalFlags,
@@ -432,6 +439,7 @@ func TestPodman_makeCreateCommandWithAdditionalPackages(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},
@@ -534,6 +542,7 @@ func TestPodman_makeCreateCommandUnshareOptions(t *testing.T) {
}
cmd := podman.makeCreateCommand(
t.Context(),
"my-container",
"my-image",
[]string{},