Path: blob/main/components/ws-daemon/pkg/iws/uidmap.go
2499 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package iws56import (7"bufio"8"context"9"fmt"10"os"11"path/filepath"12"strconv"13"strings"1415"golang.org/x/xerrors"16"google.golang.org/grpc/codes"17"google.golang.org/grpc/status"18"google.golang.org/protobuf/encoding/protojson"1920"github.com/gitpod-io/gitpod/common-go/log"21"github.com/gitpod-io/gitpod/ws-daemon/api"22"github.com/gitpod-io/gitpod/ws-daemon/pkg/container"23)2425// Uidmapper provides UID mapping services for creating Linux user namespaces26// from within a workspace.27type Uidmapper struct {28Config UidmapperConfig29Runtime container.Runtime30}3132// UidmapperConfig configures the UID mapper33type UidmapperConfig struct {34// ProcLocation is the location of the node's proc filesystem35ProcLocation string `json:"procLocation"`36// RootRange is the range to which one can map the root (uid 0) user/group to37RootRange UIDRange `json:"rootUIDRange"`38// UserRange is the range to which any other user can be mapped to39UserRange []UIDRange `json:"userUIDRange"`40}4142// UIDRange represents a range of UID/GID's43type UIDRange struct {44Start uint32 `json:"start"`45Size uint32 `json:"size"`46}4748// Contains returns true if the other range is contained by this one49func (r UIDRange) Contains(start, size uint32) bool {50if start < r.Start {51return false52}53if size > r.Size {54return false55}56return true57}5859// HandleUIDMappingRequest performs a UID mapping request60func (m *Uidmapper) HandleUIDMappingRequest(ctx context.Context, req *api.WriteIDMappingRequest, containerID container.ID, instanceID string) (err error) {61var reqjson []byte62reqjson, err = protojson.Marshal(req)63if err != nil {64return err65}6667log := log.WithFields(map[string]interface{}{68"req": string(reqjson),69"containerID": containerID,70"instanceId": instanceID,71})7273log.Debug("received UID mapping request")7475err = m.validateMapping(req.Mapping)76if err != nil {77return err78}7980containerPID, err := m.Runtime.ContainerPID(ctx, containerID)81if err != nil {82log.WithError(err).Error("handleUIDMappingRequest: cannot get containerPID")83return status.Error(codes.Internal, "cannot establish mapping")84}8586log.WithField("containerPID", containerPID)8788hostPID, err := m.findHostPID(uint64(containerPID), uint64(req.Pid))89if err != nil {90log.WithError(err).Error("handleUIDMappingRequest: cannot find PID on host")91return status.Error(codes.InvalidArgument, "cannot find PID")92}9394log = log.WithField("hostPID", hostPID)9596err = WriteMapping(hostPID, req.Gid, req.Mapping)97if err != nil {98log.WithError(err).Error("handleUIDMappingRequest: cannot write mapping")99return status.Error(codes.FailedPrecondition, "cannot write mapping")100}101102log.Debug("established UID/GID mapping")103104return nil105}106107func (m *Uidmapper) validateMapping(mapping []*api.WriteIDMappingRequest_Mapping) error {108for _, mp := range mapping {109if mp.ContainerId == 0 && !m.Config.RootRange.Contains(mp.HostId, mp.Size) {110return status.Error(codes.InvalidArgument, "mapping for UID 0 is out of range")111}112if mp.ContainerId > 0 {113var found bool114for _, r := range m.Config.UserRange {115if r.Contains(mp.HostId, mp.Size) {116found = true117break118}119}120if !found {121return status.Errorf(codes.InvalidArgument, "mapping for UID %d is out of range", mp.ContainerId)122}123}124}125return nil126}127128// WriteMapping writes uid_map and gid_map129func WriteMapping(hostPID uint64, gid bool, mapping []*api.WriteIDMappingRequest_Mapping) (err error) {130// Note: unlike shadow's newuidmap/newgidmap we do not set /proc/PID/setgroups to deny because:131// - we're writing from a privileged process, hence don't trip that restriction introduced in Linux 3.39132// - denying setgroups would prevent any meaningfull use of the NS mapped "root" user (e.g. breaks apt-get)133134var fc string135for _, m := range mapping {136fc += fmt.Sprintf("%d %d %d\n", m.ContainerId, m.HostId, m.Size)137}138139var fn string140if gid {141fn = "gid_map"142} else {143fn = "uid_map"144}145146pth := fmt.Sprintf("/proc/%d/%s", hostPID, fn)147log.WithField("path", pth).WithField("fc", fc).Debug("attempting to write UID mapping")148149err = os.WriteFile(pth, []byte(fc), 0644)150if err != nil {151return xerrors.Errorf("cannot write UID/GID mapping: %w", err)152}153154return nil155}156157// findHosPID translates an in-container PID to the root PID namespace.158func (m *Uidmapper) findHostPID(containerPID, inContainerPID uint64) (uint64, error) {159paths := []string{fmt.Sprint(containerPID)}160seen := make(map[string]struct{})161162for {163if len(paths) == 0 {164return 0, xerrors.Errorf("cannot find in-container PID %d on the node", inContainerPID)165}166167p := paths[0]168paths = paths[1:]169170if _, ok := seen[p]; ok {171continue172}173seen[p] = struct{}{}174175p = filepath.Join(m.Config.ProcLocation, p)176pid, nspid, err := readStatusFile(filepath.Join(p, "status"))177if err != nil {178log.WithField("file", filepath.Join(p, "status")).WithError(err).Error("findHostPID: cannot read PID file")179continue180}181for _, nsp := range nspid {182if nsp == inContainerPID {183return pid, nil184}185}186187taskfn := filepath.Join(p, "task")188tasks, err := os.ReadDir(taskfn)189if err != nil {190continue191}192for _, task := range tasks {193cldrn, err := os.ReadFile(filepath.Join(taskfn, task.Name(), "children"))194if err != nil {195continue196}197paths = append(paths, strings.Fields(string(cldrn))...)198}199}200}201202func (m *Uidmapper) findSupervisorPID(containerPID uint64) (uint64, error) {203paths := []string{fmt.Sprint(containerPID)}204seen := make(map[string]struct{})205206for {207if len(paths) == 0 {208return 0, xerrors.Errorf("cannot find supervisor PID for container %v", containerPID)209}210211p := paths[0]212paths = paths[1:]213214if _, ok := seen[p]; ok {215continue216}217seen[p] = struct{}{}218219procPath := filepath.Join(m.Config.ProcLocation, p)220cmdline, err := os.ReadFile(filepath.Join(procPath, "cmdline"))221if err != nil {222log.WithField("file", filepath.Join(procPath, "cmdline")).WithError(err).Error("cannot read cmdline")223continue224}225226if strings.HasPrefix(string(cmdline), "supervisor") {227pid, err := strconv.ParseUint(p, 10, 64)228if err != nil {229return 0, err230}231232return pid, nil233}234235taskfn := filepath.Join(procPath, "task")236tasks, err := os.ReadDir(taskfn)237if err != nil {238continue239}240for _, task := range tasks {241cldrn, err := os.ReadFile(filepath.Join(taskfn, task.Name(), "children"))242if err != nil {243continue244}245paths = append(paths, strings.Fields(string(cldrn))...)246}247}248}249250func readStatusFile(fn string) (pid uint64, nspid []uint64, err error) {251f, err := os.Open(fn)252if err != nil {253return254}255defer f.Close()256257scanner := bufio.NewScanner(f)258for scanner.Scan() {259line := scanner.Text()260if strings.HasPrefix(line, "Pid:") {261pid, err = strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(line, "Pid:")), 10, 64)262if err != nil {263err = xerrors.Errorf("cannot parse pid in %s: %w", fn, err)264return265}266}267if strings.HasPrefix(line, "NSpid:") {268fields := strings.Fields(strings.TrimSpace(strings.TrimPrefix(line, "NSpid:")))269for _, fld := range fields {270var npid uint64271npid, err = strconv.ParseUint(fld, 10, 64)272if err != nil {273err = xerrors.Errorf("cannot parse NSpid %v in %s: %w", fld, fn, err)274return275}276277nspid = append(nspid, npid)278}279}280}281if err = scanner.Err(); err != nil {282return283}284285return286}287288289