diff --git a/container/mount.go b/container/mount.go index fc2a16e..6b134ce 100644 --- a/container/mount.go +++ b/container/mount.go @@ -149,7 +149,7 @@ func (p *procPaths) remount(target string, flags uintptr) error { return p.mountinfo(func(d *vfs.MountInfoDecoder) error { n, err := d.Unfold(targetKFinal) if err != nil { - if errors.Is(err, ESTALE) { + if errors.As(err, new(vfs.UnfoldTargetError)) { return msg.WrapErr(err, fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) } diff --git a/container/mount_test.go b/container/mount_test.go index e50af31..f2f8e29 100644 --- a/container/mount_test.go +++ b/container/mount_test.go @@ -109,7 +109,7 @@ func TestRemount(t *testing.T) { {"close", expectArgs{0xdeadbeef}, nil, errUnique}, }}, errUnique}, - {"mountinfo stale", func(k syscallDispatcher) error { + {"mountinfo no match", func(k syscallDispatcher) error { return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, [][]kexpect{{ {"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil}, @@ -118,7 +118,7 @@ func TestRemount(t *testing.T) { {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil}, {"close", expectArgs{0xdeadbeef}, nil, nil}, {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, - }}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)}, + }}, msg.WrapErr(&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)}, {"mountinfo", func(k syscallDispatcher) error { return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) @@ -128,7 +128,7 @@ func TestRemount(t *testing.T) { {"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, {"close", expectArgs{0xdeadbeef}, nil, nil}, {"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil}, - }}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)}, + }}, wrapErrSuffix(&vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}, `cannot parse mountinfo:`)}, {"mount", func(k syscallDispatcher) error { return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) diff --git a/container/path_test.go b/container/path_test.go index c3b68b3..7184b0b 100644 --- a/container/path_test.go +++ b/container/path_test.go @@ -248,7 +248,7 @@ func TestProcPaths(t *testing.T) { t.Fatalf("WriteFile: error = %v", err) } - wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:") + wantErr := wrapErrSuffix(&vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}, "cannot parse mountinfo:") if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) { t.Fatalf("mountinfo: error = %v, want %v", err, wantErr) } diff --git a/container/vfs/mountinfo.go b/container/vfs/mountinfo.go index bcb3063..5f9f225 100644 --- a/container/vfs/mountinfo.go +++ b/container/vfs/mountinfo.go @@ -24,6 +24,32 @@ var ( ErrMountInfoSep = errors.New("bad optional fields separator") ) +type DecoderError struct { + Op string + Line int + Err error +} + +func (e *DecoderError) Unwrap() error { return e.Err } +func (e *DecoderError) Error() string { + var s string + + var numError *strconv.NumError + switch { + case errors.As(e.Err, &numError) && numError != nil: + s = "numeric field " + strconv.Quote(numError.Num) + " " + numError.Err.Error() + + default: + s = e.Err.Error() + } + + var atLine string + if e.Line >= 0 { + atLine = " at line " + strconv.Itoa(e.Line) + } + return e.Op + " mountinfo" + atLine + ": " + s +} + type ( // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. MountInfoDecoder struct { @@ -32,6 +58,7 @@ type ( current *MountInfo parseErr error + curLine int complete bool } @@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] { func (d *MountInfoDecoder) Err() error { if err := d.s.Err(); err != nil { - return err + return &DecoderError{"scan", d.curLine, err} } - return d.parseErr + if d.parseErr != nil { + return &DecoderError{"parse", d.curLine, d.parseErr} + } + return nil } func (d *MountInfoDecoder) scan() bool { @@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool { d.current.Next = m d.current = d.current.Next } + d.curLine++ return true } diff --git a/container/vfs/mountinfo_test.go b/container/vfs/mountinfo_test.go index fa75b3c..7d9b240 100644 --- a/container/vfs/mountinfo_test.go +++ b/container/vfs/mountinfo_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "iter" + "os" "path" "reflect" "slices" @@ -15,62 +16,102 @@ import ( "hakurei.app/container/vfs" ) +func TestDecoderError(t *testing.T) { + testCases := []struct { + name string + err *vfs.DecoderError + want string + target error + targetF error + }{ + {"errno", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: syscall.ENOTRECOVERABLE}, + "parse mountinfo at line 3735928559: state not recoverable", syscall.ENOTRECOVERABLE, syscall.EROFS}, + + {"strconv", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}}, + `parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, strconv.ErrSyntax, os.ErrInvalid}, + + {"unfold", &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/proc/nonexistent")}, + "unfold mountinfo: mount point /proc/nonexistent never appeared in mountinfo", vfs.UnfoldTargetError("/proc/nonexistent"), os.ErrNotExist}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("error", func(t *testing.T) { + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error: %s, want %s", got, tc.want) + } + }) + + t.Run("is", func(t *testing.T) { + if !errors.Is(tc.err, tc.target) { + t.Errorf("Is: unexpected false") + } + if errors.Is(tc.err, tc.targetF) { + t.Errorf("Is: unexpected true") + } + }) + }) + } +} + func TestMountInfo(t *testing.T) { testCases := []mountInfoTest{ {"count", sampleMountinfoBase + ` 21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoFields, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoFields}, + "", nil, nil, nil}, {"sep", sampleMountinfoBase + ` 21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoSep, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoSep}, + "", nil, nil, nil}, {"id", sampleMountinfoBase + ` id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - strconv.ErrSyntax, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "id", Err: strconv.ErrSyntax}}, + "", nil, nil, nil}, {"parent", sampleMountinfoBase + ` 21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - strconv.ErrSyntax, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "parent", Err: strconv.ErrSyntax}}, "", nil, nil, nil}, {"devno", sampleMountinfoBase + ` 21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "unexpected EOF", nil, nil, nil}, + nil, "parse mountinfo at line 6: unexpected EOF", nil, nil, nil}, {"maj", sampleMountinfoBase + ` 21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "expected integer", nil, nil, nil}, + nil, "parse mountinfo at line 6: expected integer", nil, nil, nil}, {"min", sampleMountinfoBase + ` 21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - nil, "expected integer", nil, nil, nil}, + nil, "parse mountinfo at line 6: expected integer", nil, nil, nil}, {"mountroot", sampleMountinfoBase + ` 21 20 0:53 /mnt/test rw,relatime - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, {"target", sampleMountinfoBase + ` 21 20 0:53 / rw,relatime - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, {"vfs options", sampleMountinfoBase + ` 21 20 0:53 / /mnt/test - tmpfs rw 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, + &vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, {"FS type", sampleMountinfoBase + ` -21 20 0:53 / /mnt/test rw,relatime - rw -21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`, - vfs.ErrMountInfoEmpty, "", nil, nil, nil}, +21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +21 20 0:53 / /mnt/test rw,relatime - rw`, + &vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil}, {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), @@ -266,9 +307,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s }) } else if tc.wantNode != nil || tc.wantCollectF != nil { panic("invalid test case") - } else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) { + } else if _, err := d.Unfold("/"); !reflect.DeepEqual(err, tc.wantErr) { if tc.wantError == "" { - t.Errorf("Unfold: error = %v, wantErr %v", + t.Errorf("Unfold: error = %#v, wantErr %#v", err, tc.wantErr) } else if err != nil && err.Error() != tc.wantError { t.Errorf("Unfold: error = %q, wantError %q", @@ -276,9 +317,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s } } - if err := gotErr(); !errors.Is(err, tc.wantErr) { + if err := gotErr(); !reflect.DeepEqual(err, tc.wantErr) { if tc.wantError == "" { - t.Errorf("%s: error = %v, wantErr %v", + t.Errorf("%s: error = %#v, wantErr %#v", funcName, err, tc.wantErr) } else if err != nil && err.Error() != tc.wantError { t.Errorf("%s: error = %q, wantError %q", diff --git a/container/vfs/unfold.go b/container/vfs/unfold.go index ca0e37b..d63f0a9 100644 --- a/container/vfs/unfold.go +++ b/container/vfs/unfold.go @@ -4,9 +4,14 @@ import ( "iter" "path" "strings" - "syscall" ) +type UnfoldTargetError string + +func (e UnfoldTargetError) Error() string { + return "mount point " + string(e) + " never appeared in mountinfo" +} + // MountInfoNode positions a [MountInfoEntry] in its mount hierarchy. type MountInfoNode struct { *MountInfoEntry @@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) { } if targetIndex == -1 { - return nil, syscall.ESTALE + // target does not exist in parsed mountinfo + return nil, &DecoderError{Op: "unfold", Line: -1, Err: UnfoldTargetError(targetClean)} } for _, cur := range mountinfo { diff --git a/container/vfs/unfold_test.go b/container/vfs/unfold_test.go index cd6ace1..e04c51a 100644 --- a/container/vfs/unfold_test.go +++ b/container/vfs/unfold_test.go @@ -1,11 +1,9 @@ package vfs_test import ( - "errors" "reflect" "slices" "strings" - "syscall" "testing" "hakurei.app/container/vfs" @@ -26,7 +24,7 @@ func TestUnfold(t *testing.T) { "no match", sampleMountinfoBase, "/mnt", - syscall.ESTALE, nil, nil, nil, + &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil, }, { "cover", @@ -55,7 +53,7 @@ func TestUnfold(t *testing.T) { d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) got, err := d.Unfold(tc.target) - if !errors.Is(err, tc.wantErr) { + if !reflect.DeepEqual(err, tc.wantErr) { t.Errorf("Unfold: error = %v, wantErr %v", err, tc.wantErr) }