diff --git a/container/dispatcher.go b/container/dispatcher.go index ae697ce..c94d934 100644 --- a/container/dispatcher.go +++ b/container/dispatcher.go @@ -225,7 +225,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) { return syscall.PivotRoot(newroot, putold) } func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) { - return syscall.Mount(source, target, fstype, flags, data) + return mount(source, target, fstype, flags, data) } func (direct) unmount(target string, flags int) (err error) { return syscall.Unmount(target, flags) diff --git a/container/errors.go b/container/errors.go new file mode 100644 index 0000000..12cb3b9 --- /dev/null +++ b/container/errors.go @@ -0,0 +1,60 @@ +package container + +import ( + "errors" + "os" + "syscall" +) + +type MountError struct { + Source, Target, Fstype string + + Flags uintptr + Data string + syscall.Errno +} + +func (e *MountError) Unwrap() error { + if e.Errno == 0 { + return nil + } + return e.Errno +} + +func (e *MountError) Error() string { + if e.Flags&syscall.MS_BIND != 0 { + if e.Flags&syscall.MS_REMOUNT != 0 { + return "remount " + e.Target + ": " + e.Errno.Error() + } + return "bind " + e.Source + " on " + e.Target + ": " + e.Errno.Error() + } + + if e.Fstype != FstypeNULL { + return "mount " + e.Fstype + " on " + e.Target + ": " + e.Errno.Error() + } + + // fallback case: if this is reached, the conditions for it to occur should be handled above + return "mount " + e.Target + ": " + e.Errno.Error() +} + +// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback. +func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) { + var errno syscall.Errno + if !errors.As(err, &errno) { + return 0, &os.PathError{Op: op, Path: path, Err: err} + } + return errno, nil +} + +// mount wraps syscall.Mount for error handling. +func mount(source, target, fstype string, flags uintptr, data string) error { + err := syscall.Mount(source, target, fstype, flags, data) + if err == nil { + return nil + } + if errno, pathError := errnoFallback("mount", target, err); pathError != nil { + return pathError + } else { + return &MountError{source, target, fstype, flags, data, errno} + } +} diff --git a/container/errors_test.go b/container/errors_test.go new file mode 100644 index 0000000..3d32743 --- /dev/null +++ b/container/errors_test.go @@ -0,0 +1,109 @@ +package container + +import ( + "errors" + "os" + "reflect" + "syscall" + "testing" +) + +func TestMountError(t *testing.T) { + testCases := []struct { + name string + err error + errno syscall.Errno + want string + }{ + {"bind", &MountError{ + Source: "/host/nix/store", + Target: "/sysroot/nix/store", + Fstype: FstypeNULL, + Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REC, + Data: zeroString, + Errno: syscall.ENOSYS, + }, syscall.ENOSYS, + "bind /host/nix/store on /sysroot/nix/store: function not implemented"}, + + {"remount", &MountError{ + Source: SourceNone, + Target: "/sysroot/nix/store", + Fstype: FstypeNULL, + Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REMOUNT, + Data: zeroString, + Errno: syscall.EPERM, + }, syscall.EPERM, + "remount /sysroot/nix/store: operation not permitted"}, + + {"overlay", &MountError{ + Source: SourceOverlay, + Target: sysrootPath, + Fstype: FstypeOverlay, + Data: `lowerdir=/host/var/lib/planterette/base/debian\:f92c9052`, + Errno: syscall.EINVAL, + }, syscall.EINVAL, + "mount overlay on /sysroot: invalid argument"}, + + {"fallback", &MountError{ + Source: SourceNone, + Target: sysrootPath, + Fstype: FstypeNULL, + Errno: syscall.ENOTRECOVERABLE, + }, syscall.ENOTRECOVERABLE, + "mount /sysroot: state not recoverable"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("is", func(t *testing.T) { + if !errors.Is(tc.err, tc.errno) { + t.Errorf("Is: %#v is not %v", tc.err, tc.errno) + } + }) + t.Run("error", func(t *testing.T) { + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error: %q, want %q", got, tc.want) + } + }) + }) + } + + t.Run("zero", func(t *testing.T) { + if errors.Is(new(MountError), syscall.Errno(0)) { + t.Errorf("Is: zero MountError unexpected true") + } + }) +} + +func TestErrnoFallback(t *testing.T) { + testCases := []struct { + name string + err error + wantErrno syscall.Errno + wantPath *os.PathError + }{ + {"mount", &MountError{ + Errno: syscall.ENOTRECOVERABLE, + }, syscall.ENOTRECOVERABLE, nil}, + + {"path errno", &os.PathError{ + Err: syscall.ETIMEDOUT, + }, syscall.ETIMEDOUT, nil}, + + {"fallback", errUnique, 0, &os.PathError{ + Op: "fallback", + Path: "/proc/nonexistent", + Err: errUnique, + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + errno, err := errnoFallback(tc.name, Nonexistent, tc.err) + if errno != tc.wantErrno { + t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno) + } + if !reflect.DeepEqual(err, tc.wantPath) { + t.Errorf("errnoFallback: pathError = %#v, want %#v", err, tc.wantPath) + } + }) + } +}