Skip to content

Commit 4e0b746

Browse files
authored
fix: 9p path parse error with WSL2 (#300)
1 parent c31bac7 commit 4e0b746

2 files changed

Lines changed: 114 additions & 30 deletions

File tree

‎mounts_linux.go‎

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"regexp"
11+
"strconv"
1112
"strings"
1213

1314
"golang.org/x/sys/unix"
@@ -39,7 +40,7 @@ const (
3940
// (9) mount source: filesystem specific information or "none".
4041
mountinfoMountSource = 9
4142
// (10) super options: per super block options.
42-
//mountinfoSuperOptions = 10
43+
mountinfoSuperOptions = 10
4344
)
4445

4546
// Stat returns the mountpoint's stat information.
@@ -53,7 +54,8 @@ func mounts() ([]Mount, []string, error) {
5354
filename := "/proc/self/mountinfo"
5455
lines, err := readLines(filename)
5556
if err != nil {
56-
return nil, nil, err
57+
// wrapcheck: add context to the error.
58+
return nil, nil, fmt.Errorf("reading mountinfo %q: %w", filename, err)
5759
}
5860

5961
ret := make([]Mount, 0, len(lines))
@@ -105,9 +107,9 @@ func mounts() ([]Mount, []string, error) {
105107
}
106108
d.DeviceType = deviceType(d)
107109

108-
// resolve /dev/mapper/* device names
110+
// Resolve /dev/mapper/* device names.
109111
if strings.HasPrefix(d.Device, "/dev/mapper/") {
110-
re := regexp.MustCompile(`^\/dev\/mapper\/(.*)-(.*)`)
112+
re := regexp.MustCompile(`^/dev/mapper/(.*)-(.*)`)
111113
match := re.FindAllStringSubmatch(d.Device, -1)
112114
if len(match) > 0 && len(match[0]) == 3 {
113115
d.Device = filepath.Join("/dev", match[0][1], match[0][2])
@@ -120,6 +122,51 @@ func mounts() ([]Mount, []string, error) {
120122
return ret, warnings, nil
121123
}
122124

125+
// splitMountInfoFields splits a mountinfo line into its fields.
126+
// It treats spaces and tabs as field separators and decodes certain octal escapes.
127+
func splitMountInfoFields(line string) []string {
128+
var fields []string
129+
var buf strings.Builder
130+
131+
for i := 0; i < len(line); i++ {
132+
c := line[i]
133+
134+
// Treat both space and tab as separators
135+
if c == ' ' || c == '\t' {
136+
if buf.Len() > 0 {
137+
fields = append(fields, buf.String())
138+
buf.Reset()
139+
}
140+
continue
141+
}
142+
143+
if c == '\\' && i+3 < len(line) {
144+
oct := line[i+1 : i+4]
145+
if v, err := strconv.ParseInt(oct, 8, 0); err == nil {
146+
switch byte(v) {
147+
case ' ', '\t', '\n':
148+
buf.WriteByte(byte(v))
149+
i += 3
150+
continue
151+
default:
152+
// keep unknown escapes as-is
153+
buf.WriteString("\\" + oct)
154+
i += 3
155+
continue
156+
}
157+
}
158+
}
159+
160+
buf.WriteByte(c)
161+
}
162+
163+
if buf.Len() > 0 {
164+
fields = append(fields, buf.String())
165+
}
166+
167+
return fields
168+
}
169+
123170
// parseMountInfoLine parses a line of /proc/self/mountinfo and returns the
124171
// amount of parsed fields and their values.
125172
func parseMountInfoLine(line string) (int, [11]string) {
@@ -130,43 +177,64 @@ func parseMountInfoLine(line string) (int, [11]string) {
130177
return 0, fields
131178
}
132179

180+
all := splitMountInfoFields(line)
181+
133182
var i int
134-
for _, f := range strings.Fields(line) {
135-
// when parsing the optional fields, loop until we find the separator
183+
sawSep := false
184+
sawSup := false
185+
186+
for _, f := range all {
187+
if i >= len(fields) {
188+
break
189+
}
190+
136191
if i == mountinfoOptionalFields {
137-
// (6) optional fields: zero or more fields of the form
138-
// "tag[:value]"; see below.
139-
// (7) separator: the end of the optional fields is marked
140-
// by a single hyphen.
192+
// (6) optional fields: zero or more fields of the form "tag[:value]"; see below.
193+
// (7) separator: the end of the optional fields is marked by a single hyphen.
141194
if f != "-" {
142-
if fields[i] == "" {
143-
fields[i] += f
144-
} else {
145-
fields[i] += " " + f
146-
}
147-
148-
// keep reading until we reach the separator
195+
// Join tokens with spaces for mountinfoOptionalFields.
196+
fields[i] = strings.TrimSpace(fields[i] + " " + f)
149197
continue
150198
}
151-
152-
// separator found, continue parsing
199+
// Found separator.
200+
sawSep = true
153201
i++
202+
fields[i] = f
203+
i++
204+
continue
205+
}
206+
207+
if i == mountinfoSuperOptions {
208+
// join tokens with spaces for WSL2 path=... they are splitted around spaces.
209+
fields[i] = strings.TrimSpace(fields[i] + " " + f)
210+
sawSup = true
211+
continue
154212
}
155213

214+
// Default case: copy with unescape for certain fields
156215
switch i {
157-
case mountinfoMountPoint:
158-
fallthrough
159-
case mountinfoMountSource:
160-
fallthrough
161-
case mountinfoFsType:
216+
case mountinfoMountPoint, mountinfoMountSource, mountinfoFsType:
162217
fields[i] = unescapeFstab(f)
163-
164218
default:
165219
fields[i] = f
166220
}
221+
i++
222+
}
167223

224+
// Handle malformed line (no "-" found).
225+
if !sawSep && len(all) > mountinfoOptionalFields {
226+
i = mountinfoOptionalFields
227+
}
228+
229+
// When super options are present, the index is one less than 11.
230+
if sawSup {
168231
i++
169232
}
170233

234+
// clear trailing empties.
235+
for j := i + 1; j < len(fields); j++ {
236+
fields[j] = ""
237+
}
238+
171239
return i, fields
172240
}

‎mounts_linux_test.go‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestGetFields(t *testing.T) {
2828
number: 0,
2929
},
3030
{
31-
input: " ",
31+
input: " ",
3232
number: 0,
3333
},
3434

@@ -42,7 +42,7 @@ func TestGetFields(t *testing.T) {
4242
number: 0,
4343
},
4444
{
45-
input: "# ",
45+
input: "# ",
4646
number: 0,
4747
},
4848
{
@@ -57,17 +57,17 @@ func TestGetFields(t *testing.T) {
5757
expected: [11]string{"1", "2"},
5858
},
5959
{
60-
input: "1 2",
60+
input: "1 2",
6161
number: 2,
6262
expected: [11]string{"1", "2"},
6363
},
6464
{
65-
input: "1 2 3",
65+
input: "1 2 3",
6666
number: 3,
6767
expected: [11]string{"1", "2", "3"},
6868
},
6969
{
70-
input: "1 2 3 4",
70+
input: "1 2 3 4",
7171
number: 4,
7272
expected: [11]string{"1", "2", "3", "4"},
7373
},
@@ -162,6 +162,22 @@ func TestGetFields(t *testing.T) {
162162
number: 11,
163163
expected: [11]string{"1111", "2", "3:3", "/", "/mnt/a\"b", "rw", "shared:7", "-", "tmpfs", "-", "rw,inode64"},
164164
},
165+
// WSL2 9p mount table line.
166+
{
167+
input: `380 383 0:33 / /usr/lib/wsl/drivers ro,nosuid,nodev,noatime - 9p drivers ro,dirsync,aname=drivers;fmask=222;dmask=222,mmap,access=client,msize=65536,trans=fd,rfd=8,wfd=8`,
168+
number: 11,
169+
expected: [11]string{"380", "383", "0:33", "/", "/usr/lib/wsl/drivers", "ro,nosuid,nodev,noatime", "", "-", "9p", "drivers", "ro,dirsync,aname=drivers;fmask=222;dmask=222,mmap,access=client,msize=65536,trans=fd,rfd=8,wfd=8"},
170+
},
171+
{
172+
input: `488 383 0:128 / /mnt/c rw,noatime - 9p C:\134 rw,dirsync,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=5,wfd=5`,
173+
number: 11,
174+
expected: [11]string{"488", "383", "0:128", "/", "/mnt/c", "rw,noatime", "", "-", "9p", "C:\\", "rw,dirsync,aname=drvfs;path=C:\\;uid=1000;gid=1000;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=5,wfd=5"},
175+
},
176+
{
177+
input: `516 78 0:136 / /Docker/host rw,noatime - 9p C:\134Program\040Files\134Docker\134Docker\134resources rw,dirsync,aname=drvfs;path=C:\Program Files\Docker\Docker\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3`,
178+
number: 11,
179+
expected: [11]string{"516", "78", "0:136", "/", "/Docker/host", "rw,noatime", "", "-", "9p", "C:\\Program Files\\Docker\\Docker\\resources", "rw,dirsync,aname=drvfs;path=C:\\Program Files\\Docker\\Docker\\resources;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=3,wfd=3"},
180+
},
165181
}
166182

167183
for _, tc := range tt {

0 commit comments

Comments
 (0)