podman top: join the container userns

When we execute ps(1) in the container and the container uses a userns
with a different id mapping the user id field will be wrong.

To fix this we must join the userns in such case.

Fixes #22293

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2024-06-25 14:41:30 +02:00
parent 5e27243935
commit 65ed96585d
No known key found for this signature in database
GPG Key ID: EB145DD938A3CAF2
3 changed files with 56 additions and 6 deletions

View File

@ -3,6 +3,7 @@
#define _GNU_SOURCE
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>
@ -11,6 +12,7 @@
/* keep special_exit_code in sync with container_top_linux.go */
int special_exit_code = 255;
int join_userns = 0;
char **argv = NULL;
void
@ -33,6 +35,12 @@ set_argv (int pos, char *arg)
argv[pos] = arg;
}
void
set_userns ()
{
join_userns = 1;
}
/*
We use cgo code here so we can fork then exec separately,
this is done so we can mount proc after the fork because the pid namespace is
@ -64,6 +72,23 @@ fork_exec_ps ()
fprintf (stderr, "mount proc: %m");
exit (special_exit_code);
}
if (join_userns)
{
// join the userns to make sure uid mapping match
// we are already part of the pidns so so pid 1 is the main container process
r = open ("/proc/1/ns/user", O_CLOEXEC | O_RDONLY);
if (r < 0)
{
fprintf (stderr, "open /proc/1/ns/user: %m");
exit (special_exit_code);
}
if ((status = setns (r, CLONE_NEWUSER)) < 0)
{
fprintf (stderr, "setns NEWUSER: %m");
exit (special_exit_code);
}
}
/* use execve to unset all env vars, we do not want to leak anything into the container */
execve (argv[0], argv, NULL);
fprintf (stderr, "execve: %m");

View File

@ -31,6 +31,7 @@ import (
void fork_exec_ps();
void create_argv(int len);
void set_argv(int pos, char *arg);
void set_userns();
*/
import "C"
@ -56,13 +57,13 @@ func podmanTopMain() {
os.Exit(0)
}
// podmanTopInner os.Args = {command name} {pid} {psPath} [args...]
// podmanTopInner os.Args = {command name} {pid} {userns(1/0)} {psPath} [args...]
// We are rexxec'd in a new mountns, then we need to set some security settings in order
// to safely execute ps in the container pid namespace. Most notably make sure podman and
// ps are read only to prevent a process from overwriting it.
func podmanTopInner() error {
if len(os.Args) < 3 {
return fmt.Errorf("internal error, need at least two arguments")
if len(os.Args) < 4 {
return fmt.Errorf("internal error, need at least three arguments")
}
// We have to lock the thread as we a) switch namespace below and b) use PR_SET_PDEATHSIG
@ -84,7 +85,7 @@ func podmanTopInner() error {
return fmt.Errorf("make / mount private: %w", err)
}
psPath := os.Args[2]
psPath := os.Args[3]
// try to mount everything read only
if err := unix.MountSetattr(0, "/", unix.AT_RECURSIVE, &unix.MountAttr{
@ -122,8 +123,13 @@ func podmanTopInner() error {
}
pidFD.Close()
userns := os.Args[2]
if userns == "1" {
C.set_userns()
}
args := []string{psPath}
args = append(args, os.Args[3:]...)
args = append(args, os.Args[4:]...)
C.create_argv(C.int(len(args)))
for i, arg := range args {
@ -317,7 +323,14 @@ func (c *Container) execPS(psArgs []string) ([]string, bool, error) {
wPipe.Close()
return nil, true, err
}
args := append([]string{podmanTopCommand, strconv.Itoa(c.state.PID), psPath}, psArgs...)
// see podmanTopInner()
userns := "0"
if len(c.config.IDMappings.UIDMap) > 0 {
userns = "1"
}
args := append([]string{podmanTopCommand, strconv.Itoa(c.state.PID), userns, psPath}, psArgs...)
cmd := reexec.Command(args...)
cmd.SysProcAttr = &syscall.SysProcAttr{

View File

@ -119,6 +119,18 @@ var _ = Describe("Podman top", func() {
exec := podmanTest.Podman([]string{"top", session.OutputToString(), "aux"})
exec.WaitWithDefaultTimeout()
Expect(exec).Should(ExitWithError(125, "OCI runtime attempted to invoke a command that was not found"))
session = podmanTest.Podman([]string{"run", "-d", "--uidmap=0:1000:1000", "--user", "9", fedoraMinimal, "sleep", "inf"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
result = podmanTest.Podman([]string{"top", session.OutputToString(), "-ef", "hn"})
result.WaitWithDefaultTimeout()
Expect(result).Should(ExitCleanly())
output := result.OutputToString()
Expect(output).To(ContainSubstring("sleep inf"))
// check for https://github.com/containers/podman/issues/22293
Expect(output).To(HavePrefix("9 "), "user id of process")
})
It("podman top with comma-separated options", func() {