From 61dea3749fae172ef4e4c70465bd8d5915c8e5db Mon Sep 17 00:00:00 2001 From: Chanhyeok Seo Date: Fri, 12 Jun 2026 15:03:19 +0900 Subject: [PATCH] machine: fix virtiofs mount targets on FCOS FCOS symlinks /home to var/home. systemd rejects a mount unit whose Where= path traverses a symlink, breaking default home dir mounts. Resolve known FCOS symlinks before writing the ignition unit. Fixes: #28911 Signed-off-by: Chanhyeok Seo --- pkg/machine/volume_systemd.go | 39 +++++++++++++++- pkg/machine/volume_systemd_test.go | 72 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 pkg/machine/volume_systemd_test.go diff --git a/pkg/machine/volume_systemd.go b/pkg/machine/volume_systemd.go index 28dcc08934..dda038948d 100644 --- a/pkg/machine/volume_systemd.go +++ b/pkg/machine/volume_systemd.go @@ -7,6 +7,38 @@ import ( "go.podman.io/podman/v6/pkg/systemd/parser" ) +// fcosDirSymlinks maps FCOS root-level symlinks to their real paths. +// FCOS symlinks several top-level dirs into /var for the read-only rootfs. +// systemd rejects a mount unit whose Where= traverses a symlink, so we +// resolve these before writing the ignition unit. +var fcosDirSymlinks = map[string]string{ + "/home": "/var/home", + "/mnt": "/var/mnt", + "/opt": "/var/opt", + "/root": "/var/roothome", + "/srv": "/var/srv", +} + +// canonicalizeFCOSMountTarget returns the canonical path for target by +// substituting any known FCOS root-level symlink prefix. +func canonicalizeFCOSMountTarget(target string) string { + // Guard check for empty strings or paths not starting with '/' + if len(target) < 2 || target[0] != '/' { + return target + } + + // Find the end of the first path component (e.g. "/home" in "/home/alice"). + end := 1 + for end < len(target) && target[end] != '/' { + end++ + } + + if real, ok := fcosDirSymlinks[target[:end]]; ok { + return real + target[end:] + } + return target +} + // GenerateSystemDFilesForVirtiofsMounts generates the systemd unit files needed // to mount virtiofs volumes inside a FCOS guest VM. It is shared between the // AppleHV, LibKrun, and QEMU providers. @@ -30,10 +62,13 @@ func GenerateSystemDFilesForVirtiofsMounts(mounts []VirtIoFs) ([]ignition.Unit, return nil, err } + // Use the canonical path so systemd accepts the Where= value. + // On FCOS /home is a symlink to var/home; systemd rejects non-canonical paths. + canonicalTarget := canonicalizeFCOSMountTarget(mnt.Target) virtiofsMount := ignition.Unit{ Enabled: ignition.BoolToPtr(true), - Name: fmt.Sprintf("%s.mount", parser.PathEscape(mnt.Target)), - Contents: ignition.StrToPtr(fmt.Sprintf(mountUnitFile, mnt.Tag, mnt.Target)), + Name: fmt.Sprintf("%s.mount", parser.PathEscape(canonicalTarget)), + Contents: ignition.StrToPtr(fmt.Sprintf(mountUnitFile, mnt.Tag, canonicalTarget)), } unitFiles = append(unitFiles, virtiofsMount) diff --git a/pkg/machine/volume_systemd_test.go b/pkg/machine/volume_systemd_test.go new file mode 100644 index 0000000000..7bda6f0487 --- /dev/null +++ b/pkg/machine/volume_systemd_test.go @@ -0,0 +1,72 @@ +package machine + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCanonicalizeFCOSMountTarget(t *testing.T) { + tests := []struct { + input string + expected string + }{ + // Known FCOS symlinks — must be rewritten + {"/home/alice", "/var/home/alice"}, + {"/home/alice/projects", "/var/home/alice/projects"}, + {"/mnt/data", "/var/mnt/data"}, + {"/opt/myapp", "/var/opt/myapp"}, + {"/root", "/var/roothome"}, + {"/root/.config", "/var/roothome/.config"}, + {"/srv/www", "/var/srv/www"}, + // Exact match on a symlinked dir itself + {"/home", "/var/home"}, + {"/mnt", "/var/mnt"}, + // Paths that do NOT start with a known symlink — unchanged + {"/var/home/alice", "/var/home/alice"}, + {"/tmp/foo", "/tmp/foo"}, + {"/data/work", "/data/work"}, + {"/work", "/work"}, + // Prefix collision guard: /homes should NOT match /home + {"/homes/alice", "/homes/alice"}, + {"/rootfs", "/rootfs"}, + } + + for _, tt := range tests { + got := canonicalizeFCOSMountTarget(tt.input) + assert.Equal(t, tt.expected, got, "input: %q", tt.input) + } +} + +func TestGenerateSystemDFilesForVirtiofsmountsCanonicalPath(t *testing.T) { + mounts := []VirtIoFs{ + NewVirtIoFsMount("/home/alice", "/home/alice", false), + NewVirtIoFsMount("/data", "/data", false), + } + + units, err := GenerateSystemDFilesForVirtiofsMounts(mounts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // First two units are the mount units; the rest are immutable-root helpers. + mountUnits := units[:2] + + cases := []struct { + wantName string + wantWhere string + }{ + // /home/alice must be rewritten to /var/home/alice + {"var-home-alice.mount", "/var/home/alice"}, + // /data has no FCOS symlink — stays as-is + {"data.mount", "/data"}, + } + + for i, c := range cases { + u := mountUnits[i] + assert.Equal(t, c.wantName, u.Name, "unit[%d].Name", i) + if assert.NotNil(t, u.Contents, "unit[%d].Contents", i) { + assert.Contains(t, *u.Contents, "Where="+c.wantWhere, "unit[%d] missing Where=", i) + } + } +}