From a55f3885da06a32afb1c0e640795611e3efbbbda Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 25 Oct 2025 18:19:06 -0600 Subject: [PATCH 1/9] Add PeerCred and PeerCredKey structs --- pkg/ctxkey/ctxkey.go | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pkg/ctxkey/ctxkey.go diff --git a/pkg/ctxkey/ctxkey.go b/pkg/ctxkey/ctxkey.go new file mode 100644 index 0000000000000..5a49e4141cfd0 --- /dev/null +++ b/pkg/ctxkey/ctxkey.go @@ -0,0 +1,10 @@ +package ctxkey + +// PeerCredKey is used as the context key for storing peer credentials. +var PeerCredKey = &struct{}{} + +type PeerCred struct { + PID int + UID int + GID int +} From d9b0396fcb4e206fdbe066fb2a2abb7984a0388c Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 25 Oct 2025 18:20:48 -0600 Subject: [PATCH 2/9] WIP: Add function to read SO_PEERCRED flag and determine docker-cli user's UID and GID --- cmd/dockerd/daemon.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 50193d5f97ac2..bb1e03726439c 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -13,6 +13,9 @@ import ( "strings" "sync" "time" + "syscall" + "golang.org/x/sys/unix" + containerddefaults "github.com/containerd/containerd/defaults" "github.com/docker/docker/api" @@ -21,6 +24,7 @@ import ( "github.com/docker/docker/api/server/middleware" "github.com/docker/docker/api/server/router" "github.com/docker/docker/api/server/router/build" + "github.com/docker/docker/pkg/ctxkey" checkpointrouter "github.com/docker/docker/api/server/router/checkpoint" "github.com/docker/docker/api/server/router/container" distributionrouter "github.com/docker/docker/api/server/router/distribution" @@ -81,6 +85,37 @@ func NewDaemonCli() *DaemonCli { } } +func getPeerCred(c net.Conn) (*ctxkey.PeerCred, error) { + sc, ok := c.(syscall.Conn) + if !ok { + return nil, fmt.Errorf("not a syscall.Conn") + } + + raw, err := sc.SyscallConn() + if err != nil { + return nil, fmt.Errorf("SyscallConn: %w", err) + } + + var cred *ctxkey.PeerCred + var ctrlErr error + + // Control runs a function with the underlying FD. + if err := raw.Control(func(fd uintptr) { + ucred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + if err != nil { + ctrlErr = err + return + } + cred = &ctxkey.PeerCred{PID: int(ucred.Pid), UID: int(ucred.Uid), GID: int(ucred.Gid)} + }); err != nil { + return nil, fmt.Errorf("raw.Control: %w", err) + } + if ctrlErr != nil { + return nil, fmt.Errorf("getsockopt SO_PEERCRED: %w", ctrlErr) + } + return cred, nil +} + func (cli *DaemonCli) start(opts *daemonOptions) (err error) { if cli.Config, err = loadDaemonCliConfig(opts); err != nil { return err From 2dda223682cf8f0c713a792cbf186de115dac1ed Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 25 Oct 2025 18:21:14 -0600 Subject: [PATCH 3/9] Add user's UID/GID to connection context --- cmd/dockerd/daemon.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index bb1e03726439c..a39dca2f695ce 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -220,6 +220,20 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { httpServer := &http.Server{ ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + if cred, err := getPeerCred(c); err == nil && cred != nil { + logrus.WithFields(logrus.Fields{ + "pid": cred.PID, + "uid": cred.UID, + "gid": cred.GID, + "remoteAddr": c.RemoteAddr().String(), + }).Info("accepted new connection with peer credentials") + return context.WithValue(ctx, ctxkey.PeerCredKey, cred) + } else if err != nil { + logrus.WithError(err).Error("getPeerCred error") + } + return ctx + }, } apiShutdownCtx, apiShutdownCancel := context.WithCancel(context.Background()) apiShutdownDone := make(chan struct{}) From f66bfb889e289e525a1e926a69f50ff5031cf73c Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 25 Oct 2025 18:22:30 -0600 Subject: [PATCH 4/9] WIP: Add crude method for detemrining a users cgroup based on the docker cli's PID making request --- .../router/container/container_routes.go | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index b4aa0864fb4e3..59596568adb08 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -8,11 +8,14 @@ import ( "net/http" "runtime" "strconv" + "strings" + "os" "github.com/containerd/containerd/platforms" "github.com/docker/docker/api/server/httpstatus" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/ctxkey" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -474,6 +477,65 @@ func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.Respon return httputils.WriteJSON(w, http.StatusOK, resp) } +// deriveParentFromProc finds the deepest ".slice" in the caller's cgroup path +// and returns it as a systemd slice path, e.g. "user.slice/user-1000.slice". +func deriveParentFromProc(mode string, pc *ctxkey.PeerCred) (string, error) { + if pc == nil || pc.PID == 0 { + return "", fmt.Errorf("no peer credentials") + } + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) + if err != nil { + return "", fmt.Errorf("read cgroup: %w", err) + } + // On cgroup v2, there is one line like: "0::/user.slice/user-1000.slice/session-6.scope" + // On cgroup v1, systemd is "name=systemd:/user.slice/user-1000.slice/session-6.scope" + lines := strings.Split(string(data), "\n") + var path string + for _, ln := range lines { + if ln == "" { + continue + } + parts := strings.SplitN(ln, ":", 3) + if len(parts) < 3 { + continue + } + controller, cgPath := parts[1], parts[2] + if controller == "" || controller == "name=systemd" || controller == "" /* v2 */ { + path = cgPath + // prefer v2 line if present; break on first match + if strings.HasPrefix(ln, "0::") { + break + } + } + } + if path == "" { + return "", fmt.Errorf("no cgroup path found") + } + // Extract slice segments ending with ".slice" + segs := strings.Split(strings.TrimPrefix(path, "/"), "/") + var slices []string + for _, s := range segs { + if strings.HasSuffix(s, ".slice") { + slices = append(slices, s) + } + } + if len(slices) == 0 { + // Fallback to uid mapping + return fmt.Sprintf("user.slice/user-%d.slice", pc.UID), nil + } + // Build "slice path" up to the deepest slice (exclude scopes/services) + // e.g., user.slice/user-1000.slice + var b strings.Builder + for i, s := range slices { + if i > 0 { + b.WriteString("/") + } + b.WriteString(s) + } + return b.String(), nil +} + + func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := httputils.ParseForm(r); err != nil { return err From 16c404fd9bac86fb471734c748d59ca7f13513be Mon Sep 17 00:00:00 2001 From: John Robbins Date: Sat, 25 Oct 2025 18:23:17 -0600 Subject: [PATCH 5/9] WIP: Naively add the cgroup parent hostConfig option to all /containers/create requests --- .../router/container/container_routes.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 59596568adb08..6d9ac8c8b6a45 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -553,6 +553,36 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo } return err } + + if hostConfig == nil { + hostConfig = &container.HostConfig{} + logrus.Info("initialized empty HostConfig") + } + + if cred, ok := r.Context().Value(ctxkey.PeerCredKey).(*ctxkey.PeerCred); ok && cred != nil { + logrus.WithFields(logrus.Fields{ + "pid": cred.PID, + "uid": cred.UID, + "gid": cred.GID, + }).Debug("retrieved peer credentials from context") + + if parent, err := deriveParentFromProc("syetemd", cred); err == nil { + hostConfig.CgroupParent = parent + logrus.WithFields(logrus.Fields{ + "pid": cred.PID, + "cgroup_parent": parent, + }).Info("set HostConfig.CgroupParent from deriveParentFromProc") + } else { + logrus.WithError(err).WithField("pid", cred.PID).Warn("deriveParentFromProc failed") + } + } else { + logrus.WithFields(logrus.Fields{ + "hasCred": ok && cred != nil, + "alreadySet": hostConfig != nil && hostConfig.CgroupParent != "", + }).Error("create: skipping cgroup-parent injection") + } + + version := httputils.VersionFromContext(ctx) adjustCPUShares := versions.LessThan(version, "1.19") From bb15eea54a493734d919cc0d9c6a5cfbfe0374ed Mon Sep 17 00:00:00 2001 From: John Robbins Date: Mon, 27 Oct 2025 13:53:02 -0600 Subject: [PATCH 6/9] Move all cgroup adoption helpers to its own pkg --- Dockerfile | 2 +- .../router/container/container_routes.go | 67 +-------- cmd/dockerd/daemon.go | 39 +---- pkg/cgroups/cgroups.go | 135 ++++++++++++++++++ pkg/ctxkey/ctxkey.go | 10 -- 5 files changed, 142 insertions(+), 111 deletions(-) create mode 100644 pkg/cgroups/cgroups.go delete mode 100644 pkg/ctxkey/ctxkey.go diff --git a/Dockerfile b/Dockerfile index 463d5cfc1a86f..c9f8ec3c8c3bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -183,7 +183,7 @@ FROM base AS gowinres ARG GOWINRES_VERSION=v0.3.1 RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ - GOBIN=/build/ GO111MODULE=on go install "github.com/tc-hib/go-winres@${GOWINRES_VERSION}" \ + GOBIN=/build/ GO111MODULE=on GOINSECURE=proxy.golang.org go install "github.com/tc-hib/go-winres@${GOWINRES_VERSION}" \ && /build/go-winres --help # containerd diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 6d9ac8c8b6a45..788b3233b3581 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -8,14 +8,12 @@ import ( "net/http" "runtime" "strconv" - "strings" - "os" "github.com/containerd/containerd/platforms" "github.com/docker/docker/api/server/httpstatus" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/ctxkey" + "github.com/docker/docker/pkg/cgroups" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -477,65 +475,6 @@ func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.Respon return httputils.WriteJSON(w, http.StatusOK, resp) } -// deriveParentFromProc finds the deepest ".slice" in the caller's cgroup path -// and returns it as a systemd slice path, e.g. "user.slice/user-1000.slice". -func deriveParentFromProc(mode string, pc *ctxkey.PeerCred) (string, error) { - if pc == nil || pc.PID == 0 { - return "", fmt.Errorf("no peer credentials") - } - data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) - if err != nil { - return "", fmt.Errorf("read cgroup: %w", err) - } - // On cgroup v2, there is one line like: "0::/user.slice/user-1000.slice/session-6.scope" - // On cgroup v1, systemd is "name=systemd:/user.slice/user-1000.slice/session-6.scope" - lines := strings.Split(string(data), "\n") - var path string - for _, ln := range lines { - if ln == "" { - continue - } - parts := strings.SplitN(ln, ":", 3) - if len(parts) < 3 { - continue - } - controller, cgPath := parts[1], parts[2] - if controller == "" || controller == "name=systemd" || controller == "" /* v2 */ { - path = cgPath - // prefer v2 line if present; break on first match - if strings.HasPrefix(ln, "0::") { - break - } - } - } - if path == "" { - return "", fmt.Errorf("no cgroup path found") - } - // Extract slice segments ending with ".slice" - segs := strings.Split(strings.TrimPrefix(path, "/"), "/") - var slices []string - for _, s := range segs { - if strings.HasSuffix(s, ".slice") { - slices = append(slices, s) - } - } - if len(slices) == 0 { - // Fallback to uid mapping - return fmt.Sprintf("user.slice/user-%d.slice", pc.UID), nil - } - // Build "slice path" up to the deepest slice (exclude scopes/services) - // e.g., user.slice/user-1000.slice - var b strings.Builder - for i, s := range slices { - if i > 0 { - b.WriteString("/") - } - b.WriteString(s) - } - return b.String(), nil -} - - func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := httputils.ParseForm(r); err != nil { return err @@ -559,14 +498,14 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo logrus.Info("initialized empty HostConfig") } - if cred, ok := r.Context().Value(ctxkey.PeerCredKey).(*ctxkey.PeerCred); ok && cred != nil { + if cred, ok := r.Context().Value(cgroups.PeerCredKey).(*cgroups.PeerCred); ok && cred != nil { logrus.WithFields(logrus.Fields{ "pid": cred.PID, "uid": cred.UID, "gid": cred.GID, }).Debug("retrieved peer credentials from context") - if parent, err := deriveParentFromProc("syetemd", cred); err == nil { + if parent, err := cgroups.DeriveParentFromProc(cred); err == nil { hostConfig.CgroupParent = parent logrus.WithFields(logrus.Fields{ "pid": cred.PID, diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index a39dca2f695ce..8ddaafcd53d7e 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -13,8 +13,6 @@ import ( "strings" "sync" "time" - "syscall" - "golang.org/x/sys/unix" containerddefaults "github.com/containerd/containerd/defaults" @@ -24,7 +22,7 @@ import ( "github.com/docker/docker/api/server/middleware" "github.com/docker/docker/api/server/router" "github.com/docker/docker/api/server/router/build" - "github.com/docker/docker/pkg/ctxkey" + "github.com/docker/docker/pkg/cgroups" checkpointrouter "github.com/docker/docker/api/server/router/checkpoint" "github.com/docker/docker/api/server/router/container" distributionrouter "github.com/docker/docker/api/server/router/distribution" @@ -85,37 +83,6 @@ func NewDaemonCli() *DaemonCli { } } -func getPeerCred(c net.Conn) (*ctxkey.PeerCred, error) { - sc, ok := c.(syscall.Conn) - if !ok { - return nil, fmt.Errorf("not a syscall.Conn") - } - - raw, err := sc.SyscallConn() - if err != nil { - return nil, fmt.Errorf("SyscallConn: %w", err) - } - - var cred *ctxkey.PeerCred - var ctrlErr error - - // Control runs a function with the underlying FD. - if err := raw.Control(func(fd uintptr) { - ucred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) - if err != nil { - ctrlErr = err - return - } - cred = &ctxkey.PeerCred{PID: int(ucred.Pid), UID: int(ucred.Uid), GID: int(ucred.Gid)} - }); err != nil { - return nil, fmt.Errorf("raw.Control: %w", err) - } - if ctrlErr != nil { - return nil, fmt.Errorf("getsockopt SO_PEERCRED: %w", ctrlErr) - } - return cred, nil -} - func (cli *DaemonCli) start(opts *daemonOptions) (err error) { if cli.Config, err = loadDaemonCliConfig(opts); err != nil { return err @@ -221,14 +188,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { httpServer := &http.Server{ ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. ConnContext: func(ctx context.Context, c net.Conn) context.Context { - if cred, err := getPeerCred(c); err == nil && cred != nil { + if cred, err := cgroups.GetPeerCred(c); err == nil && cred != nil { logrus.WithFields(logrus.Fields{ "pid": cred.PID, "uid": cred.UID, "gid": cred.GID, "remoteAddr": c.RemoteAddr().String(), }).Info("accepted new connection with peer credentials") - return context.WithValue(ctx, ctxkey.PeerCredKey, cred) + return context.WithValue(ctx, cgroups.PeerCredKey, cred) } else if err != nil { logrus.WithError(err).Error("getPeerCred error") } diff --git a/pkg/cgroups/cgroups.go b/pkg/cgroups/cgroups.go new file mode 100644 index 0000000000000..ead27e139b76e --- /dev/null +++ b/pkg/cgroups/cgroups.go @@ -0,0 +1,135 @@ +package cgroups + +import ( + "os" + "fmt" + "strings" + "net" + "syscall" + "golang.org/x/sys/unix" +) + +// PeerCredKey is used as the context key for storing peer credentials. +var PeerCredKey = &struct{}{} + +type PeerCred struct { + PID int + UID int + GID int +} + +func GetPeerCred(c net.Conn) (*PeerCred, error) { + sc, ok := c.(syscall.Conn) + if !ok { + return nil, fmt.Errorf("not a syscall.Conn") + } + + raw, err := sc.SyscallConn() + if err != nil { + return nil, fmt.Errorf("SyscallConn: %w", err) + } + + var cred *PeerCred + var ctrlErr error + + // Control runs a function with the underlying FD. + if err := raw.Control(func(fd uintptr) { + ucred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + if err != nil { + ctrlErr = err + return + } + cred = &PeerCred{PID: int(ucred.Pid), UID: int(ucred.Uid), GID: int(ucred.Gid)} + }); err != nil { + return nil, fmt.Errorf("raw.Control: %w", err) + } + if ctrlErr != nil { + return nil, fmt.Errorf("getsockopt SO_PEERCRED: %w", ctrlErr) + } + return cred, nil +} + +func DeriveParentFromProcCgroupfs(pc *PeerCred) (string, error) { + if pc == nil || pc.PID == 0 { + return "", fmt.Errorf("no peer credentials") + } + + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) + if err != nil { + return "", fmt.Errorf("read cgroup: %w", err) + } + + var path string + for _, ln := range strings.Split(string(data), "\n") { + if ln == "" { continue } + parts := strings.SplitN(ln, ":", 3) + if len(parts) < 3 { continue } + if strings.HasPrefix(ln, "0::") { + path = parts[2] + break + } + if parts[1] == "cpu" { path = parts[2] } + } + if path == "" { + return "", fmt.Errorf("no cgroup path found") + } + return path, nil +} + +// deriveParentFromProc finds the deepest ".slice" in the caller's cgroup path +// and returns it as a systemd slice path, e.g. "user.slice/user-1000.slice". +func DeriveParentFromProc(pc *PeerCred) (string, error) { + if pc == nil || pc.PID == 0 { + return "", fmt.Errorf("no peer credentials") + } + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) + if err != nil { + return "", fmt.Errorf("read cgroup: %w", err) + } + // On cgroup v2, there is one line like: "0::/user.slice/user-1000.slice/session-6.scope" + // On cgroup v1, systemd is "name=systemd:/user.slice/user-1000.slice/session-6.scope" + lines := strings.Split(string(data), "\n") + var path string + for _, ln := range lines { + if ln == "" { + continue + } + parts := strings.SplitN(ln, ":", 3) + if len(parts) < 3 { + continue + } + controller, cgPath := parts[1], parts[2] + if controller == "" || controller == "name=systemd" || controller == "" /* v2 */ { + path = cgPath + // prefer v2 line if present; break on first match + if strings.HasPrefix(ln, "0::") { + break + } + } + } + if path == "" { + return "", fmt.Errorf("no cgroup path found") + } + // Extract slice segments ending with ".slice" + segs := strings.Split(strings.TrimPrefix(path, "/"), "/") + var slices []string + for _, s := range segs { + if strings.HasSuffix(s, ".slice") { + slices = append(slices, s) + } + } + if len(slices) == 0 { + // Fallback to uid mapping + return fmt.Sprintf("user.slice/user-%d.slice", pc.UID), nil + } + // Build "slice path" up to the deepest slice (exclude scopes/services) + // e.g., user.slice/user-1000.slice + var b strings.Builder + for i, s := range slices { + if i > 0 { + b.WriteString("/") + } + b.WriteString(s) + } + return b.String(), nil +} diff --git a/pkg/ctxkey/ctxkey.go b/pkg/ctxkey/ctxkey.go deleted file mode 100644 index 5a49e4141cfd0..0000000000000 --- a/pkg/ctxkey/ctxkey.go +++ /dev/null @@ -1,10 +0,0 @@ -package ctxkey - -// PeerCredKey is used as the context key for storing peer credentials. -var PeerCredKey = &struct{}{} - -type PeerCred struct { - PID int - UID int - GID int -} From a013f0c2236abd90848e6c1ada91c51d24235b5a Mon Sep 17 00:00:00 2001 From: John Robbins Date: Mon, 27 Oct 2025 14:15:16 -0600 Subject: [PATCH 7/9] Fix deriveFromProc function --- pkg/cgroups/cgroups.go | 116 ++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/pkg/cgroups/cgroups.go b/pkg/cgroups/cgroups.go index ead27e139b76e..510963758c794 100644 --- a/pkg/cgroups/cgroups.go +++ b/pkg/cgroups/cgroups.go @@ -76,60 +76,66 @@ func DeriveParentFromProcCgroupfs(pc *PeerCred) (string, error) { return path, nil } -// deriveParentFromProc finds the deepest ".slice" in the caller's cgroup path -// and returns it as a systemd slice path, e.g. "user.slice/user-1000.slice". func DeriveParentFromProc(pc *PeerCred) (string, error) { - if pc == nil || pc.PID == 0 { - return "", fmt.Errorf("no peer credentials") - } - data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) - if err != nil { - return "", fmt.Errorf("read cgroup: %w", err) - } - // On cgroup v2, there is one line like: "0::/user.slice/user-1000.slice/session-6.scope" - // On cgroup v1, systemd is "name=systemd:/user.slice/user-1000.slice/session-6.scope" - lines := strings.Split(string(data), "\n") - var path string - for _, ln := range lines { - if ln == "" { - continue - } - parts := strings.SplitN(ln, ":", 3) - if len(parts) < 3 { - continue - } - controller, cgPath := parts[1], parts[2] - if controller == "" || controller == "name=systemd" || controller == "" /* v2 */ { - path = cgPath - // prefer v2 line if present; break on first match - if strings.HasPrefix(ln, "0::") { - break - } - } - } - if path == "" { - return "", fmt.Errorf("no cgroup path found") - } - // Extract slice segments ending with ".slice" - segs := strings.Split(strings.TrimPrefix(path, "/"), "/") - var slices []string - for _, s := range segs { - if strings.HasSuffix(s, ".slice") { - slices = append(slices, s) - } - } - if len(slices) == 0 { - // Fallback to uid mapping - return fmt.Sprintf("user.slice/user-%d.slice", pc.UID), nil - } - // Build "slice path" up to the deepest slice (exclude scopes/services) - // e.g., user.slice/user-1000.slice - var b strings.Builder - for i, s := range slices { - if i > 0 { - b.WriteString("/") - } - b.WriteString(s) - } - return b.String(), nil + if pc == nil || pc.PID <= 0 { + return "", fmt.Errorf("no peer credentials") + } + + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pc.PID)) + if err != nil { + return "", fmt.Errorf("read cgroup: %w", err) + } + + lines := strings.Split(string(data), "\n") + + // Prefer cgroup v2 unified line "0::/path" + var cgPath string + for _, ln := range lines { + if ln == "" { + continue + } + parts := strings.SplitN(ln, ":", 3) + if len(parts) < 3 { + continue + } + controller, path := parts[1], parts[2] + + // v2 line + if strings.HasPrefix(ln, "0::") { + cgPath = path + break + } + // v1 systemd controller + if controller == "name=systemd" { + cgPath = path + // keep searching in case a v2 line appears later; if not, this stays + } + } + + if cgPath == "" { + // Fallback: pick a reasonable slice based on UID; user slices typically live under user.slice. + // Return a single slice *name* (no '/'). + if pc.UID >= 1000 { + return fmt.Sprintf("user-%d.slice", pc.UID), nil + } + return "system.slice", nil + } + + // Extract the deepest *.slice component and return just that unit name. + segs := strings.Split(strings.TrimPrefix(cgPath, "/"), "/") + var lastSlice string + for _, s := range segs { + if strings.HasSuffix(s, ".slice") { + lastSlice = s + } + } + if lastSlice != "" { + return lastSlice, nil // e.g., "user-1000.slice" + } + + // No *.slice segments found: fallback like above. + if pc.UID >= 1000 { + return fmt.Sprintf("user-%d.slice", pc.UID), nil + } + return "system.slice", nil } From 887995ffc4204bd472fa9c37b2dca8493b587f57 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 31 Oct 2025 08:46:59 -0600 Subject: [PATCH 8/9] Remove debug logging to make code easier to read --- api/server/router/container/container_routes.go | 17 ----------------- cmd/dockerd/daemon.go | 8 -------- 2 files changed, 25 deletions(-) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 788b3233b3581..0deb6d76ea509 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -499,26 +499,9 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo } if cred, ok := r.Context().Value(cgroups.PeerCredKey).(*cgroups.PeerCred); ok && cred != nil { - logrus.WithFields(logrus.Fields{ - "pid": cred.PID, - "uid": cred.UID, - "gid": cred.GID, - }).Debug("retrieved peer credentials from context") - if parent, err := cgroups.DeriveParentFromProc(cred); err == nil { hostConfig.CgroupParent = parent - logrus.WithFields(logrus.Fields{ - "pid": cred.PID, - "cgroup_parent": parent, - }).Info("set HostConfig.CgroupParent from deriveParentFromProc") - } else { - logrus.WithError(err).WithField("pid", cred.PID).Warn("deriveParentFromProc failed") } - } else { - logrus.WithFields(logrus.Fields{ - "hasCred": ok && cred != nil, - "alreadySet": hostConfig != nil && hostConfig.CgroupParent != "", - }).Error("create: skipping cgroup-parent injection") } diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 8ddaafcd53d7e..e9dc3266aa22b 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -189,15 +189,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout. ConnContext: func(ctx context.Context, c net.Conn) context.Context { if cred, err := cgroups.GetPeerCred(c); err == nil && cred != nil { - logrus.WithFields(logrus.Fields{ - "pid": cred.PID, - "uid": cred.UID, - "gid": cred.GID, - "remoteAddr": c.RemoteAddr().String(), - }).Info("accepted new connection with peer credentials") return context.WithValue(ctx, cgroups.PeerCredKey, cred) - } else if err != nil { - logrus.WithError(err).Error("getPeerCred error") } return ctx }, From 339b9c4d664e1bfe6780fbfaaf9916ee39b063b5 Mon Sep 17 00:00:00 2001 From: John Robbins Date: Fri, 21 Nov 2025 16:59:28 -0500 Subject: [PATCH 9/9] Use cgroupfs mode function --- api/server/router/container/container_routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 0deb6d76ea509..940326ed23223 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -499,7 +499,7 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo } if cred, ok := r.Context().Value(cgroups.PeerCredKey).(*cgroups.PeerCred); ok && cred != nil { - if parent, err := cgroups.DeriveParentFromProc(cred); err == nil { + if parent, err := cgroups.DeriveParentFromProcCgroupfs(cred); err == nil { hostConfig.CgroupParent = parent } }