Skip to content

Commit

Permalink
Merge pull request #51 from giuseppe/userns
Browse files Browse the repository at this point in the history
psgo: add support for running in a namespace
  • Loading branch information
vrothberg authored May 23, 2019
2 parents 2e6ba54 + 609a016 commit 69d5b73
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 18 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ before_install:
- sudo apt-get install -qq bats

script:
- make validate
- make build
- make test
- make validate || travis_terminate 1
- make build || travis_terminate 1
- make test || travis_terminate 1
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ install:

.PHONY: .install.lint
.install.lint:
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
# Workaround for https://github.com/golangci/golangci-lint/issues/523
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint@master

.PHONY: uninstall
uninstall:
Expand Down
41 changes: 41 additions & 0 deletions internal/proc/ns.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@
package proc

import (
"bufio"
"fmt"
"io"
"os"

"github.com/pkg/errors"
)

type IDMap struct {
ContainerID int
HostID int
Size int
}

// ParsePIDNamespace returns the content of /proc/$pid/ns/pid.
func ParsePIDNamespace(pid string) (string, error) {
pidNS, err := os.Readlink(fmt.Sprintf("/proc/%s/ns/pid", pid))
Expand All @@ -36,3 +46,34 @@ func ParseUserNamespace(pid string) (string, error) {
}
return userNS, nil
}

// ReadMappings reads the user namespace mappings at the specified path
func ReadMappings(path string) ([]IDMap, error) {
file, err := os.Open(path)
if err != nil {
return nil, errors.Wrapf(err, "cannot open %s", path)
}
defer file.Close()

mappings := []IDMap{}

buf := bufio.NewReader(file)
for {
line, _, err := buf.ReadLine()
if err != nil {
if err == io.EOF {
return mappings, nil
}
return nil, errors.Wrapf(err, "cannot read line from %s", path)
}
if line == nil {
return mappings, nil
}

containerID, hostID, size := 0, 0, 0
if _, err := fmt.Sscanf(string(line), "%d %d %d", &containerID, &hostID, &size); err != nil {
return nil, errors.Wrapf(err, "cannot parse %s", string(line))
}
mappings = append(mappings, IDMap{ContainerID: containerID, HostID: hostID, Size: size})
}
}
4 changes: 2 additions & 2 deletions internal/process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Process struct {
Hgroup string
}

// LookupGID returns the textual group ID, if it can be optained, or the
// LookupGID returns the textual group ID, if it can be obtained, or the
// decimal representation otherwise.
func LookupGID(gid string) (string, error) {
gidNum, err := strconv.Atoi(gid)
Expand All @@ -59,7 +59,7 @@ func LookupGID(gid string) (string, error) {
return g.Name, nil
}

// LookupUID return the textual user ID, if it can be optained, or the decimal
// LookupUID return the textual user ID, if it can be obtained, or the decimal
// representation otherwise.
func LookupUID(uid string) (string, error) {
uidNum, err := strconv.Atoi(uid)
Expand Down
127 changes: 120 additions & 7 deletions psgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package psgo

import (
"fmt"
"io/ioutil"
"os"
"runtime"
"sort"
Expand All @@ -43,13 +44,40 @@ import (
"golang.org/x/sys/unix"
)

// IDMap specifies a mapping range from the host to the container IDs.
type IDMap struct {
// ContainerID is the first ID in the container.
ContainerID int
// HostID is the first ID in the host.
HostID int
// Size specifies how long is the range. e.g. 1 means a single user
// is mapped.
Size int
}

// JoinNamespaceOpts specifies different options for joining the specified namespaces.
type JoinNamespaceOpts struct {
// UIDMap specifies a mapping for UIDs in the container. If specified
// huser will perform the reverse mapping.
UIDMap []IDMap
// GIDMap specifies a mapping for GIDs in the container. If specified
// hgroup will perform the reverse mapping.
GIDMap []IDMap

// FillMappings specified whether UIDMap and GIDMap must be initialized
// with the current user namespace.
FillMappings bool
}

type psContext struct {
// Processes in the container.
containersProcesses []*process.Process
// Processes on the host. Used to map those to the ones running in the container.
hostProcesses []*process.Process
// tty and pty devices.
ttys *[]dev.TTY
// Various options
opts *JoinNamespaceOpts
}

// processFunc is used to map a given aixFormatDescriptor to a corresponding
Expand All @@ -69,10 +97,36 @@ type aixFormatDescriptor struct {
// onHost controls if data of the corresponding host processes will be
// extracted as well.
onHost bool
// procFN points to the corresponding method to etract the desired data.
// procFN points to the corresponding method to extract the desired data.
procFn processFunc
}

// findID converts the specified id to the host mapping
func findID(idStr string, mapping []IDMap, lookupFunc func(uid string) (string, error), overflowFile string) (string, error) {
if len(mapping) == 0 {
return idStr, nil
}

id, err := strconv.ParseInt(idStr, 10, 0)
if err != nil {
return "", errors.Wrapf(err, "cannot parse %s", idStr)
}
for _, m := range mapping {
if int(id) >= m.ContainerID && int(id) < m.ContainerID+m.Size {
user := fmt.Sprintf("%d", m.HostID+(int(id)-m.ContainerID))

return lookupFunc(user)
}
}

// User not found, read the overflow
overflow, err := ioutil.ReadFile(overflowFile)
if err != nil {
return "", errors.Wrapf(err, "cannot read %s", overflowFile)
}
return string(overflow), nil
}

// translateDescriptors parses the descriptors and returns a correspodning slice of
// aixFormatDescriptors. Descriptors can be specified in the normal and in the
// code form (if supported). If the descriptors slice is empty, the
Expand Down Expand Up @@ -272,6 +326,46 @@ func ListDescriptors() (list []string) {
// JoinNamespaceAndProcessInfo has the same semantics as ProcessInfo but joins
// the mount namespace of the specified pid before extracting data from `/proc`.
func JoinNamespaceAndProcessInfo(pid string, descriptors []string) ([][]string, error) {
return JoinNamespaceAndProcessInfoWithOptions(pid, descriptors, &JoinNamespaceOpts{})
}

func readMappings(path string) ([]IDMap, error) {
mappings, err := proc.ReadMappings(path)
if err != nil {
return nil, err
}
var res []IDMap
for _, i := range mappings {
m := IDMap{ContainerID: i.ContainerID, HostID: i.HostID, Size: i.Size}
res = append(res, m)
}
return res, nil
}

func contextFromOptions(options *JoinNamespaceOpts) (*psContext, error) {
ctx := new(psContext)
ctx.opts = options
if ctx.opts != nil && ctx.opts.FillMappings {
uidMappings, err := readMappings("/proc/self/uid_map")
if err != nil {
return nil, err
}

gidMappings, err := readMappings("/proc/self/gid_map")
if err != nil {
return nil, err
}
ctx.opts.UIDMap = uidMappings
ctx.opts.GIDMap = gidMappings

ctx.opts.FillMappings = false
}
return ctx, nil
}

// JoinNamespaceAndProcessInfoWithOptions has the same semantics as ProcessInfo but joins
// the mount namespace of the specified pid before extracting data from `/proc`.
func JoinNamespaceAndProcessInfoWithOptions(pid string, descriptors []string, options *JoinNamespaceOpts) ([][]string, error) {
var (
data [][]string
dataErr error
Expand All @@ -283,7 +377,10 @@ func JoinNamespaceAndProcessInfo(pid string, descriptors []string) ([][]string,
return nil, err
}

ctx := new(psContext)
ctx, err := contextFromOptions(options)
if err != nil {
return nil, err
}

// extract data from host processes only on-demand / when at least one
// of the specified descriptors requires host data
Expand Down Expand Up @@ -356,10 +453,10 @@ func JoinNamespaceAndProcessInfo(pid string, descriptors []string) ([][]string,
return data, dataErr
}

// JoinNamespaceAndProcessInfoByPids has similar semantics to
// JoinNamespaceAndProcessInfoByPidsWithOptions has similar semantics to
// JoinNamespaceAndProcessInfo and avoids duplicate entries by joining a giving
// PID namepsace only once.
func JoinNamespaceAndProcessInfoByPids(pids []string, descriptors []string) ([][]string, error) {
// PID namespace only once.
func JoinNamespaceAndProcessInfoByPidsWithOptions(pids []string, descriptors []string, options *JoinNamespaceOpts) ([][]string, error) {
// Extracting data from processes that share the same PID namespace
// would yield duplicate results. Avoid that by extracting data only
// from the first process in `pids` from a given PID namespace.
Expand All @@ -385,7 +482,7 @@ func JoinNamespaceAndProcessInfoByPids(pids []string, descriptors []string) ([][

data := [][]string{}
for i, pid := range pidList {
pidData, err := JoinNamespaceAndProcessInfo(pid, descriptors)
pidData, err := JoinNamespaceAndProcessInfoWithOptions(pid, descriptors, options)
if os.IsNotExist(errors.Cause(err)) {
// catch race conditions
continue
Expand All @@ -402,6 +499,13 @@ func JoinNamespaceAndProcessInfoByPids(pids []string, descriptors []string) ([][
return data, nil
}

// JoinNamespaceAndProcessInfoByPids has similar semantics to
// JoinNamespaceAndProcessInfo and avoids duplicate entries by joining a giving
// PID namespace only once.
func JoinNamespaceAndProcessInfoByPids(pids []string, descriptors []string) ([][]string, error) {
return JoinNamespaceAndProcessInfoByPidsWithOptions(pids, descriptors, &JoinNamespaceOpts{})
}

// ProcessInfo returns the process information of all processes in the current
// mount namespace. The input format must be a comma-separated list of
// supported AIX format descriptors. If the input string is empty, the
Expand All @@ -425,7 +529,10 @@ func ProcessInfoByPids(pids []string, descriptors []string) ([][]string, error)
return nil, err
}

ctx := new(psContext)
ctx, err := contextFromOptions(nil)
if err != nil {
return nil, err
}
ctx.containersProcesses, err = process.FromPIDs(pids, false)
if err != nil {
return nil, err
Expand Down Expand Up @@ -725,6 +832,9 @@ func processHPID(p *process.Process, ctx *psContext) (string, error) {
// of the (container) or "?" if no corresponding process could be found.
func processHUSER(p *process.Process, ctx *psContext) (string, error) {
if hp := findHostProcess(p, ctx); hp != nil {
if ctx.opts != nil && len(ctx.opts.UIDMap) > 0 {
return findID(p.Status.Uids[1], ctx.opts.UIDMap, process.LookupUID, "/proc/sys/fs/overflowuid")
}
return hp.Huser, nil
}
return "?", nil
Expand All @@ -735,6 +845,9 @@ func processHUSER(p *process.Process, ctx *psContext) (string, error) {
// found.
func processHGROUP(p *process.Process, ctx *psContext) (string, error) {
if hp := findHostProcess(p, ctx); hp != nil {
if ctx.opts != nil && len(ctx.opts.GIDMap) > 0 {
return findID(p.Status.Gids[1], ctx.opts.GIDMap, process.LookupGID, "/proc/sys/fs/overflowgid")
}
return hp.Hgroup, nil
}
return "?", nil
Expand Down
18 changes: 13 additions & 5 deletions sample/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ func main() {
data [][]string
err error

pids = flag.String("pids", "", "comma separated list of process IDs to retrieve")
format = flag.String("format", "", "ps(1) AIX format comma-separated string")
list = flag.Bool("list", false, "list all supported descriptors")
join = flag.Bool("join", false, "join namespace of provided pids (containers)")
pids = flag.String("pids", "", "comma separated list of process IDs to retrieve")
format = flag.String("format", "", "ps(1) AIX format comma-separated string")
list = flag.Bool("list", false, "list all supported descriptors")
join = flag.Bool("join", false, "join namespace of provided pids (containers)")
fillMappings = flag.Bool("fill-mappings", false, "fill the UID and GID mappings with the current user namespace")
)

flag.Parse()

if *fillMappings && !*join {
fmt.Fprintln(os.Stderr, "-fill-mappings requires -join")
os.Exit(1)
}

if *list {
fmt.Println(strings.Join(psgo.ListDescriptors(), ", "))
return
Expand All @@ -40,8 +46,10 @@ func main() {
}

if len(pidsList) > 0 {
opts := psgo.JoinNamespaceOpts{FillMappings: *fillMappings}

if *join {
data, err = psgo.JoinNamespaceAndProcessInfoByPids(pidsList, descriptors)
data, err = psgo.JoinNamespaceAndProcessInfoByPidsWithOptions(pidsList, descriptors, &opts)
} else {
data, err = psgo.ProcessInfoByPids(pidsList, descriptors)
}
Expand Down
16 changes: 16 additions & 0 deletions test/join.bats
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,19 @@ function is_podman_available() {
sudo podman rm -f $ID_1 $ID_2
sudo podman pod rm $POD_ID
}

@test "Test fill-mappings" {
run unshare -muinpfr --mount-proc true
if [[ "$status" -ne 0 ]]; then
skip "unshare doesn't support all the needed options"
fi

INTERVAL=10$RANDOM
unshare -muinpfr --mount-proc sleep $INTERVAL &

PID=$(pgrep -fa $INTERVAL | grep -v unshare | cut -f 1 -d ' ')
run nsenter --preserve-credentials -U -t $PID ./bin/psgo -pids $PID -join -fill-mappings -format huser
[ "$status" -eq 0 ]
[[ ${lines[0]} != "root" ]]
kill -9 $PID
}

0 comments on commit 69d5b73

Please sign in to comment.