Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions data/config/toolbox.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@
# consulted, and if it's not present there then it will be pulled from a
# suitable remote registry.
## image = "registry.fedoraproject.org/fedora-toolbox:34"

# Map supplementary groups from the host to numerical group IDs in the toolbox
# container. Each mapping uses the form 'HOST_GID:CONTAINER_GID'. The host GID
# must belong to the user and must be delegated through subgid(5).
## gid_maps = ["971:100000"]
17 changes: 16 additions & 1 deletion doc/toolbox-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ toolbox\-create - Create a new Toolbx container
## SYNOPSIS
**toolbox create** [*--authfile FILE*]
[*--distro DISTRO* | *-d DISTRO*]
[*--gid-map HOST_GID:CONTAINER_GID*]...
[*--image NAME* | *-i NAME*]
[*--release RELEASE* | *-r RELEASE*]
[*CONTAINER*]
Expand Down Expand Up @@ -115,6 +116,20 @@ If NAME does not contain a registry, the local image storage will be
consulted, and if it's not present there then it will be pulled from a suitable
remote registry.

**--gid-map** HOST_GID:CONTAINER_GID

Map the supplementary group HOST_GID from the host to CONTAINER_GID in the
Toolbx container. This option can be specified multiple times to map more than
one supplementary group. HOST_GID must belong to the user and must be delegated
to the user through `subgid(5)`. The mapping is set when the container is
created and cannot be changed afterwards.

Mappings can be persisted with the `gid_maps` option in
`$XDG_CONFIG_HOME/containers/toolbox.conf`, which is usually
`$HOME/.config/containers/toolbox.conf`. If at least one `--gid-map` option is
specified on the command line, those mappings replace the entire list from the
configuration file instead of being appended to it. See `toolbox.conf(5)`.

**--release** RELEASE, **-r** RELEASE

Create a Toolbx container for a different operating system RELEASE than the
Expand Down Expand Up @@ -150,4 +165,4 @@ $ toolbox create --authfile ~/auth.json --image registry.example.com/bar

`toolbox(1)`, `toolbox-init-container(1)`, `podman(1)`, `podman-create(1)`,
`podman-inspect(1)`, `podman-login(1)`, `podman-pull(1)`,
`containers-auth.json(5)`
`containers-auth.json(5)`, `subgid(5)`, `toolbox.conf(5)`
19 changes: 17 additions & 2 deletions doc/toolbox.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ If NAME does not contain a registry, the local image storage will be
consulted, and if it's not present there then it will be pulled from a suitable
remote registry.

**gid_maps** = ["HOST_GID:CONTAINER_GID", ...]

Map supplementary groups from the host to numerical group IDs in the Toolbx
container. Each mapping is specified as HOST_GID:CONTAINER_GID. HOST_GID must
belong to the user and must be delegated to the user through `subgid(5)`.
Values specified with the `--gid-map` command line option override the entire
list instead of being appended to it.

**release** = "RELEASE"

Create a Toolbx container for a different operating system RELEASE than the
Expand All @@ -45,7 +53,8 @@ Fields specified here can be overridden by any of the files below.
**$XDG_CONFIG_HOME/containers/toolbox.conf**

This is meant for user-specific changes. Fields specified here override any of
the files above.
the files above. If `XDG_CONFIG_HOME` is not set, the path is usually
`$HOME/.config/containers/toolbox.conf`.

## EXAMPLES

Expand All @@ -62,6 +71,12 @@ release = "36"
image = "registry.fedoraproject.org/fedora-toolbox:36"
```

### Map supplementary host groups into the container:
```
[general]
gid_maps = ["971:100000", "965:100001"]
```

## SEE ALSO

`toolbox(1)`, `toolbox-create(1)`
`toolbox(1)`, `toolbox-create(1)`, `subgid(5)`
157 changes: 149 additions & 8 deletions src/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -52,6 +53,7 @@ var (
authFile string
container string
distro string
gidMaps []string
image string
release string
}
Expand All @@ -65,6 +67,11 @@ var (
}
)

type gidMapping struct {
host uint32
container uint32
}

var createCmd = &cobra.Command{
Use: "create",
Short: "Create a new Toolbx container",
Expand Down Expand Up @@ -92,6 +99,11 @@ func init() {
"",
"Create a Toolbx container for a different operating system distribution than the host")

flags.StringArrayVar(&createFlags.gidMaps,
"gid-map",
nil,
"Map a supplementary host GID to a container GID as HOST_GID:CONTAINER_GID (repeatable)")

flags.StringVarP(&createFlags.image,
"image",
"i",
Expand Down Expand Up @@ -180,14 +192,28 @@ func create(cmd *cobra.Command, args []string) error {
return err
}

if err := createContainer(container, image, release, createFlags.authFile, true); err != nil {
gidMapValues := createFlags.gidMaps
if !cmd.Flag("gid-map").Changed {
gidMapValues = utils.GetGIDMappings()
}

gidMappings, err := parseGIDMappings(gidMapValues)
if err != nil {
return err
}

if err := validateGIDMappings(gidMappings); err != nil {
return err
}

if err := createContainer(container, image, release, createFlags.authFile, gidMappings, true); err != nil {
return err
}

return nil
}

func createContainer(container, image, release, authFile string, showCommandToEnter bool) error {
func createContainer(container, image, release, authFile string, gidMappings []gidMapping, showCommandToEnter bool) error {
if container == "" {
panic("container not specified")
}
Expand Down Expand Up @@ -286,11 +312,9 @@ func createContainer(container, image, release, authFile string, showCommandToEn
devPtsMount = []string{"--mount", "type=devpts,destination=/dev/pts"}
}

var usernsArg string
if currentUser.Uid == "0" {
usernsArg = "host"
} else {
usernsArg = "keep-id"
userNamespaceArgs, err := getUserNamespaceArgs(currentUser.Uid, currentUser.Gid, gidMappings)
if err != nil {
return err
}

dbusSystemSocket, err := getDBusSystemSocket()
Expand Down Expand Up @@ -454,9 +478,13 @@ func createContainer(container, image, release, authFile string, showCommandToEn
"--no-hosts",
"--pid", "host",
"--privileged",
}...)

createArgs = append(createArgs, userNamespaceArgs...)

createArgs = append(createArgs, []string{
"--security-opt", "label=disable",
"--ulimit", "host",
"--userns", usernsArg,
"--user", "root:root",
"--volume", "/:/run/host:rslave",
"--volume", "/dev:/dev:rslave",
Expand Down Expand Up @@ -508,6 +536,119 @@ func createContainer(container, image, release, authFile string, showCommandToEn
return nil
}

func parseGIDMappings(values []string) ([]gidMapping, error) {
const invalidGID = uint64(1<<32 - 1)

mappings := make([]gidMapping, 0, len(values))
hostGIDs := make(map[uint32]struct{}, len(values))
containerGIDs := make(map[uint32]struct{}, len(values))

for _, value := range values {
parts := strings.Split(value, ":")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("invalid GID mapping %q: expected HOST_GID:CONTAINER_GID", value)
}

hostGID64, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil || hostGID64 == invalidGID {
return nil, fmt.Errorf("invalid host GID %q in mapping %q", parts[0], value)
}

containerGID64, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil || containerGID64 == invalidGID {
return nil, fmt.Errorf("invalid container GID %q in mapping %q", parts[1], value)
}

hostGID := uint32(hostGID64)
containerGID := uint32(containerGID64)

if _, found := hostGIDs[hostGID]; found {
return nil, fmt.Errorf("host GID %d is mapped more than once", hostGID)
}
if _, found := containerGIDs[containerGID]; found {
return nil, fmt.Errorf("container GID %d is mapped more than once", containerGID)
}

hostGIDs[hostGID] = struct{}{}
containerGIDs[containerGID] = struct{}{}
mappings = append(mappings, gidMapping{host: hostGID, container: containerGID})
}

return mappings, nil
}

func validateGIDMappings(mappings []gidMapping) error {
if len(mappings) == 0 {
return nil
}

if currentUser.Uid == "0" {
return errors.New("option --gid-map is not supported when running Toolbx as root")
}

primaryGID64, err := strconv.ParseUint(currentUser.Gid, 10, 32)
if err != nil {
return fmt.Errorf("failed to parse primary GID %s", currentUser.Gid)
}
primaryGID := uint32(primaryGID64)

groupIDs, err := os.Getgroups()
if err != nil {
return errors.New("failed to get supplementary groups")
}
supplementaryGIDs := make(map[uint32]struct{}, len(groupIDs))
for _, groupID := range groupIDs {
if groupID >= 0 {
supplementaryGIDs[uint32(groupID)] = struct{}{}
}
}

for _, mapping := range mappings {
if mapping.host == primaryGID {
return fmt.Errorf("host GID %d is the primary GID and cannot be mapped as a supplementary group",
mapping.host)
}
if mapping.container == primaryGID {
return fmt.Errorf("container GID %d conflicts with the primary GID", mapping.container)
}
if _, found := supplementaryGIDs[mapping.host]; !found {
return fmt.Errorf("host GID %d is not a supplementary group of user %s",
mapping.host,
currentUser.Username)
}
}

return nil
}

func getUserNamespaceArgs(uid, gid string, gidMappings []gidMapping) ([]string, error) {
if len(gidMappings) == 0 {
userns := "keep-id"
if uid == "0" {
userns = "host"
}

return []string{"--userns", userns}, nil
}

if uid == "0" {
return nil, errors.New("GID mappings are not supported when running Toolbx as root")
}

args := []string{
"--uidmap", fmt.Sprintf("+u%s:0:1", uid),
"--gidmap", fmt.Sprintf("+g%s:0:1", gid),
}

for _, mapping := range gidMappings {
args = append(args,
"--gidmap", fmt.Sprintf("+g%d:@%d:1", mapping.container, mapping.host),
"--group-add", strconv.FormatUint(uint64(mapping.container), 10))
}

return args, nil
}

func createHelp(cmd *cobra.Command, args []string) {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func runCommand(container string,
return nil
}

if err := createContainer(container, image, release, "", false); err != nil {
if err := createContainer(container, image, release, "", nil, false); err != nil {
return err
}
} else if containersCount == 1 && defaultContainer {
Expand Down
5 changes: 5 additions & 0 deletions src/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,11 @@ func SetUpConfiguration() error {
return nil
}

// GetGIDMappings returns supplementary GID mappings from the merged configuration files.
func GetGIDMappings() []string {
return viper.GetStringSlice("general.gid_maps")
}

// ShortID shortens provided id to first 12 characters.
func ShortID(id string) string {
if len(id) > idTruncLength {
Expand Down