Skip to content

Commit

Permalink
Support using /usr/bin/getsubids (--subid-source=dynamic)
Browse files Browse the repository at this point in the history
* `--subid-source=auto` (default): Try dynamic, then fall back to static
* `--subid-source=dynamic`:        Execute `/usr/bin/getsubids`
* `--subid-source=static`:         Read `/etc/{subuid,subgid}`

Fix issue 254

Signed-off-by: Akihiro Suda <[email protected]>
  • Loading branch information
AkihiroSuda committed Nov 15, 2022
1 parent 1206c9e commit b39d64e
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,4 @@ Undocumented environment variables are subject to change.
- [`./docs/mount.md`](./docs/mount.md): Mount (`--propagation`, ...)
- [`./docs/process.md`](./docs/process.md): Process (`--pidns`, `--reaper`, `--cgroupns`, `--evacuate-cgroup2`, ...)
- [`./docs/api.md`](./docs/api.md): REST API
- [`./docs/subid.md`](./docs/subid.md): Sub UIDs and sub GIDs
1 change: 1 addition & 0 deletions cmd/rootlesskit/category.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
CategoryPort = "Port"
CategoryMount = "Mount"
CategoryProcess = "Process"
CategorySubID = "SubID"
CategoryMisc = "Misc"
)

Expand Down
6 changes: 6 additions & 0 deletions cmd/rootlesskit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ See https://rootlesscontaine.rs/getting-started/common/ .
Name: "evacuate-cgroup2",
Usage: "evacuate processes into the specified subgroup. Requires --pidns and --cgroupns",
}, CategoryProcess),
Categorize(&cli.StringFlag{
Name: "subid-source",
Value: "auto",
Usage: "the source of the subids. \"dynamic\" executes /usr/bin/getsubids. \"static\" reads /etc/{subuid,subgid}. [auto,dynamic,static]",
}, CategorySubID),
}
app.CustomAppHelpTemplate = `NAME:
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
Expand Down Expand Up @@ -264,6 +269,7 @@ func createParentOpt(clicontext *cli.Context, pipeFDEnvKey, stateDirEnvKey, pare
ParentEGIDEnvKey: parentEGIDEnvKey,
Propagation: clicontext.String("propagation"),
EvacuateCgroup2: clicontext.String("evacuate-cgroup2"),
SubidSource: parent.SubidSource(clicontext.String("subid-source")),
}
if opt.EvacuateCgroup2 != "" {
if !opt.CreateCgroupNS {
Expand Down
40 changes: 40 additions & 0 deletions docs/subid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# subid sources

The subid sources can be specified via the `--subid-source=(auto|dynamic|static)` flag.

The `auto` source is the default since RootlessKit v1.1.0.
Prior to v1.1.0, only the `static` source was supported.

## Auto
The `auto` source (`--subid-source=auto`) tries the `dynamic` source and fall backs to the `static` source on an error.

## Dynamic
The `dynamic` source (`--subid-source=dynamic`) executes the `/usr/bin/getsubids` binary to get the subids.

The `getsubuids` binary is known to be available for the following distributions:
- Fedora, since 35 (`dnf install shadow-utils-subid`)
- Alpine, since 3.16 (`apkg install shadow-subids`)
- Ubuntu, since 22.10 (`apt-get install uidmap`)

The `getsubids` binary typically reads subids from `/etc/subuid` and `/etc/subgid` as in the static mode,
but it can be also configured to use SSSD by specifying `subid: sss` in `/etc/nsswitch.conf`.

See also https://manpages.debian.org/testing/uidmap/getsubids.1.en.html .

## Static
The `static` source (`--subid-source=static`) reads subids from `/etc/subuid` and `/etc/subgid`.

`/etc/subuid` and `/etc/subgid` should contain more than 65536 sub-IDs. e.g. `penguin:231072:65536`. These files are automatically configured on most distributions.

```console
$ id -u
1001
$ whoami
penguin
$ grep "^$(whoami):" /etc/subuid
penguin:231072:65536
$ grep "^$(whoami):" /etc/subgid
penguin:231072:65536
```

See also https://rootlesscontaine.rs/getting-started/common/subuid/
122 changes: 122 additions & 0 deletions pkg/parent/dynidtools/dynidtools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package dynidtools

import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"

"github.com/rootless-containers/rootlesskit/pkg/parent/idtools"
"github.com/sirupsen/logrus"
)

func GetSubIDRanges(uid int, username string) ([]idtools.SubIDRange, []idtools.SubIDRange, error) {
getsubidsExeName := "getsubids"
if v := os.Getenv("GETSUBIDS"); v != "" {
getsubidsExeName = v
}
getsubidsExe, err := exec.LookPath(getsubidsExeName)
if err != nil {
return nil, nil, fmt.Errorf("subid-source:dynamic: %w", err)
}

uByUsername, uByUsernameErr := execGetsubids(getsubidsExe, false, username)
uByUID, uByUIDErr := execGetsubids(getsubidsExe, false, strconv.Itoa(uid))
// Typically, uByUsernameErr == nil, uByUIDErr == "Error fetching ranges" (exit code 1)
if uByUsernameErr != nil {
logrus.WithError(uByUsernameErr).Debugf("subid-source:dynamic: failed to get subuids by the username %q", username)
}
if uByUIDErr != nil {
logrus.WithError(uByUIDErr).Debugf("subid-source:dynamic: failed to get subuids by the UID %d", uid)
if uByUsernameErr != nil {
return nil, nil, fmt.Errorf("subid-source:dynamic: failed to get subuids by the username %q: %w; also failed to get subuids by the UID %d: %v",
username, uByUsernameErr, uid, uByUIDErr)
}
}

gByUsername, gByUsernameErr := execGetsubids(getsubidsExe, true, username)
gByUID, gByUIDErr := execGetsubids(getsubidsExe, true, strconv.Itoa(uid))
// Typically, gByUsernameErr == nil, gByUIDErr == "Error fetching ranges" (exit code 1)
if gByUsernameErr != nil {
logrus.WithError(gByUsernameErr).Debugf("subid-source:dynamic: failed to get subgids by the username %q", username)
}
if gByUIDErr != nil {
logrus.WithError(gByUIDErr).Debugf("subid-source:dynamic: failed to get subgids by the UID %d", uid)
if gByUsernameErr != nil {
return nil, nil, fmt.Errorf("subid-source:dynamic: failed to get subgids by the username %q: %w; also failed to get subuids by the UID %d: %v",
username, gByUsernameErr, uid, gByUIDErr)
}
}

u := append(uByUsername, uByUID...)
g := append(gByUsername, gByUID...)
return u, g, nil
}

// execGetsubids executes `getsubids [-g] user`
func execGetsubids(exe string, g bool, s string) ([]idtools.SubIDRange, error) {
var args []string
if g {
args = append(args, "-g")
}
var stderr bytes.Buffer
args = append(args, s)
cmd := exec.Command(exe, args...)
cmd.Stderr = &stderr
logrus.Debugf("Executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to exec %v: %w (stdout=%q, stderr=%q)", cmd.Args, err, string(out), stderr.String())
}
r := bytes.NewReader(out)
ranges, warns, err := parseGetsubidsOutput(r)
for _, warn := range warns {
logrus.Warnf("Error while parsing the result of %v: %s (stdout=%q, stderr=%q)", cmd.Args, warn, string(out), stderr.String())
}
return ranges, err
}

func parseGetsubidsOutput(r io.Reader) (res []idtools.SubIDRange, warns []string, err error) {
sc := bufio.NewScanner(r)
for i := 0; sc.Scan(); i++ {
line := strings.TrimSpace(sc.Text())
// line is like "0: foo 100000 655360"
if line == "" || strings.HasPrefix(line, "#") {
continue
}
splitByColon := strings.Split(line, ":")
switch len(splitByColon) {
case 0, 1:
return res, warns, fmt.Errorf("line %d: unparsable line %q", i+1, line)
case 2:
// NOP
default:
warns = append(warns, fmt.Sprintf("line %d: line %q contains unknown fields", i+1, line))
}
triplet := strings.Fields(strings.TrimSpace(splitByColon[1]))
switch len(triplet) {
case 0, 1, 2:
return res, warns, fmt.Errorf("line %d: unparsable line %q", i+1, line)
case 3:
// NOP
default:
warns = append(warns, fmt.Sprintf("line %d: line %q contains unknown fields", i+1, line))
}
var entry idtools.SubIDRange
entry.Start, err = strconv.Atoi(triplet[1])
if err != nil {
return res, warns, fmt.Errorf("line %d: unparsable line %q: failed to Atoi(%q): %w", i+1, line, triplet[1], err)
}
entry.Length, err = strconv.Atoi(triplet[2])
if err != nil {
return res, warns, fmt.Errorf("line %d: unparsable line %q: failed to Atoi(%q): %w", i+1, line, triplet[2], err)
}
res = append(res, entry)
}
err = sc.Err()
return
}
25 changes: 25 additions & 0 deletions pkg/parent/dynidtools/dynidtools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dynidtools

import (
"strings"
"testing"

"github.com/rootless-containers/rootlesskit/pkg/parent/idtools"
"gotest.tools/v3/assert"
)

func TestParseGetsubidsOutput(t *testing.T) {
const s = `# foo
0: foo 100000 655360
`
expected := []idtools.SubIDRange{
{
Start: 100000,
Length: 655360,
},
}
got, warn, err := parseGetsubidsOutput(strings.NewReader(s))
assert.NilError(t, err)
assert.Equal(t, 0, len(warn))
assert.DeepEqual(t, expected, got)
}
27 changes: 19 additions & 8 deletions pkg/parent/idtools/idtools.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ type IDMap struct {
Size int `json:"size"`
}

type subIDRange struct {
type SubIDRange struct {
Start int
Length int
}

type ranges []subIDRange
type ranges []SubIDRange

func (e ranges) Len() int { return len(e) }
func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
Expand Down Expand Up @@ -97,25 +97,36 @@ type IdentityMapping struct {
// using the data from /etc/sub{uid,gid} ranges, creates the
// proper uid and gid remapping ranges for that user/group pair
func NewIdentityMapping(uid int, username string) (*IdentityMapping, error) {
subuidRanges, err := parseSubuid(uid, username)
subuidRanges, subgidRanges, err := GetSubIDRanges(uid, username)
if err != nil {
return nil, err
}
return NewIdentityMappingFromSubIDRanges(subuidRanges, subgidRanges), nil
}

func GetSubIDRanges(uid int, username string) ([]SubIDRange, []SubIDRange, error) {
subuidRanges, err := parseSubuid(uid, username)
if err != nil {
return nil, nil, err
}
subgidRanges, err := parseSubgid(uid, username)
if err != nil {
return nil, err
return nil, nil, err
}
if len(subuidRanges) == 0 {
return nil, fmt.Errorf("No subuid ranges found for user %d (%q)", uid, username)
return nil, nil, fmt.Errorf("No subuid ranges found for user %d (%q)", uid, username)
}
if len(subgidRanges) == 0 {
return nil, fmt.Errorf("No subgid ranges found for user %d (%q)", uid, username)
return nil, nil, fmt.Errorf("No subgid ranges found for user %d (%q)", uid, username)
}
return subuidRanges, subgidRanges, nil
}

func NewIdentityMappingFromSubIDRanges(subuidRanges, subgidRanges []SubIDRange) *IdentityMapping {
return &IdentityMapping{
uids: createIDMap(subuidRanges),
gids: createIDMap(subgidRanges),
}, nil
}
}

// NewIDMappingsFromMaps creates a new mapping from two slices
Expand Down Expand Up @@ -232,7 +243,7 @@ func parseSubidFile(path string, uid int, username string) (ranges, error) {
if err != nil {
return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
}
rangeList = append(rangeList, subIDRange{startid, length})
rangeList = append(rangeList, SubIDRange{startid, length})
}
}
return rangeList, s.Err()
Expand Down
61 changes: 49 additions & 12 deletions pkg/parent/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/rootless-containers/rootlesskit/pkg/msgutil"
"github.com/rootless-containers/rootlesskit/pkg/network"
"github.com/rootless-containers/rootlesskit/pkg/parent/cgrouputil"
"github.com/rootless-containers/rootlesskit/pkg/parent/dynidtools"
"github.com/rootless-containers/rootlesskit/pkg/parent/idtools"
"github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/sigproxy"
Expand All @@ -45,8 +46,17 @@ type Opt struct {
ParentEGIDEnvKey string // optional env key to propagate getegid() value
Propagation string
EvacuateCgroup2 string // e.g. "rootlesskit_evacuation"
SubidSource SubidSource
}

type SubidSource string

const (
SubidSourceAuto = SubidSource("auto") // Try dynamic then fallback to static
SubidSourceDynamic = SubidSource("dynamic") // /usr/bin/getsubids
SubidSourceStatic = SubidSource("static") // /etc/{subuid,subgid}
)

// Documented state files. Undocumented ones are subject to change.
const (
StateFileLock = "lock"
Expand Down Expand Up @@ -175,7 +185,7 @@ func Parent(opt Opt) error {
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start the child: %w", err)
}
if err := setupUIDGIDMap(cmd.Process.Pid); err != nil {
if err := setupUIDGIDMap(cmd.Process.Pid, opt.SubidSource); err != nil {
return fmt.Errorf("failed to setup UID/GID map: %w", err)
}
sigc := sigproxy.ForwardAllSignals(context.TODO(), cmd.Process.Pid)
Expand Down Expand Up @@ -286,11 +296,45 @@ func Parent(opt Opt) error {
return err
}

func newugidmapArgs() ([]string, []string, error) {
func getSubIDRanges(u *user.User, subidSource SubidSource) ([]idtools.SubIDRange, []idtools.SubIDRange, error) {
uid, err := strconv.Atoi(u.Uid)
if err != nil {
return nil, nil, err
}
switch subidSource {
case SubidSourceStatic:
logrus.Debugf("subid-source: using the static source")
return idtools.GetSubIDRanges(uid, u.Username)
case SubidSourceDynamic:
logrus.Debugf("subid-source: using the dynamic source")
return dynidtools.GetSubIDRanges(uid, u.Username)
case "", SubidSourceAuto:
subuidRanges, subgidRanges, err := getSubIDRanges(u, SubidSourceDynamic)
if err == nil && len(subuidRanges) > 0 && len(subgidRanges) > 0 {
return subuidRanges, subgidRanges, nil
}
logrus.WithError(err).Debugf("failed to use subid source %q, falling back to %q", SubidSourceDynamic, SubidSourceStatic)
return getSubIDRanges(u, SubidSourceStatic)
default:
return nil, nil, fmt.Errorf("unknown subid source %q", subidSource)
}
}

func newugidmapArgs(subidSource SubidSource) ([]string, []string, error) {
u, err := user.Current()
if err != nil {
return nil, nil, err
}
subuidRanges, subgidRanges, err := getSubIDRanges(u, subidSource)
if err != nil {
return nil, nil, err
}
logrus.Debugf("subuid ranges=%v", subuidRanges)
logrus.Debugf("subgid ranges=%v", subgidRanges)
return newugidmapArgsFromSubIDRanges(u, subuidRanges, subgidRanges)
}

func newugidmapArgsFromSubIDRanges(u *user.User, subuidRanges, subgidRanges []idtools.SubIDRange) ([]string, []string, error) {
uidMap := []string{
"0",
u.Uid,
Expand All @@ -302,14 +346,7 @@ func newugidmapArgs() ([]string, []string, error) {
"1",
}

uid, err := strconv.Atoi(u.Uid)
if err != nil {
return nil, nil, err
}
ims, err := idtools.NewIdentityMapping(uid, u.Username)
if err != nil {
return nil, nil, err
}
ims := idtools.NewIdentityMappingFromSubIDRanges(subuidRanges, subgidRanges)

uidMapLast := 1
for _, im := range ims.UIDs() {
Expand All @@ -333,8 +370,8 @@ func newugidmapArgs() ([]string, []string, error) {
return uidMap, gidMap, nil
}

func setupUIDGIDMap(pid int) error {
uArgs, gArgs, err := newugidmapArgs()
func setupUIDGIDMap(pid int, subidSource SubidSource) error {
uArgs, gArgs, err := newugidmapArgs(subidSource)
if err != nil {
return fmt.Errorf("failed to compute uid/gid map: %w", err)
}
Expand Down
Loading

0 comments on commit b39d64e

Please sign in to comment.