aboutsummaryrefslogtreecommitdiffhomepage
path: root/container
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-08-29 21:51:31 +0900
committerOphestra <cat@gensokyo.uk>2025-08-29 21:51:31 +0900
commit50972096cd8bffd9b17dbdf656ec34c48edb8f86 (patch)
tree1ad00bbf65f59539aa9bd0b3e1f42b89b7ca3fcb /container
parent905b9f9785263297611a61facc8b6bdf4c3e257f (diff)
container/vfs: wrap decoder errors
This passes line information and handles strconv errors so it reads better. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container')
-rw-r--r--container/mount.go2
-rw-r--r--container/mount_test.go6
-rw-r--r--container/path_test.go2
-rw-r--r--container/vfs/mountinfo.go35
-rw-r--r--container/vfs/mountinfo_test.go75
-rw-r--r--container/vfs/unfold.go10
-rw-r--r--container/vfs/unfold_test.go6
7 files changed, 106 insertions, 30 deletions
diff --git a/container/mount.go b/container/mount.go
index fc2a16e2..6b134ce4 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 e50af318..f2f8e29b 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 c3b68b3d..7184b0bf 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 bcb3063f..5f9f2257 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}
+ }
+ if d.parseErr != nil {
+ return &DecoderError{"parse", d.curLine, d.parseErr}
}
- return 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 fa75b3c9..7d9b2406 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 ca0e37b7..d63f0a9f 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 cd6ace18..e04c51a5 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)
}