From 01d397a658f2f6834b9550a129dce15af489883c Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 1 Dec 2023 11:49:29 +0100 Subject: [PATCH] podman: new option --preserve-fd add a new option --preserve-fd that allows to specify a list of FDs to pass down to the container. It is similar to --preserve-fds but it allows to specify a list of FDs instead of the maximum FD number to preserve. --preserve-fd and --preserve-fds are mutually exclusive. It requires crun since runc would complain if any fd below --preserve-fds is not preserved. Closes: https://github.com/containers/podman/issues/20844 Signed-off-by: Giuseppe Scrivano --- cmd/podman/containers/exec.go | 10 ++++ cmd/podman/containers/run.go | 15 +++++- docs/source/markdown/options/preserve-fd.md | 10 ++++ docs/source/markdown/podman-exec.1.md.in | 2 + docs/source/markdown/podman-run.1.md.in | 2 + libpod/container_config.go | 3 ++ libpod/container_exec.go | 4 ++ libpod/oci.go | 3 ++ libpod/oci_conmon_common.go | 51 +++++++++++++++++---- libpod/oci_conmon_exec_common.go | 20 ++++---- libpod/options.go | 12 +++++ pkg/domain/entities/containers.go | 2 + pkg/domain/entities/pods.go | 1 + pkg/domain/infra/abi/containers.go | 2 + pkg/specgen/generate/container_create.go | 4 ++ pkg/specgen/specgen.go | 5 ++ pkg/specgenutil/specgen.go | 8 ++++ test/system/030-run.bats | 19 ++++++++ test/system/075-exec.bats | 22 +++++++++ 19 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 docs/source/markdown/options/preserve-fd.md diff --git a/cmd/podman/containers/exec.go b/cmd/podman/containers/exec.go index 36be01479a..e8eb557bd0 100644 --- a/cmd/podman/containers/exec.go +++ b/cmd/podman/containers/exec.go @@ -83,6 +83,10 @@ func execFlags(cmd *cobra.Command) { flags.UintVar(&execOpts.PreserveFDs, preserveFdsFlagName, 0, "Pass N additional file descriptors to the container") _ = cmd.RegisterFlagCompletionFunc(preserveFdsFlagName, completion.AutocompleteNone) + preserveFdFlagName := "preserve-fd" + flags.UintSliceVar(&execOpts.PreserveFD, preserveFdFlagName, nil, "Pass a list of additional file descriptors to the container") + _ = cmd.RegisterFlagCompletionFunc(preserveFdFlagName, completion.AutocompleteNone) + workdirFlagName := "workdir" flags.StringVarP(&execOpts.WorkDir, workdirFlagName, "w", "", "Working directory inside the container") _ = cmd.RegisterFlagCompletionFunc(workdirFlagName, completion.AutocompleteDefault) @@ -139,6 +143,12 @@ func exec(cmd *cobra.Command, args []string) error { execOpts.Envs = envLib.Join(execOpts.Envs, cliEnv) + for _, fd := range execOpts.PreserveFD { + if !rootless.IsFdInherited(int(fd)) { + return fmt.Errorf("file descriptor %d is not available - the preserve-fd option requires that file descriptors must be passed", fd) + } + } + for fd := 3; fd < int(3+execOpts.PreserveFDs); fd++ { if !rootless.IsFdInherited(fd) { return fmt.Errorf("file descriptor %d is not available - the preserve-fds option requires that file descriptors must be passed", fd) diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index 09c9df8742..3ce6ada180 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -67,9 +67,13 @@ func runFlags(cmd *cobra.Command) { flags.BoolVar(&runRmi, "rmi", false, "Remove image unless used by other containers, implies --rm") preserveFdsFlagName := "preserve-fds" - flags.UintVar(&runOpts.PreserveFDs, "preserve-fds", 0, "Pass a number of additional file descriptors into the container") + flags.UintVar(&runOpts.PreserveFDs, preserveFdsFlagName, 0, "Pass a number of additional file descriptors into the container") _ = cmd.RegisterFlagCompletionFunc(preserveFdsFlagName, completion.AutocompleteNone) + preserveFdFlagName := "preserve-fd" + flags.UintSliceVar(&runOpts.PreserveFD, preserveFdFlagName, nil, "Pass a file descriptor into the container") + _ = cmd.RegisterFlagCompletionFunc(preserveFdFlagName, completion.AutocompleteNone) + flags.BoolVarP(&runOpts.Detach, "detach", "d", false, "Run container in background and print container ID") detachKeysFlagName := "detach-keys" @@ -85,7 +89,8 @@ func runFlags(cmd *cobra.Command) { flags.BoolVar(&runOpts.Passwd, passwdFlagName, true, "add entries to /etc/passwd and /etc/group") if registry.IsRemote() { - _ = flags.MarkHidden("preserve-fds") + _ = flags.MarkHidden(preserveFdsFlagName) + _ = flags.MarkHidden(preserveFdFlagName) _ = flags.MarkHidden("conmon-pidfile") _ = flags.MarkHidden("pidfile") } @@ -135,6 +140,11 @@ func run(cmd *cobra.Command, args []string) error { return err } + for _, fd := range runOpts.PreserveFD { + if !rootless.IsFdInherited(int(fd)) { + return fmt.Errorf("file descriptor %d is not available - the preserve-fd option requires that file descriptors must be passed", fd) + } + } for fd := 3; fd < int(3+runOpts.PreserveFDs); fd++ { if !rootless.IsFdInherited(fd) { return fmt.Errorf("file descriptor %d is not available - the preserve-fds option requires that file descriptors must be passed", fd) @@ -196,6 +206,7 @@ func run(cmd *cobra.Command, args []string) error { } cliVals.PreserveFDs = runOpts.PreserveFDs + cliVals.PreserveFD = runOpts.PreserveFD s := specgen.NewSpecGenerator(imageName, cliVals.RootFS) if err := specgenutil.FillOutSpecGen(s, &cliVals, args); err != nil { return err diff --git a/docs/source/markdown/options/preserve-fd.md b/docs/source/markdown/options/preserve-fd.md new file mode 100644 index 0000000000..42530dcfe4 --- /dev/null +++ b/docs/source/markdown/options/preserve-fd.md @@ -0,0 +1,10 @@ +####> This option file is used in: +####> podman exec, run +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--preserve-fd**=*FD1[,FD2,...]* + +Pass down to the process the additional file descriptors specified in the comma separated list. It can be specified multiple times. +This option is only supported with the crun OCI runtime. It might be a security risk to use this option with other OCI runtimes. + +(This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines) diff --git a/docs/source/markdown/podman-exec.1.md.in b/docs/source/markdown/podman-exec.1.md.in index cf54d2d225..d1cdb7a905 100644 --- a/docs/source/markdown/podman-exec.1.md.in +++ b/docs/source/markdown/podman-exec.1.md.in @@ -27,6 +27,8 @@ Start the exec session, but do not attach to it. The command runs in the backgro @@option latest +@@option preserve-fd + @@option preserve-fds @@option privileged diff --git a/docs/source/markdown/podman-run.1.md.in b/docs/source/markdown/podman-run.1.md.in index 76e5ea5351..7132673861 100644 --- a/docs/source/markdown/podman-run.1.md.in +++ b/docs/source/markdown/podman-run.1.md.in @@ -308,6 +308,8 @@ This is used to override the Podman provided user setup in favor of entrypoint c @@option pod-id-file.container +@@option preserve-fd + @@option preserve-fds @@option privileged diff --git a/libpod/container_config.go b/libpod/container_config.go index aefce67219..7df0c2c396 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -416,6 +416,9 @@ type ContainerMiscConfig struct { // to 0, 1, 2) that will be passed to the executed process. The total FDs // passed will be 3 + PreserveFDs. PreserveFDs uint `json:"preserveFds,omitempty"` + // PreserveFD is a list of additional file descriptors (in addition + // to 0, 1, 2) that will be passed to the executed process. + PreserveFD []uint `json:"preserveFd,omitempty"` // Timezone is the timezone inside the container. // Local means it has the same timezone as the host machine Timezone string `json:"timezone,omitempty"` diff --git a/libpod/container_exec.go b/libpod/container_exec.go index f04f369d60..babfca09e8 100644 --- a/libpod/container_exec.go +++ b/libpod/container_exec.go @@ -66,6 +66,9 @@ type ExecConfig struct { // given is the number that will be passed into the exec session, // starting at 3. PreserveFDs uint `json:"preserveFds,omitempty"` + // PreserveFD is a list of additional file descriptors (in addition + // to 0, 1, 2) that will be passed to the executed process. + PreserveFD []uint `json:"preserveFd,omitempty"` // ExitCommand is the exec session's exit command. // This command will be executed when the exec session exits. // If unset, no command will be executed. @@ -1092,6 +1095,7 @@ func prepareForExec(c *Container, session *ExecSession) (*ExecOptions, error) { opts.Cwd = session.Config.WorkDir opts.User = session.Config.User opts.PreserveFDs = session.Config.PreserveFDs + opts.PreserveFD = session.Config.PreserveFD opts.DetachKeys = session.Config.DetachKeys opts.ExitCommand = session.Config.ExitCommand opts.ExitCommandDelay = session.Config.ExitCommandDelay diff --git a/libpod/oci.go b/libpod/oci.go index 8b55e58adc..59264dfe7d 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -202,6 +202,9 @@ type ExecOptions struct { // to 0, 1, 2) that will be passed to the executed process. The total FDs // passed will be 3 + PreserveFDs. PreserveFDs uint + // PreserveFD is a list of additional file descriptors (in addition + // to 0, 1, 2) that will be passed to the executed process. + PreserveFD []uint // DetachKeys is a set of keys that, when pressed in sequence, will // detach from the container. // If not provided, the default keys will be used. diff --git a/libpod/oci_conmon_common.go b/libpod/oci_conmon_common.go index a8514a622b..e40c843833 100644 --- a/libpod/oci_conmon_common.go +++ b/libpod/oci_conmon_common.go @@ -1038,6 +1038,39 @@ func (r *ConmonOCIRuntime) getLogTag(ctr *Container) (string, error) { return b.String(), nil } +func getPreserveFdExtraFiles(preserveFD []uint, preserveFDs uint) (uint, []*os.File, []*os.File, error) { + var filesToClose []*os.File + var extraFiles []*os.File + + preserveFDsMap := make(map[uint]struct{}) + for _, i := range preserveFD { + if i < 3 { + return 0, nil, nil, fmt.Errorf("cannot preserve FD %d, consider using the passthrough log-driver to pass STDIO streams into the container: %w", i, define.ErrInvalidArg) + } + if i-2 > preserveFDs { + // preserveFDs is the number of FDs above 2 to keep around. + // e.g. if the user specified FD=3, then preserveFDs must be 1. + preserveFDs = i - 2 + } + preserveFDsMap[i] = struct{}{} + } + + if preserveFDs > 0 { + for fd := 3; fd < int(3+preserveFDs); fd++ { + if len(preserveFDsMap) > 0 { + if _, ok := preserveFDsMap[uint(fd)]; !ok { + extraFiles = append(extraFiles, nil) + continue + } + } + f := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)) + filesToClose = append(filesToClose, f) + extraFiles = append(extraFiles, f) + } + } + return preserveFDs, filesToClose, extraFiles, nil +} + // createOCIContainer generates this container's main conmon instance and prepares it for starting func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *ContainerCheckpointOptions) (int64, error) { var stderrBuf bytes.Buffer @@ -1114,10 +1147,11 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co args = append(args, []string{"--exit-command-arg", arg}...) } - // Pass down the LISTEN_* environment (see #10443). preserveFDs := ctr.config.PreserveFDs + + // Pass down the LISTEN_* environment (see #10443). if val := os.Getenv("LISTEN_FDS"); val != "" { - if ctr.config.PreserveFDs > 0 { + if preserveFDs > 0 || len(ctr.config.PreserveFD) > 0 { logrus.Warnf("Ignoring LISTEN_FDS to preserve custom user-specified FDs") } else { fds, err := strconv.Atoi(val) @@ -1128,6 +1162,10 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co } } + preserveFDs, filesToClose, extraFiles, err := getPreserveFdExtraFiles(ctr.config.PreserveFD, preserveFDs) + if err != nil { + return 0, err + } if preserveFDs > 0 { args = append(args, formatRuntimeOpts("--preserve-fds", strconv.FormatUint(uint64(preserveFDs), 10))...) } @@ -1189,14 +1227,7 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co return 0, fmt.Errorf("configuring conmon env: %w", err) } - var filesToClose []*os.File - if preserveFDs > 0 { - for fd := 3; fd < int(3+preserveFDs); fd++ { - f := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)) - filesToClose = append(filesToClose, f) - cmd.ExtraFiles = append(cmd.ExtraFiles, f) - } - } + cmd.ExtraFiles = extraFiles cmd.Env = r.conmonEnv // we don't want to step on users fds they asked to preserve diff --git a/libpod/oci_conmon_exec_common.go b/libpod/oci_conmon_exec_common.go index b44dbe0efa..ec44b21554 100644 --- a/libpod/oci_conmon_exec_common.go +++ b/libpod/oci_conmon_exec_common.go @@ -391,8 +391,13 @@ func (r *ConmonOCIRuntime) startExec(c *Container, sessionID string, options *Ex args := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), ociLog, define.NoLogging, c.config.LogTag) - if options.PreserveFDs > 0 { - args = append(args, formatRuntimeOpts("--preserve-fds", strconv.FormatUint(uint64(options.PreserveFDs), 10))...) + preserveFDs, filesToClose, extraFiles, err := getPreserveFdExtraFiles(options.PreserveFD, options.PreserveFDs) + if err != nil { + return nil, nil, err + } + + if preserveFDs > 0 { + args = append(args, formatRuntimeOpts("--preserve-fds", strconv.FormatUint(uint64(preserveFDs), 10))...) } if options.Terminal { @@ -442,19 +447,12 @@ func (r *ConmonOCIRuntime) startExec(c *Container, sessionID string, options *Ex return nil, nil, fmt.Errorf("configuring conmon env: %w", err) } - var filesToClose []*os.File - if options.PreserveFDs > 0 { - for fd := 3; fd < int(3+options.PreserveFDs); fd++ { - f := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd)) - filesToClose = append(filesToClose, f) - execCmd.ExtraFiles = append(execCmd.ExtraFiles, f) - } - } + execCmd.ExtraFiles = extraFiles // we don't want to step on users fds they asked to preserve // Since 0-2 are used for stdio, start the fds we pass in at preserveFDs+3 execCmd.Env = r.conmonEnv - execCmd.Env = append(execCmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", options.PreserveFDs+3), fmt.Sprintf("_OCI_STARTPIPE=%d", options.PreserveFDs+4), fmt.Sprintf("_OCI_ATTACHPIPE=%d", options.PreserveFDs+5)) + execCmd.Env = append(execCmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", preserveFDs+3), fmt.Sprintf("_OCI_STARTPIPE=%d", preserveFDs+4), fmt.Sprintf("_OCI_ATTACHPIPE=%d", preserveFDs+5)) execCmd.Env = append(execCmd.Env, conmonEnv...) execCmd.ExtraFiles = append(execCmd.ExtraFiles, childSyncPipe, childStartPipe, childAttachPipe) diff --git a/libpod/options.go b/libpod/options.go index 83f8afba08..09a833a4a7 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1555,6 +1555,18 @@ func WithPreserveFDs(fd uint) CtrCreateOption { } } +// WithPreserveFD forwards from the process running Libpod into the container +// the given list of extra FDs to the created container +func WithPreserveFD(fds []uint) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + ctr.config.PreserveFD = fds + return nil + } +} + // WithCreateCommand adds the full command plus arguments of the current // process to the container config. func WithCreateCommand(cmd []string) CtrCreateOption { diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index a47b9ed20c..1b64600082 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -297,6 +297,7 @@ type ExecOptions struct { Interactive bool Latest bool PreserveFDs uint + PreserveFD []uint Privileged bool Tty bool User string @@ -360,6 +361,7 @@ type ContainerRunOptions struct { InputStream *os.File OutputStream *os.File PreserveFDs uint + PreserveFD []uint Rm bool SigProxy bool Spec *specgen.SpecGenerator diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 0baff93a66..d57dedee40 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -250,6 +250,7 @@ type ContainerCreateOptions struct { PodIDFile string Personality string PreserveFDs uint + PreserveFD []uint Privileged bool PublishAll bool Pull string diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index e8cb8e346e..1a6d044656 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -809,6 +809,7 @@ func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime) (*libpod.E execConfig.WorkDir = options.WorkDir execConfig.DetachKeys = &options.DetachKeys execConfig.PreserveFDs = options.PreserveFDs + execConfig.PreserveFD = options.PreserveFD execConfig.AttachStdin = options.Interactive // Make an exit command @@ -858,6 +859,7 @@ func (ic *ContainerEngine) ContainerExec(ctx context.Context, nameOrID string, o if err != nil { return ec, err } + containers, err := getContainers(ic.Libpod, getContainersOptions{latest: options.Latest, names: []string{nameOrID}}) if err != nil { return ec, err diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 940fefae08..ea5fa1e7c3 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -355,6 +355,10 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l options = append(options, libpod.WithPreserveFDs(s.PreserveFDs)) } + if s.PreserveFD != nil { + options = append(options, libpod.WithPreserveFD(s.PreserveFD)) + } + if s.Stdin { options = append(options, libpod.WithStdin()) } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 9568ddc048..80b6899902 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -180,6 +180,11 @@ type ContainerBasicConfig struct { // set tags as `json:"-"` for not supported remote // Optional. PreserveFDs uint `json:"-"` + // PreserveFD is a list of additional file descriptors (in addition + // to 0, 1, 2) that will be passed to the executed process. + // set tags as `json:"-"` for not supported remote + // Optional. + PreserveFD []uint `json:"-"` // Timezone is the timezone inside the container. // Local means it has the same timezone as the host machine // Optional. diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index c15c560314..306a19b394 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -838,9 +838,17 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions if len(s.Name) == 0 || len(c.Name) != 0 { s.Name = c.Name } + + if c.PreserveFDs != 0 && c.PreserveFD != nil { + return errors.New("cannot specify both --preserve-fds and --preserve-fd") + } + if s.PreserveFDs == 0 || c.PreserveFDs != 0 { s.PreserveFDs = c.PreserveFDs } + if s.PreserveFD == nil || c.PreserveFD != nil { + s.PreserveFD = c.PreserveFD + } if s.OOMScoreAdj == nil || c.OOMScoreAdj != nil { s.OOMScoreAdj = c.OOMScoreAdj diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 6729a0f5a7..30949e390d 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -80,6 +80,25 @@ echo $rand | 0 | $rand is "$output" "$content" "container read input from fd 4" } +# 'run --preserve-fd' passes a list of additional file descriptors into the container +@test "podman run --preserve-fd" { + skip_if_remote "preserve-fd is meaningless over remote" + + runtime=$(podman_runtime) + if [[ $runtime != "crun" ]]; then + skip "runtime is $runtime; preserve-fd requires crun" + fi + + content=$(random_string 20) + echo "$content" > $PODMAN_TMPDIR/tempfile + + # /proc/self/fd will have 0 1 2, possibly 3 & 4, but no 2-digit fds other than 40 + run_podman run --rm -i --preserve-fd=9,40 $IMAGE sh -c '/bin/ls -C -w999 /proc/self/fd; cat <&9; cat <&40' 9<<<"fd9" 10 $PODMAN_TMPDIR/tempfile + + # /proc/self/fd will have 0 1 2, possibly 3 & 4, but no 2-digit fds other than 40 + run_podman exec --preserve-fd=9,40 $cid sh -c '/bin/ls -C -w999 /proc/self/fd; cat <&9; cat <&40' 9<<<"fd9" 10