diff --git a/container/container.go b/container/container.go index f9e7cf1..428309c 100644 --- a/container/container.go +++ b/container/container.go @@ -11,6 +11,7 @@ import ( "os/exec" "runtime" "strconv" + "sync" . "syscall" "time" @@ -143,11 +144,18 @@ func (e *StartError) Error() string { // Message returns a user-facing error message. func (e *StartError) Message() string { if e.Passthrough { + var ( + numError *strconv.NumError + ) + switch { case errors.As(e.Err, new(*os.PathError)), errors.As(e.Err, new(*os.SyscallError)): return "cannot " + e.Err.Error() + case errors.As(e.Err, &numError) && numError != nil: + return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error() + default: return e.Err.Error() } @@ -158,6 +166,39 @@ func (e *StartError) Message() string { return "cannot " + e.Error() } +// for ensureCloseOnExec +var ( + closeOnExecOnce sync.Once + closeOnExecErr error +) + +// ensureCloseOnExec ensures all currently open file descriptors have the syscall.FD_CLOEXEC flag set. +// This is only ran once as it is intended to handle files left open by the parent, and any file opened +// on this side should already have syscall.FD_CLOEXEC set. +func ensureCloseOnExec() error { + closeOnExecOnce.Do(func() { + const fdPrefixPath = "/proc/self/fd/" + + var entries []os.DirEntry + if entries, closeOnExecErr = os.ReadDir(fdPrefixPath); closeOnExecErr != nil { + return + } + + var fd int + for _, ent := range entries { + if fd, closeOnExecErr = strconv.Atoi(ent.Name()); closeOnExecErr != nil { + break // not reached + } + CloseOnExec(fd) + } + }) + + if closeOnExecErr == nil { + return nil + } + return &StartError{Fatal: true, Step: "set FD_CLOEXEC on all open files", Err: closeOnExecErr, Passthrough: true} +} + // Start starts the container init. The init process blocks until Serve is called. func (p *Container) Start() error { if p == nil || p.cmd == nil || @@ -168,6 +209,10 @@ func (p *Container) Start() error { return errors.New("container: already started") } + if err := ensureCloseOnExec(); err != nil { + return err + } + // map to overflow id to work around ownership checks if p.Uid < 1 { p.Uid = OverflowUid(p.msg) diff --git a/container/container_test.go b/container/container_test.go index 5aaa876..4979760 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -44,8 +44,7 @@ func TestStartError(t *testing.T) { Fatal: true, Step: "set up params stream", Err: container.ErrReceiveEnv, - }, - "set up params stream: environment variable not set", + }, "set up params stream: environment variable not set", container.ErrReceiveEnv, syscall.EBADF, "cannot set up params stream: environment variable not set"}, @@ -53,8 +52,7 @@ func TestStartError(t *testing.T) { Fatal: true, Step: "set up params stream", Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF}, - }, - "set up params stream pipe2: bad file descriptor", + }, "set up params stream pipe2: bad file descriptor", syscall.EBADF, os.ErrInvalid, "cannot set up params stream pipe2: bad file descriptor"}, @@ -62,16 +60,14 @@ func TestStartError(t *testing.T) { Fatal: true, Step: "prctl(PR_SET_NO_NEW_PRIVS)", Err: syscall.EPERM, - }, - "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted", + }, "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted", syscall.EPERM, syscall.EACCES, "cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"}, {"landlock abi", &container.StartError{ Step: "get landlock ABI", Err: syscall.ENOSYS, - }, - "get landlock ABI: function not implemented", + }, "get landlock ABI: function not implemented", syscall.ENOSYS, syscall.ENOEXEC, "cannot get landlock ABI: function not implemented"}, @@ -79,8 +75,7 @@ func TestStartError(t *testing.T) { Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", Err: syscall.ENOSYS, Origin: true, - }, - "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", + }, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", syscall.ENOSYS, syscall.ENOSPC, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"}, @@ -88,8 +83,7 @@ func TestStartError(t *testing.T) { Fatal: true, Step: "create landlock ruleset", Err: syscall.EBADFD, - }, - "create landlock ruleset: file descriptor in bad state", + }, "create landlock ruleset: file descriptor in bad state", syscall.EBADFD, syscall.EBADF, "cannot create landlock ruleset: file descriptor in bad state"}, @@ -97,8 +91,7 @@ func TestStartError(t *testing.T) { Fatal: true, Step: "enforce landlock ruleset", Err: syscall.ENOTRECOVERABLE, - }, - "enforce landlock ruleset: state not recoverable", + }, "enforce landlock ruleset: state not recoverable", syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT, "cannot enforce landlock ruleset: state not recoverable"}, @@ -109,8 +102,7 @@ func TestStartError(t *testing.T) { Path: "/proc/nonexistent", Err: syscall.ENOENT, }, Passthrough: true, - }, - "fork/exec /proc/nonexistent: no such file or directory", + }, "fork/exec /proc/nonexistent: no such file or directory", syscall.ENOENT, syscall.ENOSYS, "cannot fork/exec /proc/nonexistent: no such file or directory"}, @@ -120,11 +112,19 @@ func TestStartError(t *testing.T) { Syscall: "open", Err: syscall.ENOSYS, }, Passthrough: true, - }, - "open: function not implemented", + }, "open: function not implemented", syscall.ENOSYS, syscall.ENOENT, "cannot open: function not implemented"}, + {"start FD_CLOEXEC", &container.StartError{ + Fatal: true, + Step: "set FD_CLOEXEC on all open files", + Err: func() error { _, err := strconv.Atoi("invalid"); return err }(), + Passthrough: true, + }, `strconv.Atoi: parsing "invalid": invalid syntax`, + strconv.ErrSyntax, os.ErrInvalid, + `cannot parse "invalid": invalid syntax`}, + {"start other", &container.StartError{ Step: "start container init", Err: &net.OpError{ @@ -132,8 +132,7 @@ func TestStartError(t *testing.T) { Net: "unix", Err: syscall.ECONNREFUSED, }, Passthrough: true, - }, - "dial unix: connection refused", + }, "dial unix: connection refused", syscall.ECONNREFUSED, syscall.ECONNABORTED, "dial unix: connection refused"}, } diff --git a/test/sandbox/test.py b/test/sandbox/test.py index 24fd1d3..ad38e73 100644 --- a/test/sandbox/test.py +++ b/test/sandbox/test.py @@ -46,7 +46,7 @@ swaymsg("exec hakurei run cat") check_filter(0, "pdlike", "cat") # Check fd leak: -swaymsg("exec hakurei -v run sleep infinity") +swaymsg("exec exec 127