From 86a355ce42a71f183e3b45fe741da24c97f6c941 Mon Sep 17 00:00:00 2001 From: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:38:24 +0200 Subject: [PATCH 1/3] expose podman gidmap support Signed-off-by: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> --- doc/toolbox-create.1.md | 10 ++- src/cmd/create.go | 152 +++++++++++++++++++++++++++++++++++++--- src/cmd/run.go | 2 +- 3 files changed, 154 insertions(+), 10 deletions(-) diff --git a/doc/toolbox-create.1.md b/doc/toolbox-create.1.md index f338dd824..f1e82a60f 100644 --- a/doc/toolbox-create.1.md +++ b/doc/toolbox-create.1.md @@ -115,6 +115,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-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. + **--release** RELEASE, **-r** RELEASE Create a Toolbx container for a different operating system RELEASE than the @@ -150,4 +158,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)` diff --git a/src/cmd/create.go b/src/cmd/create.go index bdd86be37..5959d0720 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "time" @@ -52,6 +53,7 @@ var ( authFile string container string distro string + gidMaps []string image string release string } @@ -65,6 +67,11 @@ var ( } ) +type gidMapping struct { + host uint32 + container uint32 +} + var createCmd = &cobra.Command{ Use: "create", Short: "Create a new Toolbx container", @@ -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", @@ -180,14 +192,23 @@ func create(cmd *cobra.Command, args []string) error { return err } - if err := createContainer(container, image, release, createFlags.authFile, true); err != nil { + gidMappings, err := parseGIDMappings(createFlags.gidMaps) + 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") } @@ -286,11 +307,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() @@ -454,9 +473,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", @@ -508,6 +531,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() { diff --git a/src/cmd/run.go b/src/cmd/run.go index ed421aa68..644dd66a5 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -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 { From 199aa765477176eef6b6076b27b81e53a2b8eb03 Mon Sep 17 00:00:00 2001 From: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:38:24 +0200 Subject: [PATCH 2/3] add persistent configuration of gidmaps Signed-off-by: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> --- data/config/toolbox.conf | 5 +++++ doc/toolbox.conf.5.md | 16 +++++++++++++++- src/cmd/create.go | 7 ++++++- src/pkg/utils/utils.go | 5 +++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/data/config/toolbox.conf b/data/config/toolbox.conf index 67cbeaaca..3a66b1344 100644 --- a/data/config/toolbox.conf +++ b/data/config/toolbox.conf @@ -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"] diff --git a/doc/toolbox.conf.5.md b/doc/toolbox.conf.5.md index 67612a034..09679bdc9 100644 --- a/doc/toolbox.conf.5.md +++ b/doc/toolbox.conf.5.md @@ -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. + **release** = "RELEASE" Create a Toolbx container for a different operating system RELEASE than the @@ -62,6 +70,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)` diff --git a/src/cmd/create.go b/src/cmd/create.go index 5959d0720..6b369e79c 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -192,7 +192,12 @@ func create(cmd *cobra.Command, args []string) error { return err } - gidMappings, err := parseGIDMappings(createFlags.gidMaps) + gidMapValues := createFlags.gidMaps + if !cmd.Flag("gid-map").Changed { + gidMapValues = utils.GetGIDMappings() + } + + gidMappings, err := parseGIDMappings(gidMapValues) if err != nil { return err } diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 790f08423..bef4cf669 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -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 { From da4a688975e46642939b2046a9be3de9d18d00ab Mon Sep 17 00:00:00 2001 From: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:38:45 +0200 Subject: [PATCH 3/3] add docs Signed-off-by: Max Beckmann <37029778+maxbeckmann@users.noreply.github.com> --- doc/toolbox-create.1.md | 9 ++++++++- doc/toolbox.conf.5.md | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/toolbox-create.1.md b/doc/toolbox-create.1.md index f1e82a60f..760902aa4 100644 --- a/doc/toolbox-create.1.md +++ b/doc/toolbox-create.1.md @@ -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*] @@ -123,6 +124,12 @@ 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 @@ -158,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)`, `subgid(5)` +`containers-auth.json(5)`, `subgid(5)`, `toolbox.conf(5)` diff --git a/doc/toolbox.conf.5.md b/doc/toolbox.conf.5.md index 09679bdc9..ff569d248 100644 --- a/doc/toolbox.conf.5.md +++ b/doc/toolbox.conf.5.md @@ -32,7 +32,7 @@ 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. +list instead of being appended to it. **release** = "RELEASE" @@ -53,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