diff --git a/sandbox/container_test.go b/sandbox/container_test.go index c8bdcd0..1306360 100644 --- a/sandbox/container_test.go +++ b/sandbox/container_test.go @@ -111,7 +111,7 @@ func TestContainer(t *testing.T) { mnt = append(mnt, &check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"}, &check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"}, - &check.Mntent{FSName: "rootfs", Dir: "/etc/hostname", Type: "tmpfs", Opts: "host_passthrough"}, + &check.Mntent{FSName: "rootfs", Dir: "/etc/hostname", Type: "tmpfs", Opts: "\x00"}, ) for _, name := range libPaths { mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1}) diff --git a/sandbox/mount.go b/sandbox/mount.go index bb5d613..6683aae 100644 --- a/sandbox/mount.go +++ b/sandbox/mount.go @@ -1,22 +1,102 @@ package sandbox import ( + "errors" "fmt" "os" + "path/filepath" "syscall" + + "git.gensokyo.uk/security/fortify/sandbox/vfs" ) func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error { - var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND - mf |= flags & syscall.MS_REC if eq { - msg.Verbosef("resolved %q flags %#x", target, mf) + msg.Verbosef("resolved %q flags %#x", target, flags) } else { - msg.Verbosef("resolved %q on %q flags %#x", source, target, mf) + msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) } - return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""), - fmt.Sprintf("cannot mount %q on %q:", source, target)) + if err := syscall.Mount(source, target, "", + syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot mount %q on %q:", source, target)) + } + + var targetFinal string + if v, err := filepath.EvalSymlinks(target); err != nil { + return msg.WrapErr(err, err.Error()) + } else { + targetFinal = v + if targetFinal != target { + msg.Verbosef("target resolves to %q", targetFinal) + } + } + + // final target path according to the kernel through proc + var targetKFinal string + { + var destFd int + if err := IgnoringEINTR(func() (err error) { + destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0) + return + }); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot open %q:", targetFinal)) + } + if v, err := os.Readlink(p.fd(destFd)); err != nil { + return msg.WrapErr(err, err.Error()) + } else if err = syscall.Close(destFd); err != nil { + return wrapErrSuffix(err, + fmt.Sprintf("cannot close %q:", targetFinal)) + } else { + targetKFinal = v + } + } + + mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY + return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error { + n, err := d.Unfold(targetKFinal) + if err != nil { + if errors.Is(err, syscall.ESTALE) { + return msg.WrapErr(err, + fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal)) + } + return wrapErrSuffix(err, + "cannot unfold mount hierarchy:") + } + + if err = remountWithFlags(n, mf); err != nil { + return err + } + if flags&syscall.MS_REC == 0 { + return nil + } + + for cur := range n.Collective() { + err = remountWithFlags(cur, mf) + if err != nil && !errors.Is(err, syscall.EACCES) { + return err + } + } + + return nil + }) +} + +func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { + kf, unmatched := n.Flags() + if len(unmatched) != 0 { + msg.Verbosef("unmatched vfs options: %q", unmatched) + } + + if kf&mf != mf { + return wrapErrSuffix(syscall.Mount("none", n.Clean, "", + syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf, + ""), + fmt.Sprintf("cannot remount %q:", n.Clean)) + } + return nil } func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { diff --git a/sandbox/path.go b/sandbox/path.go index 5a28bdf..78c5a99 100644 --- a/sandbox/path.go +++ b/sandbox/path.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "syscall" + + "git.gensokyo.uk/security/fortify/sandbox/vfs" ) const ( @@ -64,14 +66,29 @@ func ensureFile(name string, perm os.FileMode) error { var hostProc = newProcPats(hostPath) func newProcPats(prefix string) *procPaths { - return &procPaths{prefix, prefix + "/self", prefix + "/self/mountinfo"} + return &procPaths{prefix + "/proc", prefix + "/proc/self"} } type procPaths struct { - prefix string - self string - mountinfo string + prefix string + self string } func (p *procPaths) stdout() string { return p.self + "/fd/1" } func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) } +func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { + if r, err := os.Open(p.self + "/mountinfo"); err != nil { + return msg.WrapErr(err, err.Error()) + } else { + d := vfs.NewMountInfoDecoder(r) + err0 := f(d) + if err = r.Close(); err != nil { + return wrapErrSuffix(err, + "cannot close mountinfo:") + } else if err = d.Err(); err != nil { + return wrapErrSuffix(err, + "cannot parse mountinfo:") + } + return err0 + } +}