aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--container/container.go45
-rw-r--r--container/container_test.go39
-rw-r--r--test/sandbox/test.py2
3 files changed, 65 insertions, 21 deletions
diff --git a/container/container.go b/container/container.go
index f9e7cf12..428309c0 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 5aaa8763..49797607 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 24fd1d3a..ad38e73c 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</proc/cmdline && hakurei -v run sleep infinity")
pd_identity0_sleep_pid = int(machine.wait_until_succeeds("pgrep -U 10000 -x sleep", timeout=60))
print(machine.succeed(f"hakurei-test fd {pd_identity0_sleep_pid}"))
machine.succeed(f"kill -INT {pd_identity0_sleep_pid}")