Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mountinfo: fix path unescaping #16

Merged
merged 2 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions mountinfo/mountinfo_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,18 @@ func parseInfoFile(r io.Reader, filter FilterFunc) ([]*Info, error) {
p := &Info{}

// Fill in the fields that a filter might check
p.Mountpoint, err = strconv.Unquote(`"` + fields[4] + `"`)
p.Mountpoint, err = unescape(fields[4])
if err != nil {
return nil, fmt.Errorf("Parsing '%s' failed: unable to unquote mount point field: %w", fields[4], err)
return nil, fmt.Errorf("Parsing '%s' failed: mount point: %w", fields[4], err)
}
p.Fstype, err = unescape(fields[sepIdx+1])
if err != nil {
return nil, fmt.Errorf("Parsing '%s' failed: fstype: %w", fields[sepIdx+1], err)
}
p.Source, err = unescape(fields[sepIdx+2])
if err != nil {
return nil, fmt.Errorf("Parsing '%s' failed: source: %w", fields[sepIdx+2], err)
}
p.Fstype = fields[sepIdx+1]
p.Source = fields[sepIdx+2]
p.VfsOpts = fields[sepIdx+3]

// Run a filter soon so we can skip parsing/adding entries
Expand All @@ -101,9 +107,9 @@ func parseInfoFile(r io.Reader, filter FilterFunc) ([]*Info, error) {
p.Major, _ = strconv.Atoi(mm[0])
p.Minor, _ = strconv.Atoi(mm[1])

p.Root, err = strconv.Unquote(`"` + fields[3] + `"`)
p.Root, err = unescape(fields[3])
if err != nil {
return nil, fmt.Errorf("Parsing '%s' failed: unable to unquote root field: %w", fields[3], err)
return nil, fmt.Errorf("Parsing '%s' failed: root: %w", fields[3], err)
}

p.Opts = fields[5]
Expand Down Expand Up @@ -150,3 +156,61 @@ func PidMountInfo(pid int) ([]*Info, error) {

return parseInfoFile(f, nil)
}

// A few specific characters in mountinfo path entries (root and mountpoint)
// are escaped using a backslash followed by a character's ascii code in octal.
//
// space -- as \040
// tab (aka \t) -- as \011
// newline (aka \n) -- as \012
// backslash (aka \\) -- as \134
//
// This function converts path from mountinfo back, i.e. it unescapes the above sequences.
func unescape(path string) (string, error) {
// try to avoid copying
if strings.IndexByte(path, '\\') == -1 {
return path, nil
}

// The following code is UTF-8 transparent as it only looks for some
// specific characters (backslach and 0..7) with values < utf8.RuneSelf,
// and everything else is passed through as is.
buf := make([]byte, len(path))
bufLen := 0
for i := 0; i < len(path); i++ {
if path[i] != '\\' {
buf[bufLen] = path[i]
bufLen++
continue
}
s := path[i:]
if len(s) < 4 {
// too short
return "", fmt.Errorf("bad escape sequence %q: too short", s)
}
c := s[1]
switch c {
case '0', '1', '2', '3', '4', '5', '6', '7':
v := c - '0'
for j := 2; j < 4; j++ { // one digit already; two more
x := s[j] - '0'
if x < 0 || x > 7 {
return "", fmt.Errorf("bad escape sequence %q: not a digit", s[:3])
}
v = (v << 3) | x
}
if v > 255 {
return "", fmt.Errorf("bad escape sequence %q: out of range" + s[:3])
}
buf[bufLen] = v
bufLen++
i += 3
continue
default:
return "", fmt.Errorf("bad escape sequence %q: not a digit" + s[:3])

}
}

return string(buf[:bufLen]), nil
}
61 changes: 60 additions & 1 deletion mountinfo/mountinfo_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,8 @@ const (
99 15 8:33 / /media/REMOVE\040ME rw,nosuid,nodev,relatime - fuseblk /dev/sdc1 rw,user_id=0,group_id=0,allow_other,blksize=4096`

mountInfoWithSpaces = `486 28 252:1 / /mnt/foo\040bar rw,relatime shared:243 - ext4 /dev/vda1 rw,data=ordered
31 21 0:23 / /DATA/foo_bla_bla rw,relatime - cifs //foo/BLA\040BLA\040BLA/ rw,sec=ntlm,cache=loose,unc=\\foo\BLA BLA BLA,username=my_login,domain=mydomain.com,uid=12345678,forceuid,gid=12345678,forcegid,addr=10.1.30.10,file_mode=0755,dir_mode=0755,nounix,rsize=61440,wsize=65536,actimeo=1`
31 21 0:23 / /DATA/foo_bla_bla rw,relatime - cifs //foo/BLA\040BLA\040BLA/ rw,sec=ntlm,cache=loose,unc=\\foo\BLA BLA BLA,username=my_login,domain=mydomain.com,uid=12345678,forceuid,gid=12345678,forcegid,addr=10.1.30.10,file_mode=0755,dir_mode=0755,nounix,rsize=61440,wsize=65536,actimeo=1
649 94 259:5 /tmp/newline\012tab\011space\040backslash\134quote1'quote2" /tmp/newline\012tab\011space\040backslash\134quote1'quote2" rw,relatime shared:47 - ext4 /dev/nvme0n1p5 rw,seclabel`
)

func TestParseFedoraMountinfo(t *testing.T) {
Expand Down Expand Up @@ -511,6 +512,21 @@ func TestParseMountinfoWithSpaces(t *testing.T) {
Source: `//foo/BLA\040BLA\040BLA/`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is the test that's failing

VfsOpts: `rw,sec=ntlm,cache=loose,unc=\\foo\BLA`,
},
{
ID: 649,
Parent: 94,
Major: 259,
Minor: 5,
Root: `/tmp/newline
tab space backslash\quote1'quote2"`,
Mountpoint: `/tmp/newline
tab space backslash\quote1'quote2"`,
Opts: "rw,relatime",
Optional: "shared:47",
Fstype: "ext4",
Source: `/dev/nvme0n1p5`,
VfsOpts: `rw,seclabel`,
},
}

if len(infos) != len(expected) {
Expand Down Expand Up @@ -655,3 +671,46 @@ func TestParseMountinfoExtraCases(t *testing.T) {
}
}
}

func TestUnescape(t *testing.T) {
testCases := []struct {
input, output string
isErr bool
}{
{"", "", false},
{"/", "/", false},
{"/some/longer/path", "/some/longer/path", false},
{"/path\\040with\\040spaces", "/path with spaces", false},
{"/path/with\\134backslash", "/path/with\\backslash", false},
{"/tab\\011in/path", "/tab\tin/path", false},
{`/path/"with'quotes`, `/path/"with'quotes`, false},
{`/path/"with'quotes,\040space,\011tab`, `/path/"with'quotes, space, tab`, false},
{`\12`, "", true},
{`\134`, `\`, false},
{`"'"'"'`, `"'"'"'`, false},
{`/\1345`, `/\5`, false},
{`/\12x`, "", true},
{`\0`, "", true},
{`\x`, "", true},
{"\\\\", "", true},
}

for _, tc := range testCases {
res, err := unescape(tc.input)
if tc.isErr == true {
if err == nil {
t.Errorf("Input %q, want error, got nil", tc.input)
}
// no more checks
continue
}
if res != tc.output {
t.Errorf("Input %q, want %q, got %q", tc.input, tc.output, res)
}
if err != nil {
t.Errorf("Input %q, want nil, got error %v", tc.input, err)
continue
}
}

}