Files
buildkit/executor/oci/user.go
Tonis Tiigi 408266e4ba user: limit size of parsed passwd/group files
Resolving a username to uid/gid read /etc/passwd and /etc/group via
os.Open with no upper bound, letting a crafted image force unbounded
memory use during user resolution. Cap reads at 10MiB and reject
non-regular files in both the OCI executor and the chown user resolver.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 83cfc1e0ea1dcf8816f259ee6720b8694ab874e5)
2026-06-24 10:56:53 -07:00

158 lines
3.8 KiB
Go

package oci
import (
"context"
"io"
"os"
"slices"
"strconv"
"strings"
"github.com/containerd/containerd/v2/core/containers"
containerdoci "github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/continuity/fs"
"github.com/moby/sys/user"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
const maxUserFileBytes = 10 << 20
func GetUser(root, username string) (uint32, uint32, []uint32, error) {
var isDefault bool
if username == "" {
username = "0"
isDefault = true
}
// fast path from uid/gid
if uid, gid, err := ParseUIDGID(username); err == nil {
return uid, gid, nil, nil
}
passwdFile, err := openUserFile(root, "/etc/passwd")
if err == nil {
defer passwdFile.Close()
}
groupFile, err := openUserFile(root, "/etc/group")
if err == nil {
defer groupFile.Close()
}
execUser, err := user.GetExecUser(username, nil, passwdFile, groupFile)
if err != nil {
if isDefault {
return 0, 0, nil, nil
}
return 0, 0, nil, err
}
var sgids []uint32
for _, g := range execUser.Sgids {
sgids = append(sgids, uint32(g))
}
return uint32(execUser.Uid), uint32(execUser.Gid), sgids, nil
}
// ParseUIDGID takes the fast path to parse UID and GID if and only if they are both provided
func ParseUIDGID(str string) (uid uint32, gid uint32, err error) {
if str == "" {
return 0, 0, nil
}
parts := strings.SplitN(str, ":", 2)
if len(parts) == 1 {
return 0, 0, errors.New("groups ID is not provided")
}
if uid, err = parseUID(parts[0]); err != nil {
return 0, 0, err
}
if gid, err = parseUID(parts[1]); err != nil {
return 0, 0, err
}
return
}
func openUserFile(root, p string) (io.ReadCloser, error) {
p, err := fs.RootPath(root, p)
if err != nil {
return nil, errors.WithStack(err)
}
f, err := os.Open(p)
if err != nil {
return nil, errors.WithStack(err)
}
info, err := f.Stat()
if err != nil {
f.Close()
return nil, errors.WithStack(err)
}
if !info.Mode().IsRegular() {
f.Close()
return nil, errors.Errorf("%s is not a regular file", p)
}
return &limitedReadCloser{
ReadCloser: f,
r: &io.LimitedReader{R: f, N: maxUserFileBytes + 1},
name: p,
}, nil
}
type limitedReadCloser struct {
io.ReadCloser
r *io.LimitedReader
name string
}
func (l *limitedReadCloser) Read(p []byte) (int, error) {
n, err := l.r.Read(p)
if l.r.N == 0 {
return n, errors.Errorf("%q exceeds %d bytes", l.name, maxUserFileBytes)
}
return n, err
}
func parseUID(str string) (uint32, error) {
if str == "root" {
return 0, nil
}
uid, err := strconv.ParseUint(str, 10, 32)
if err != nil {
return 0, err
}
return uint32(uid), nil
}
// WithUIDGID allows the UID and GID for the Process to be set
// FIXME: This is a temporeray fix for the missing supplementary GIDs from containerd
// once the PR in containerd is merged we should remove this function.
func WithUIDGID(uid, gid uint32, sgids []uint32) containerdoci.SpecOpts {
return func(_ context.Context, _ containerdoci.Client, _ *containers.Container, s *containerdoci.Spec) error {
defer ensureAdditionalGids(s)
setProcess(s)
s.Process.User.UID = uid
s.Process.User.GID = gid
s.Process.User.AdditionalGids = sgids
return nil
}
}
// setProcess sets Process to empty if unset
// FIXME: Same on this one. Need to be removed after containerd fix merged
func setProcess(s *containerdoci.Spec) {
if s.Process == nil {
s.Process = &specs.Process{}
}
}
// ensureAdditionalGids ensures that the primary GID is also included in the additional GID list.
// From https://github.com/containerd/containerd/blob/v1.7.0-beta.4/oci/spec_opts.go#L124-L133
func ensureAdditionalGids(s *containerdoci.Spec) {
setProcess(s)
if slices.Contains(s.Process.User.AdditionalGids, s.Process.User.GID) {
return
}
s.Process.User.AdditionalGids = append([]uint32{s.Process.User.GID}, s.Process.User.AdditionalGids...)
}