Skip to content

Fix --preserve-fds validation logic. - #5475

Open
mgelde wants to merge 1 commit into
opencontainers:mainfrom
mgelde:5474-fix-preserve-fds-validation
Open

mgelde wants to merge 1 commit into
opencontainers:mainfrom
mgelde:5474-fix-preserve-fds-validation

Conversation

@mgelde

@mgelde mgelde commented Sep 15, 2026

Copy link
Copy Markdown

The --preserve-fds option can be used to preserve file descriptors and pass them into the container. There is a check that should verify if those file-descriptors are actually open, but its results are tainted by file descriptors opened by Go code after execve and before the check runs. This is also mentioned in #5474.

Currently, running runc with --preserve-fds set to 2 whilst not actually passing any descriptors reproducibly leaks runc-internal file descriptors into the container. Instead, an error message would be more helpful in this situation.

This patch moves the validation logic to a point before any file descriptors related to cgroups-handling or similar are opened by runc.

Closes: #5474

@mgelde
mgelde force-pushed the 5474-fix-preserve-fds-validation branch from 37c7d9b to 1b8e363 Compare September 15, 2026 14:00
Comment thread utils_linux.go
defer closer()
defer procSelfFd.Close()
for i := baseFd; i < baseFd+r.preserveFDs; i++ {
err := unix.Faccessat(int(procSelfFd.Fd()), strconv.Itoa(i), unix.F_OK, 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Go runtime and runc itself opens all FDs with CLOEXEC, we can just replace unix.Faccessat to unix.Fctnl(F_GETFD) and check there's no CLOEXEC flag, and error out otherwise (basically the check you added to checkPreserveFDs can be done right here).

This will simplify the logic a lot (no cli parsing, no NumFiles etc) without any downsides.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, the fix itself (modulo tests) can be as simple as this:

--- a/utils_linux.go
+++ b/utils_linux.go
@@ -15,7 +15,6 @@ import (
      "github.com/urfave/cli/v3"
      "golang.org/x/sys/unix"

-     "github.com/opencontainers/runc/internal/pathrs"
      "github.com/opencontainers/runc/internal/third_party/systemd/activation"
      "github.com/opencontainers/runc/libcontainer"
      "github.com/opencontainers/runc/libcontainer/configs"
@@ -245,16 +244,15 @@ func (r *runner) run(config *specs.Process) (_ int, retErr error) {
              process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
      }
      baseFd := 3 + len(process.ExtraFiles)
-     procSelfFd, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC)
-     if err != nil {
-             return -1, err
-     }
-     defer closer()
-     defer procSelfFd.Close()
      for i := baseFd; i < baseFd+r.preserveFDs; i++ {
-             err := unix.Faccessat(int(procSelfFd.Fd()), strconv.Itoa(i), unix.F_OK, 0)
-             if err != nil {
-                     return -1, fmt.Errorf("unable to stat preserved-fd %d (of %d): %w", i-baseFd, r.preserveFDs, err)
+             // Check that the fd was really inherited from runc's caller. Merely
+             // checking that the fd is open is not sufficient, as the fd number
+             // could have been reused by runc itself (or the Go runtime). Any such
+             // fd has the close-on-exec flag set, while an inherited one can not
+             // have it, as it would have been closed by execve.
+             flags, err := unix.FcntlInt(uintptr(i), unix.F_GETFD, 0)
+             if err != nil || flags&unix.FD_CLOEXEC != 0 {
+                     return -1, fmt.Errorf("preserved-fd %d (of %d) was not passed to runc", i-baseFd, r.preserveFDs)
              }
              process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
      }

(note I simplified the error message as well)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no way that should fail, as long as all those FDs really always set CLOEXEC; now and forever AND this check continues to happen both before execve AND before joining the container namespaces e.g. during runc run --preserve-fds=X (otherwise we'd have a race). At present that appears to be the case.

So if that is how it's done (i.e. CLOEXEC is always set), I agree the extra safety of checking early does not outweigh the added complexity.

I'll push an updated version.

Comment thread utils_linux.go Outdated
for i := baseFd; i < baseFd+preserveFDs; i++ {
flags, err := unix.FcntlInt(uintptr(i), unix.F_GETFD, 0)
if err != nil {
return fmt.Errorf("unable to stat preserved-fd %d (of %d): %w", i-baseFd, preserveFDs, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is fcntl not stat

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed that as well.

@mgelde
mgelde force-pushed the 5474-fix-preserve-fds-validation branch from 1b8e363 to 8bc15df Compare September 16, 2026 08:29

@kolyshkin kolyshkin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like integration tests fail. Also, can you add Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>?

The --preserve-fds option can be used to preserve file descriptors and
pass them into the container. There is a check that should verify if
those file-descriptors are actually open, but its results are tainted by
file descriptors opened by Go code after execve and before the check
runs.

Currently, running runc with --preserve-fds set to 2 whilst not actually
passing any descriptors reproducibly leaks runc-internal file
descriptors into the container. Instead, an error message would be more
helpful in this situation.

This patch moves the validation logic to a point before any file
descriptors related to cgroups-handling or similar are opened by runc.

Closes: opencontainers#5474
Signed-off-by: Marcus Gelderie <Marcus.Gelderie@hs-aalen.de>
Co-authored-by: Kir Kolyshkin <kolyshkin@gmail.com>
@mgelde
mgelde force-pushed the 5474-fix-preserve-fds-validation branch from 8bc15df to ae6631b Compare September 18, 2026 07:07
@mgelde

mgelde commented Sep 18, 2026

Copy link
Copy Markdown
Author

Yes, I somehow cannot get the integration tests to run locally. They always fail, even on main. Even in a VM with another Linux distribution I could not get them to run. So I hope what I gather from the runner logs here will suffice.

From what I can see in the runner results, the failures are due to the changed position of the check:

  • The log exists after the failed run (test checked for absence). The check now occurs later than previously and the log file is now created before the check.
  • and the create test fails because another check preempts ours and terminates the call with another error message than what the test expected. At least what I was able to reproduce locally points that way.

I have pushed another version in the hopes that fixes it. For now I cannot spend more time on getting those tests to run locally. If this patch does not resolve the issue, I'll revisit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--preserve-fds leaks runc's own file descriptors when fewer FDs are actually passed

2 participants