From fe4df2daca64fa532f500546625389b12504b6cb Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:42:24 +0700 Subject: [PATCH 1/2] fix(mountinfo): join super options split on spaces Locate the mountinfo separator field instead of assuming a fixed offset, and join any trailing fields into super options so unescaped spaces in values (e.g. Docker Desktop 9p path= on WSL2) do not break parsing. Use SplitN when parsing key=value mount options. Fixes #872 Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- mountinfo.go | 43 ++++++++++++++++++++++++++++++++++--------- mountinfo_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/mountinfo.go b/mountinfo.go index a011edc6..8994730e 100644 --- a/mountinfo.go +++ b/mountinfo.go @@ -81,9 +81,24 @@ func parseMountInfoString(mountString string) (*MountInfo, error) { return nil, fmt.Errorf("%w: Too few fields in mount string: %s", ErrFileParse, mountString) } - if mountInfo[mountInfoLength-4] != "-" { + sepIdx := -1 + for i := 6; i < mountInfoLength; i++ { + if mountInfo[i] == "-" { + sepIdx = i + break + } + } + if sepIdx == -1 { return nil, fmt.Errorf("%w: couldn't find separator in expected field: %s", ErrFileParse, mountInfo[mountInfoLength-4]) } + if sepIdx+2 >= mountInfoLength { + return nil, fmt.Errorf("%w: Too few fields in mount string: %s", ErrFileParse, mountString) + } + + superOptions := "" + if sepIdx+3 < mountInfoLength { + superOptions = strings.Join(mountInfo[sepIdx+3:], " ") + } mount := &MountInfo{ MajorMinorVer: mountInfo[2], @@ -91,9 +106,9 @@ func parseMountInfoString(mountString string) (*MountInfo, error) { MountPoint: mountInfo[4], Options: mountOptionsParser(mountInfo[5]), OptionalFields: nil, - FSType: mountInfo[mountInfoLength-3], - Source: mountInfo[mountInfoLength-2], - SuperOptions: mountOptionsParser(mountInfo[mountInfoLength-1]), + FSType: mountInfo[sepIdx+1], + Source: mountInfo[sepIdx+2], + SuperOptions: mountOptionsParser(superOptions), } mount.MountID, err = strconv.Atoi(mountInfo[0]) @@ -106,11 +121,21 @@ func parseMountInfoString(mountString string) (*MountInfo, error) { } // Has optional fields, which is a space separated list of values. // Example: shared:2 master:7 - if mountInfo[6] != "" { - mount.OptionalFields, err = mountOptionsParseOptionalFields(mountInfo[6 : mountInfoLength-4]) - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrFileParse, err) + if sepIdx > 6 { + optional := make([]string, 0, sepIdx-6) + for _, field := range mountInfo[6:sepIdx] { + if field != "" { + optional = append(optional, field) + } + } + if len(optional) > 0 { + mount.OptionalFields, err = mountOptionsParseOptionalFields(optional) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrFileParse, err) + } } + } else if mountInfo[6] == "-" { + mount.OptionalFields = map[string]string{} } return mount, nil } @@ -148,7 +173,7 @@ func mountOptionsParseOptionalFields(o []string) (map[string]string, error) { func mountOptionsParser(mountOptions string) map[string]string { opts := make(map[string]string) for opt := range strings.SplitSeq(mountOptions, ",") { - splitOption := strings.Split(opt, "=") + splitOption := strings.SplitN(opt, "=", 2) if len(splitOption) < 2 { key := splitOption[0] opts[key] = "" diff --git a/mountinfo_test.go b/mountinfo_test.go index 832edac2..01334fdf 100644 --- a/mountinfo_test.go +++ b/mountinfo_test.go @@ -196,6 +196,32 @@ func TestMountInfo(t *testing.T) { SuperOptions: map[string]string{"rw": "", "data": "ordered"}, }, }, + { + name: "9p Docker Desktop mount with space in super option path", + s: "3196 80 0:172 / /Docker/host rw,noatime - 9p C:\\Program\\040Files\\Docker\\Docker\\resources rw,aname=drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/,cache=5,access=client,msize=65536,trans=fd,rfd=3,wfd=3", + mount: &MountInfo{ + MountID: 3196, + ParentID: 80, + MajorMinorVer: "0:172", + Root: "/", + MountPoint: "/Docker/host", + Options: map[string]string{"rw": "", "noatime": ""}, + OptionalFields: map[string]string{}, + FSType: "9p", + Source: `C:\Program\040Files\Docker\Docker\resources`, + SuperOptions: map[string]string{ + "rw": "", + "aname": "drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/", + "cache": "5", + "access": "client", + "msize": "65536", + "trans": "fd", + "rfd": "3", + "wfd": "3", + }, + }, + invalid: false, + }, } for i, test := range tests { From 69effe4ed942597a1ed7c9c6d2572f3526d859e3 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:48:42 +0700 Subject: [PATCH 2/2] fix(mountinfo): gofmt the super-options test fixture CI lint failed because the composite literal alignment was not gofmt-clean. Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- mountinfo_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mountinfo_test.go b/mountinfo_test.go index 01334fdf..f416a895 100644 --- a/mountinfo_test.go +++ b/mountinfo_test.go @@ -210,14 +210,14 @@ func TestMountInfo(t *testing.T) { FSType: "9p", Source: `C:\Program\040Files\Docker\Docker\resources`, SuperOptions: map[string]string{ - "rw": "", - "aname": "drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/", - "cache": "5", - "access": "client", - "msize": "65536", - "trans": "fd", - "rfd": "3", - "wfd": "3", + "rw": "", + "aname": "drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/", + "cache": "5", + "access": "client", + "msize": "65536", + "trans": "fd", + "rfd": "3", + "wfd": "3", }, }, invalid: false,