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 <gscrivan@redhat.com>
This commit is contained in:
Giuseppe Scrivano
2023-12-01 11:49:29 +01:00
parent 6b9221d852
commit 01d397a658
19 changed files with 172 additions and 23 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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"`

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -250,6 +250,7 @@ type ContainerCreateOptions struct {
PodIDFile string
Personality string
PreserveFDs uint
PreserveFD []uint
Privileged bool
PublishAll bool
Pull string

View File

@ -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

View File

@ -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())
}

View File

@ -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.

View File

@ -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

View File

@ -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</dev/null 40<$PODMAN_TMPDIR/tempfile
assert "${lines[0]}" !~ [123][0-9] "/proc/self/fd must not contain 10-39"
assert "${lines[1]}" = "fd9" "cat from fd 9"
assert "${lines[2]}" = "$content" "cat from fd 40"
}
@test "podman run - uidmapping has no /sys/kernel mounts" {
skip_if_cgroupsv1 "run --uidmap fails on cgroups v1 (issue 15025, wontfix)"
skip_if_rootless "cannot umount as rootless"

View File

@ -223,4 +223,26 @@ load helpers
assert "$output" = "0" ".ExecIDs must be empty"
}
# 'exec --preserve-fd' passes a list of additional file descriptors into the container
@test "podman exec --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
run_podman run -d $IMAGE top
cid="$output"
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 exec --preserve-fd=9,40 $cid sh -c '/bin/ls -C -w999 /proc/self/fd; cat <&9; cat <&40' 9<<<"fd9" 10</dev/null 40<$PODMAN_TMPDIR/tempfile
assert "${lines[0]}" !~ [123][0-9] "/proc/self/fd must not contain 10-39"
assert "${lines[1]}" = "fd9" "cat from fd 9"
assert "${lines[2]}" = "$content" "cat from fd 40"
}
# vim: filetype=sh