Skip to content

Commit

Permalink
Support subordinate user and group IDs on enterprise set-ups
Browse files Browse the repository at this point in the history
On enterprise FreeIPA set-ups, the subordinate user and group IDs are
provided by SSSD's sss plugin for the GNU Name Service Switch (or NSS)
functionality of the GNU C Library.  They are not listed in /etc/subuid
and /etc/subgid.

The CGO interaction with libsubid.so is loosely based on 'readSubid' in
github.com/containers/storage/pkg/idtools [1].

Unlike 'readSubid', this code considers the absence of any range (ie.,
nRanges == 0) to be an error as well.

More importantly, this code uses dlopen(3) and friends to dynamically
load the symbols from libsubid.so, instead of linking to libsubid.so at
build-time and having the dependency noted in the /usr/bin/toolbox
binary.  This is done because libsubid.so itself depends on several
other shared libraries, and indirect dependencies can't be influenced
by the RUNPATH [2] embedded in the /usr/bin/toolbox binary [3].  Hence,
when the binary is used inside Toolbx containers (eg., as the entry
point), those indirect dependencies won't be picked from the host's
runtime against which the binary was built.  This can render the binary
useless due to ABI compatibility issues.  Using dlopen(3) avoids this
problem, especially because libsubid.so is only used when running on the
host.

Care was taken to not load and link libsubid.so twice to separately
validate the subordinate ranges for the user and the group.  Sadly,
there doesn't seem to be a way to close the file descriptor that's used
by libsubid.so for logging.  Hence, this one file descriptor (currently
number 3, unless the parent process passes down others) pointing to
/dev/null is leaked for the life cycle of the toolbox process.

Version 4 of the libsubid.so API/ABI [4] was released in Shadow 4.10,
which is newer than the versions shipped on RHEL 8 and Debian 10 [5],
and even that newer version had some problems [6].  Therefore, support
for older versions, with the relevant workarounds, is necessary.

This code doesn't set the public variables Prog and shadow_logfd that
older Shadow versions used to expect for logging, because from Shadow
4.9 onwards there's a separate function [4,7] to specify these.  This
can be changed if there are libsubid.so versions in the wild that really
do need those public variables to be set.

Finally, ISO C99 is required because of the use of <stdbool.h> in the
libsubid.so API.

Some changes by Debarshi Ray.

[1] https://github.com/containers/storage/blob/main/pkg/idtools/idtools_supported.go

[2] https://man7.org/linux/man-pages/man8/ld.so.8.html

[3] Commit 6063eb2
    containers#821

[4] Shadow commit 32f641b207f6ddff
    shadow-maint/shadow@32f641b207f6ddff
    shadow-maint/shadow#443

[5] https://packages.debian.org/source/buster/shadow

[6] Shadow commit 79157cbad87f42cd
    shadow-maint/shadow@79157cbad87f42cd
    shadow-maint/shadow#465

[7] Shadow commit 2b22a6909dba60d
    shadow-maint/shadow@2b22a6909dba60d
    shadow-maint/shadow#325

containers#1074

Signed-off-by: Martin Jackson <[email protected]>
  • Loading branch information
Martin Jackson authored and debarshiray committed Dec 20, 2022
1 parent e789c16 commit d3a9f3e
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 47 deletions.
3 changes: 3 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ project(
'c',
version: '0.0.99.3',
license: 'ASL 2.0',
default_options: 'c_std=c99',
meson_version: '>= 0.58.0',
)

Expand All @@ -13,6 +14,8 @@ if not cc.has_argument('-print-file-name=libc.so')
error('C compiler does not support the -print-file-name argument.')
endif

subid_dep = cc.find_library('subid', has_headers: ['shadow/subid.h'])

go = find_program('go')
go_md2man = find_program('go-md2man')
podman = find_program('podman')
Expand Down
3 changes: 2 additions & 1 deletion playbooks/dependencies-centos-9-stream.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- ninja-build
- openssl
- podman
- shadow-utils-subid-devel
- skopeo
- systemd
- udisks2
Expand Down Expand Up @@ -50,7 +51,7 @@
chdir: '{{ zuul.project.src_dir }}'

- name: Check versions of crucial packages
command: rpm -qa ShellCheck codespell *kernel* *glibc* golang podman conmon containernetworking-plugins containers-common container-selinux crun fuse-overlayfs flatpak-session-helper
command: rpm -qa ShellCheck codespell *kernel* *glibc* golang shadow-utils-subid-devel podman conmon containernetworking-plugins containers-common container-selinux crun fuse-overlayfs flatpak-session-helper

- name: Show podman versions
command: podman version
Expand Down
3 changes: 2 additions & 1 deletion playbooks/dependencies-fedora.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- ninja-build
- openssl
- podman
- shadow-utils-subid-devel
- skopeo
- systemd
- udisks2
Expand All @@ -50,7 +51,7 @@
chdir: '{{ zuul.project.src_dir }}'

- name: Check versions of crucial packages
command: rpm -qa ShellCheck codespell *kernel* *glibc* golang podman conmon containernetworking-plugins containers-common container-selinux crun fuse-overlayfs flatpak-session-helper
command: rpm -qa ShellCheck codespell *kernel* *glibc* golang shadow-utils-subid-devel podman conmon containernetworking-plugins containers-common container-selinux crun fuse-overlayfs flatpak-session-helper

- name: Show podman versions
command: podman version
Expand Down
48 changes: 6 additions & 42 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package cmd

import (
"bufio"
"errors"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -141,17 +140,11 @@ func preRun(cmd *cobra.Command, args []string) error {
logrus.Debugf("Running on a cgroups v%d host", cgroupsVersion)

if currentUser.Uid != "0" {
logrus.Debugf("Checking if /etc/subgid and /etc/subuid have entries for user %s",
currentUser.Username)
logrus.Debugf("Looking for sub-GID and sub-UID ranges for user %s", currentUser.Username)

if _, err := validateSubIDFile("/etc/subuid"); err != nil {
logrus.Debugf("Validating sub-ID file /etc/subuid: %s", err)
return newSubIDFileError()
}

if _, err := validateSubIDFile("/etc/subgid"); err != nil {
logrus.Debugf("Validating sub-ID file /etc/subgid: %s", err)
return newSubIDFileError()
if _, err := utils.ValidateSubIDRanges(currentUser); err != nil {
logrus.Debugf("Validating sub-GID and sub-UID ranges: %s", err)
return newSubIDError()
}
}
}
Expand Down Expand Up @@ -321,9 +314,9 @@ func migrate() error {
return nil
}

func newSubIDFileError() error {
func newSubIDError() error {
var builder strings.Builder
fmt.Fprintf(&builder, "/etc/subgid and /etc/subuid don't have entries for user %s\n", currentUser.Username)
fmt.Fprintf(&builder, "Missing subgid and/or subuid ranges for user %s\n", currentUser.Username)
fmt.Fprintf(&builder, "See the podman(1), subgid(5), subuid(5) and usermod(8) manuals for more\n")
fmt.Fprintf(&builder, "information.")

Expand Down Expand Up @@ -393,32 +386,3 @@ func setUpLoggers() error {

return nil
}

func validateSubIDFile(path string) (bool, error) {
if utils.IsInsideContainer() {
panic("cannot validate sub-IDs inside container")
}

file, err := os.Open(path)
if err != nil {
return false, fmt.Errorf("failed to open: %w", err)
}

defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)

prefixes := []string{currentUser.Username + ":", currentUser.Uid + ":"}

for scanner.Scan() {
line := scanner.Text()
for _, prefix := range prefixes {
if strings.HasPrefix(line, prefix) {
return true, nil
}
}
}

return false, fmt.Errorf("failed to find an entry for user %s", currentUser.Username)
}
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sources = files(
'pkg/shell/shell.go',
'pkg/utils/errors.go',
'pkg/utils/utils.go',
'pkg/utils/utils_cgo.go',
'pkg/version/version.go',
)

Expand Down
140 changes: 140 additions & 0 deletions src/pkg/utils/utils_cgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright © 2019 – 2022 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package utils

import (
"errors"
"fmt"
"os/user"
"unsafe"
)

/*
#cgo LDFLAGS: -ldl
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <shadow/subid.h>
#if SUBID_ABI_MAJOR < 4
# define subid_get_gid_ranges get_subgid_ranges
# define subid_get_uid_ranges get_subuid_ranges
#endif
#define TOOLBOX_STRINGIFY_HELPER(s) #s
#define TOOLBOX_STRINGIFY(s) TOOLBOX_STRINGIFY_HELPER (s)
typedef bool (*ToolboxSubidInitFunc) (const char *progname, FILE *logfd);
typedef int (*ToolboxSubidGetRangesFunc) (const char *owner, struct subid_range **ranges);
const char *TOOLBOX_LIBSUBID = "libsubid.so." TOOLBOX_STRINGIFY (SUBID_ABI_VERSION);
const char *TOOLBOX_LIBSUBID_INIT = "libsubid_init";
const char *TOOLBOX_SUBID_INIT = "subid_init";
const char *TOOLBOX_SUBID_GET_GID_RANGES_SYMBOL = TOOLBOX_STRINGIFY (subid_get_gid_ranges);
const char *TOOLBOX_SUBID_GET_UID_RANGES_SYMBOL = TOOLBOX_STRINGIFY (subid_get_uid_ranges);
void
toolbox_subid_init (void *subid_init_func)
{
(* (ToolboxSubidInitFunc) subid_init_func) (NULL, NULL);
}
int
toolbox_subid_get_id_ranges (void *subid_get_id_ranges_func, const char *owner, struct subid_range **ranges)
{
int ret_val = 0;
ret_val = (* (ToolboxSubidGetRangesFunc) subid_get_id_ranges_func) (owner, ranges);
return ret_val;
}
*/
import "C"

func validateSubIDRange(user *user.User, libsubid unsafe.Pointer, cSubidGetIDRangesSymbol *C.char) (bool, error) {
subid_get_id_ranges := C.dlsym(libsubid, cSubidGetIDRangesSymbol)
if subid_get_id_ranges == nil {
subidGetIDRangesSymbol := C.GoString(cSubidGetIDRangesSymbol)
return false, fmt.Errorf("cannot dlsym(3) %s", subidGetIDRangesSymbol)
}

cUsername := C.CString(user.Username)
defer C.free(unsafe.Pointer(cUsername))

var cRanges *C.struct_subid_range
defer C.free(unsafe.Pointer(cRanges))

nRanges := C.toolbox_subid_get_id_ranges(subid_get_id_ranges, cUsername, &cRanges)
if nRanges <= 0 {
cUid := C.CString(user.Uid)
defer C.free(unsafe.Pointer(cUid))

nRanges = C.toolbox_subid_get_id_ranges(subid_get_id_ranges, cUid, &cRanges)
}

if nRanges <= 0 {
return false, errors.New("cannot read subids")
}

return true, nil
}

func ValidateSubIDRanges(user *user.User) (bool, error) {
if IsInsideContainer() {
panic("cannot validate subordinate IDs inside container")
}

if user == nil {
panic("cannot validate subordinate IDs when user is nil")
}

if user.Username == "ALL" {
return false, errors.New("username ALL not supported")
}

libsubid := C.dlopen(C.TOOLBOX_LIBSUBID, C.RTLD_LAZY)
if libsubid == nil {
filename := C.GoString(C.TOOLBOX_LIBSUBID)
return false, fmt.Errorf("cannot dlopen(3) %s", filename)
}

defer C.dlclose(libsubid)

subid_init := C.dlsym(libsubid, C.TOOLBOX_SUBID_INIT)
if subid_init == nil {
subid_init = C.dlsym(libsubid, C.TOOLBOX_LIBSUBID_INIT)
}

if subid_init != nil {
C.toolbox_subid_init(subid_init)
}

if _, err := validateSubIDRange(user, libsubid, C.TOOLBOX_SUBID_GET_GID_RANGES_SYMBOL); err != nil {
return false, err
}

if _, err := validateSubIDRange(user, libsubid, C.TOOLBOX_SUBID_GET_UID_RANGES_SYMBOL); err != nil {
return false, err
}

return true, nil
}
6 changes: 3 additions & 3 deletions test/system/104-run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ teardown() {
create_default_container

# File descriptors 3 and 4 are reserved by Bats.
run --separate-stderr $TOOLBOX run --preserve-fds 3 readlink /proc/self/fd/5 5>/dev/null
run --separate-stderr $TOOLBOX run --preserve-fds 3 readlink /proc/self/fd/6 6>/dev/null

assert_success
assert_line --index 0 "/dev/null"
Expand Down Expand Up @@ -336,12 +336,12 @@ teardown() {
create_default_container

# File descriptors 3 and 4 are reserved by Bats.
run -125 --separate-stderr $TOOLBOX run --preserve-fds 3 readlink /proc/self/fd/5
run -125 --separate-stderr $TOOLBOX run --preserve-fds 3 readlink /proc/self/fd/6

assert_failure
assert [ ${#lines[@]} -eq 0 ]
lines=("${stderr_lines[@]}")
assert_line --index 0 "Error: file descriptor 5 is not available - the preserve-fds option requires that file descriptors must be passed"
assert_line --index 0 "Error: file descriptor 6 is not available - the preserve-fds option requires that file descriptors must be passed"
assert_line --index 1 "Error: failed to invoke 'podman exec' in container $default_container_name"
assert [ ${#stderr_lines[@]} -eq 2 ]
}
Expand Down

0 comments on commit d3a9f3e

Please sign in to comment.