diff --git a/procfs_linux.go b/procfs_linux.go index ab417ed..33fee6f 100644 --- a/procfs_linux.go +++ b/procfs_linux.go @@ -54,6 +54,112 @@ func verifyProcRoot(procRoot *os.File) error { return nil } +var ( + hasPrivateMountsBool bool + hasPrivateMountsOnce sync.Once +) + +func hasPrivateMounts() bool { + hasPrivateMountsOnce.Do(func() { + // Just try to use open_tree to open a file without OPEN_TREE_CLONE. + // This is equivalent to openat, but checks if the new mount syscalls + // are available. + fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC) + if err == nil { + hasPrivateMountsBool = true + _ = unix.Close(fd) + } + }) + return hasPrivateMountsBool +} + +func fsopen(fsName string, flags int) (*os.File, error) { + // Make sure we always set O_CLOEXEC. + flags |= unix.FSOPEN_CLOEXEC + fd, err := unix.Fsopen(fsName, flags) + if err != nil { + return nil, os.NewSyscallError("fsopen "+fsName, err) + } + return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil +} + +func fsmount(ctx *os.File, flags, mountAttrs int) (*os.File, error) { + // Make sure we always set O_CLOEXEC. + flags |= unix.FSMOUNT_CLOEXEC + fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs) + if err != nil { + return nil, os.NewSyscallError("fsmount "+ctx.Name(), err) + } + return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil +} + +func newPrivateProcMount() (*os.File, error) { + procfsCtx, err := fsopen("proc", unix.FSOPEN_CLOEXEC) + if err != nil { + return nil, err + } + defer procfsCtx.Close() + + // Try to configure hidepid=ptraceable,subset=pid if possible, but ignore errors. + _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable") + _ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") + + // Get an actual handle. + if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil { + return nil, os.NewSyscallError("fsconfig create procfs", err) + } + return fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID) +} + +func openTree(dir *os.File, path string, flags uint) (*os.File, error) { + dirFd := -int(unix.EBADF) + if dir != nil { + dirFd = int(dir.Fd()) + } + // Make sure we always set O_CLOEXEC. + flags |= unix.OPEN_TREE_CLOEXEC + fd, err := unix.OpenTree(dirFd, path, flags) + if err != nil { + return nil, &os.PathError{Op: "open_tree", Path: path, Err: err} + } + return os.NewFile(uintptr(fd), dir.Name()+"/"+path), nil +} + +func clonePrivateProcMount() (*os.File, error) { + // Try to make a clone without using AT_RECURSIVE if we can. If this works, + // we can be sure there are no over-mounts and so if the root is valid then + // we're golden. Otherwise, we have to deal with over-mounts. + procfsHandle, err := openTree(nil, "/proc", unix.OPEN_TREE_CLONE) + if err != nil { + procfsHandle, err = openTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE) + } + if err != nil { + return nil, fmt.Errorf("creating a detached procfs clone: %w", err) + } + if err := verifyProcRoot(procfsHandle); err != nil { + _ = procfsHandle.Close() + return nil, err + } + // TODO: Add support for checking for overmounts inside our /proc clone by + // using listmounts(2) or checking stx_mnt_id from statx() when doing + // procSelfFdReadlink(). + return procfsHandle, nil +} + +func privateProcRoot() (*os.File, error) { + if !hasPrivateMounts() { + return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP) + } + // Try to create a new procfs mount from scratch if we can. This ensures we + // can get a procfs mount even if /proc is fake (for whatever reason). + procRoot, err := newPrivateProcMount() + if err != nil { + // Try to clone /proc then... + procRoot, err = clonePrivateProcMount() + } + return procRoot, err +} + var ( procRootHandle *os.File procRootError error @@ -63,15 +169,16 @@ var ( ) func doGetProcRoot() (*os.File, error) { - // TODO: Use fsopen or open_tree to get a safe handle that cannot be - // over-mounted and we can absolutely verify. - - procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) + procRoot, err := privateProcRoot() if err != nil { - return nil, err - } - if err := verifyProcRoot(procRoot); err != nil { - return nil, err + // Fall back to using a /proc handle if making a private mount failed. + procRoot, err = os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) + if err != nil { + return nil, err + } + if err := verifyProcRoot(procRoot); err != nil { + return nil, err + } } return procRoot, nil }