diff --git a/TODO b/TODO index 2ca2a8d50f..0f9b28ab49 100644 --- a/TODO +++ b/TODO @@ -33,7 +33,8 @@ Features: systemd-journald writes to /var/log/journal, which could be useful when we doing disk usage calculations and so on. -* taint systemd if the overflowuid/overflowgid is not 65534 +* taint systemd if the overflowuid/overflowgid is not 65534, and if there are + fewer than 65536 users assigned to the system. * deprecate PermissionsStartOnly= and RootDirectoryStartOnly= in favour of the ExecStart= prefix chars @@ -77,16 +78,9 @@ Features: * beef up pam_systemd to take unit file settings such as cgroups properties as parameters -* export UID ranges nspawns's --private-user and DynamicUser= uses in - the systemd.pc pkg-config file, the same way we already expose the system - user boundary there - * a new "systemd-analyze security" tool outputting a checklist of security features a service does and does not implement -* Whenever we check a UID against the system UID range, also check for the - dynamic UID range - * maybe hook of xfs/ext4 quotactl() with services? i.e. automatically manage the quota of a the user indicated in User= via unit file settings, like the other resource management concepts. Would mix nicely with DynamicUser=1. Or diff --git a/UIDS-GIDS.md b/UIDS-GIDS.md new file mode 100644 index 0000000000..d44d93d144 --- /dev/null +++ b/UIDS-GIDS.md @@ -0,0 +1,242 @@ +# Users, Groups, UIDs and GIDs on `systemd` systems + +Here's a summary of the requirements `systemd` (and Linux) make on UID/GID +assignments and their ranges. + +Note that while in theory UIDs and GIDs are orthogonal concepts they really +aren't IRL. With that in mind, when we discuss UIDs below it should be assumed +that whatever we say about UIDs applies to GIDs in mostly the same way, and all +the special assignments and ranges for UIDs always have mostly the same +validity for GIDs too. + +## Special Linux UIDs + +In theory, the range of the C type `uid_t` is 32bit wide on Linux, +i.e. 0…4294967295. However, four UIDs are special on Linux: + +1. 0 → The `root` super-user + +2. 65534 → The `nobody` UID, also called the "overflow" UID or similar. It's + where various subsystems map unmappable users to, for example NFS or user + namespacing. (The latter can be changed with a sysctl during runtime, but + that's not supported on `systemd`. If you do change it you void your + warranty.) Because Fedora is a bit confused the `nobody` user is called + `nfsnobody` there (and they have a different `nobody` user at UID 99). I + hope this will be corrected eventually though. (Also, some distributions + call the `nobody` group `nogroup`. I wish they didn't.) + +3. 4294967295, aka "32bit `(uid_t) -1`" → This UID is not a valid user ID, as + setresuid(), chown() and friends treat -1 as a special request to not change + the UID of the process/file. This UID is hence not available for assignment + to users in the user database. + +4. 65535, aka "16bit `(uid_t) -1`" → Once upon a time `uid_t` used to be 16bit, and + programs compiled for that would hence assume that `(uid_t) -1` is 65535. This + UID is hence not usable either. + +The `nss-systemd` glibc NSS module will synthesize user database records for +the UIDs 0 and 65534 if the system user database doesn't list them. This means +that any system where this module is enabled works to some minimal level +without `/etc/passwd`. + +## Special Distribution UID ranges + +Distributions generally split the available UID range in two: + +1. 1…999 → System users. These are users that do not map to actual "human" + users, but are used as security identities for system daemons, to implement + privilege separation and run system daemons with minimal privileges. + +2. 1000…65533 and 65536…4294967294 → Everything else, i.e. regular (human) users. + +Note that most distributions allow changing the boundary between system and +regular users, even during runtime as user configuration. Moreover, some older +systems placed the boundary at 499/500, or even 99/100. In `systemd`, the +boundary is configurable only during compilation time, as this should be a +decision for distribution builders, not for users. Moreover, we strongly +discourage downstreams to change the boundary from the upstream default of +999/1000. + +Also note that programs such as `adduser` tend to allocate from a subset of the +available regular user range only, usually 1000..60000. And it's also usually +user-configurable, too. + +Note that systemd requires that system users and groups are resolvable without +networking available — a requirement that is not made for regular users. This +means regular users may be stored in remote LDAP or NIS databases, but system +users may not (except when there's a consistent local cache kept, that is +available during earliest boot, including in the initial RAM disk). + +## Special `systemd` GIDs + +`systemd` defines no special UIDs beyond what Linux already defines (see +above). However, it does define some special group/GID assignments, which are +primarily used for `systemd-udevd`'s device management. The precise list of the +currently defined groups is found in this `sysusers.d` snippet: +[basic.conf](https://raw.githubusercontent.com/systemd/systemd/master/sysusers.d/basic.conf.in) + +It's strongly recommended that downstream distributions include these groups in +their default group databases. + +Note that the actual GID numbers assigned to these groups do not have to be +constant beyond a specific system. There's one exception however: the `tty` +group must have the GID 5. That's because it must be encoded in the `devpts` +mount parameters during earliest boot, at a time where NSS lookups are not +possible. (Note that the actual GID can be changed during `systemd` build time, +but downstreams are strongly advised against doing that.) + +## Special `systemd` UID ranges + +`systemd` defines a number of special UID ranges: + +1. 61184…65519 → UIDs for dynamic users are allocated from this range (see the + `DynamicUser=` documentation in + [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)). This + range has been chosen so that it is below the 16bit boundary (i.e. below + 65535), in order to provide compatibility with container environments that + assign a 64K range of UIDs to containers using user namespacing. This range + is above the 60000 boundary, so that its allocations are unlikely to be + affected by `adduser` allocations (see above). And we leave some room + upwards for other purposes. (And if you wonder why precisely these numbers: + if you write them in hexadecimal, they might make more sense: 0xEF00 and + 0xFFEF). The `nss-systemd` module will synthesize user records implicitly + for all currently allocated dynamic users from this range. Thus, NSS-based + user record resolving works correctly without those users being in + `/etc/passwd`. + +2. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of + per-container UID ranges. When the `--private-users=pick` switch is used (or + `-U`) then it will automatically find a so far unused 16bit subrange of this + range and assign it to the container. The range is picked so that the upper + 16bit of the 32bit UIDs are constant for all users of the container, while + the lower 16bit directly encode the 65536 UIDs assigned to the + container. This mode of allocation means that the upper 16bit of any UID + assigned to a container are kind of a "container ID", while the lower 16bit + directly expose the container's own UID numbers. If you wonder why precisely + these numbers, consider them in hexadecimal: 0x00080000…0x6FFFFFFF. This + range is above the 16bit boundary. Moreover it's below the 31bit boundary, + as some broken code (specifically: the kernel's `devpts` file system) + erroneously considers UIDs signed integers, and hence can't deal with values + above 2^31. The `nss-mymachines` glibc NSS module will synthesize user + database records for all UIDs assigned to a running container from this + range. + +Note for both allocation ranges: when an UID allocation takes place NSS is +checked for collisions first, and a different UID is picked if an entry is +found. Thus, the user database is used as synchronization mechanism to ensure +exclusive ownership of UIDs and UID ranges. To ensure compatibility with other +subsystems allocating from the same ranges it is hence essential that they +ensure that whatever they pick shows up in the user/group databases, either by +providing an NSS module, or by adding entries directly to `/etc/passwd` and +`/etc/group`. For performance reasons, do note that `systemd-nspawn` will only +do an NSS check for the first UID of the range it allocates, not all 65536 of +them. Also note that while the allocation logic is operating, the glibc +`lckpwdf()` user database lock is taken, in order to make this logic race-free. + +## Figuring out the system's UID boundaries + +The most important boundaries of the local system may be queried with +`pkg-config`: + +``` +$ pkg-config --variable=systemuidmax systemd +999 +$ pkg-config --variable=dynamicuidmin systemd +61184 +$ pkg-config --variable=dynamicuidmax systemd +65519 +$ pkg-config --variable=containeruidbasemin systemd +524288 +$ pkg-config --variable=containeruidbasemax systemd +1878982656 +``` + +(Note that the latter encodes the maximum UID *base* `systemd-nspawn` might +pick — given that 64K UIDs are assigned to each container according to this +allocation logic, the maximum UID used for this range is hence +1878982656+65535=1879048191.) + +Note that systemd does not make any of these values runtime-configurable. All +these boundaries are chosen during build time. That said, the system UID/GID +boundary is traditionally configured in /etc/login.defs, though systemd won't +look there during runtime. + +## Considerations for container managers + +If you hack on a container manager, and wonder how and how many UIDs best to +assign to your containers, here are a few recommendations: + +1. Definitely, don't assign less than 65536 UIDs/GIDs. After all the `nobody` +user has magic properties, and hence should be available in your container, and +given that it's assigned the UID 65534, you should really cover the full 16bit +range in your container. Note that systemd will — as mentioned — synthesize +user records for the `nobody` user, and assumes its availability in various +other parts of its codebase, too, hence assigning fewer users means you lose +compatibility with running systemd code inside your container. And most likely +other packages make similar restrictions. + +2. While it's fine to assign more than 65536 UIDs/GIDs to a container, there's +most likely not much value in doing so, as Linux distributions won't use the +higher ranges by default (as mentioned neither `adduser` nor `systemd`'s +dynamic user concept allocate from above the 16bit range). Unless you actively +care for nested containers, it's hence probably a good idea to allocate exactly +65536 UIDs per container, and neither less nor more. A pretty side-effect is +that by doing so, you expose the same number of UIDs per container as Linux 2.2 +supported for the whole system, back in the days. + +3. Consider allocating UID ranges for containers so that the first UID you +assign has the lower 16bits all set to zero. That way, the upper 16bits become +a container ID of some kind, while the lower 16bits directly encode the +internal container UID. This is the way `systemd-nspawn` allocates UID ranges +(see above). Following this allocation logic ensures best compability with +`systemd-nspawn` and all other container managers following the scheme, as it +is sufficient then to check NSS for the first UID you pick regarding conflicts, +as that's what they do, too. Moreover, it makes `chown()`ing container file +system trees nicely robust to interruptions: as the external UID encodes the +internal UID in a fixed way, it's very easy to adjust the container's base UID +without the need to know the original base UID: to change the container base, +just mask away the upper 16bit, and insert the upper 16bit of the new container +base instead. Here are the easy conversions to derive the internal UID, the +external UID, and the container base UID from each other: + + ``` + INTERNAL_UID = EXTERNAL_UID & 0x0000FFFF + CONTAINER_BASE_UID = EXTERNAL_UID & 0xFFFF0000 + EXTERNAL_UID = INTERNAL_UID | CONTAINER_BASE_UID + ``` + +4. When picking a UID range for containers, make sure to check NSS first, with +a simple `getpwuid()` call: if there's already a user record for the first UID +you want to pick, then it's already in use: pick a different one. Wrap that +call in a `lckpwdf()` + `ulckpwdf()` pair, to make allocation +race-free. Provide an NSS module that makes all UIDs you end up taking show up +in the user database, and make sure that the NSS module returns up-to-date +information before you release the lock, so that other system components can +safely use the NSS user database as allocation check, too. Note that if you +follow this scheme no changes to `/etc/passwd` need to be made, thus minimizing +the artifacts the container manager persistently leaves in the system. + +## Summary + +| UID/GID | Purpose | Defined By | Listed in | +|-----------------------|-----------------------|---------------|-------------------------------| +| 0 | `root` user | Linux | `/etc/passwd` + `nss-systemd` | +| 1…4 | System users | Distributions | `/etc/passwd` | +| 5 | `tty` group | `systemd` | `/etc/passwd` | +| 6…999 | System users | Distributions | `/etc/passwd` | +| 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | +| 60001…61183 | Unused | | | +| 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` | +| 65520…65533 | Unused | | | +| 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | +| 65535 | 16bit `(uid_t) -1` | Linux | | +| 65536…524287 | Unused | | | +| 524288…1879048191 | Container UID ranges | `systemd` | `nss-mymachines` | +| 1879048192…4294967294 | Unused | | | +| 4294967295 | 32bit `(uid_t) -1` | Linux | | + +Note that "Unused" in the table above doesn't meant that these ranges are +really unused. It just means that these ranges have no well-established +pre-defined purposes between Linux, generic low-level distributions and +`systemd`. There might very well be other packages that allocate from these +ranges. diff --git a/meson.build b/meson.build index edfa02afbb..eda74c4765 100644 --- a/meson.build +++ b/meson.build @@ -614,9 +614,6 @@ conf.set('SYSTEM_UID_MAX', system_uid_max) substs.set('systemuidmax', system_uid_max) message('maximum system UID is @0@'.format(system_uid_max)) -conf.set_quoted('NOBODY_USER_NAME', get_option('nobody-user')) -conf.set_quoted('NOBODY_GROUP_NAME', get_option('nobody-group')) - system_gid_max = get_option('system-gid-max') if system_gid_max == '' system_gid_max = run_command( @@ -629,6 +626,64 @@ conf.set('SYSTEM_GID_MAX', system_gid_max) substs.set('systemgidmax', system_gid_max) message('maximum system GID is @0@'.format(system_gid_max)) +dynamic_uid_min = get_option('dynamic-uid-min').to_int() +dynamic_uid_max = get_option('dynamic-uid-max').to_int() +conf.set('DYNAMIC_UID_MIN', dynamic_uid_min) +conf.set('DYNAMIC_UID_MAX', dynamic_uid_max) +substs.set('dynamicuidmin', dynamic_uid_min) +substs.set('dynamicuidmax', dynamic_uid_max) + +container_uid_base_min = get_option('container-uid-base-min').to_int() +container_uid_base_max = get_option('container-uid-base-max').to_int() +conf.set('CONTAINER_UID_BASE_MIN', container_uid_base_min) +conf.set('CONTAINER_UID_BASE_MAX', container_uid_base_max) +substs.set('containeruidbasemin', container_uid_base_min) +substs.set('containeruidbasemax', container_uid_base_max) + +nobody_user = get_option('nobody-user') +nobody_group = get_option('nobody-group') + +getent_result = run_command('getent', 'passwd', '65534') +if getent_result.returncode() == 0 + name = getent_result.stdout().split(':')[0] + if name != nobody_user + message('WARNING:\n' + + ' The local user with the UID 65534 does not match the configured user name "@0@" of the nobody user (its name is @1@).\n'.format(nobody_user, name) + + ' Your build will result in an user table setup that is incompatible with the local system.') + endif +endif +id_result = run_command('id', '-u', nobody_user) +if id_result.returncode() == 0 + id = id_result.stdout().to_int() + if id != 65534 + message('WARNING:\n' + + ' The local user with the configured user name "@0@" of the nobody user does not have UID 65534 (it has @1@).\n'.format(nobody_user, id) + + ' Your build will result in an user table setup that is incompatible with the local system.') + endif +endif + +getent_result = run_command('getent', 'group', '65534') +if getent_result.returncode() == 0 + name = getent_result.stdout().split(':')[0] + if name != nobody_group + message('WARNING:\n' + + ' The local group with the GID 65534 does not match the configured group name "@0@" of the nobody group (its name is @1@).\n'.format(nobody_group, name) + + ' Your build will result in an group table setup that is incompatible with the local system.') + endif +endif +id_result = run_command('id', '-g', nobody_group) +if id_result.returncode() == 0 + id = id_result.stdout().to_int() + if id != 65534 + message('WARNING:\n' + + ' The local group with the configured group name "@0@" of the nobody group does not have UID 65534 (it has @1@).\n'.format(nobody_group, id) + + ' Your build will result in an group table setup that is incompatible with the local system.') + endif +endif + +conf.set_quoted('NOBODY_USER_NAME', nobody_user) +conf.set_quoted('NOBODY_GROUP_NAME', nobody_group) + tty_gid = get_option('tty-gid') conf.set('TTY_GID', tty_gid) substs.set('TTY_GID', tty_gid) @@ -2506,12 +2561,16 @@ status = [ 'users GID: @0@'.format(users_gid), 'maximum system UID: @0@'.format(system_uid_max), 'maximum system GID: @0@'.format(system_gid_max), + 'minimum dynamic UID: @0@'.format(dynamic_uid_min), + 'maximum dynamic UID: @0@'.format(dynamic_uid_max), + 'minimum container UID base: @0@'.format(container_uid_base_min), + 'maximum container UID base: @0@'.format(container_uid_base_max), '/dev/kvm access mode: @0@'.format(get_option('dev-kvm-mode')), 'render group access mode: @0@'.format(get_option('group-render-mode')), 'certificate root directory: @0@'.format(get_option('certificate-root')), 'support URL: @0@'.format(support_url), - 'nobody user name: @0@'.format(get_option('nobody-user')), - 'nobody group name: @0@'.format(get_option('nobody-group')), + 'nobody user name: @0@'.format(nobody_user), + 'nobody group name: @0@'.format(nobody_group), 'fallback hostname: @0@'.format(get_option('fallback-hostname')), 'symbolic gateway hostnames: @0@'.format(', '.join(gateway_hostnames)), diff --git a/meson_options.txt b/meson_options.txt index 8c03b0c6de..f0c0506ff1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -147,6 +147,18 @@ option('system-uid-max', type : 'string', description : 'maximum system UID') option('system-gid-max', type : 'string', description : 'maximum system GID') +option('dynamic-uid-min', type : 'string', + description : 'minimum dynamic UID', + value : '61184') # That's → 0x0000EF00 in hex +option('dynamic-uid-max', type : 'string', + description : 'maximum dynamic UID', + value : '65519') # That's → 0x0000FFEF in hex +option('container-uid-base-min', type : 'string', + description : 'minimum container UID base', + value : '524288') # That's → 0x00080000 in hex +option('container-uid-base-max', type : 'string', + description : 'maximum container UID base', + value : '1878982656') # That's → 0x6FFF0000 in hex option('tty-gid', type : 'string', description : 'the numeric GID of the "tty" group', value : '5') diff --git a/mkosi.build b/mkosi.build index c2693dedcf..0781f0d9c4 100755 --- a/mkosi.build +++ b/mkosi.build @@ -28,7 +28,41 @@ export LC_CTYPE=en_US.UTF-8 sysvinit_path=`realpath /etc/init.d` -[ -f "$BUILDDIR"/build.ninja ] || meson "$BUILDDIR" -D "sysvinit-path=$sysvinit_path" -D default-hierarchy=unified -D man=false +nobody_user=`id -u -n 65534 2> /dev/null` +if [ "$nobody_user" != "" ] ; then + # Validate that we can translate forth and back + if [ "`id -u $nobody_user`" != 65534 ] ; then + nobody_user="" + fi +fi +if [ "$nobody_user" = "" ] ; then + if id -u nobody 2> /dev/null ; then + # The "nobody" user is defined already for something else, pick the Fedora name + nobody_user=nfsnobody + else + # The "nobody" user name is free, use it + nobody_user=nobody + fi +fi + +nobody_group=`id -g -n 65534 2> /dev/null` +if [ "$nobody_group" != "" ] ; then + # Validate that we can translate forth and back + if [ "`id -g $nobody_group`" != 65534 ] ; then + nobody_group="" + fi +fi +if [ "$nobody_group" = "" ] ; then + if id -u nobody 2> /dev/null ; then + # The "nobody" group is defined already for something else, pick the Fedora name + nobody_group=nfsnobody + else + # The "nobody" group name is free, use it + nobody_group=nobody + fi +fi + +[ -f "$BUILDDIR"/build.ninja ] || meson "$BUILDDIR" -D "sysvinit-path=$sysvinit_path" -D default-hierarchy=unified -D man=false -D "nobody-user=$nobody_user" -D "nobody-group=$nobody_group" ninja -C "$BUILDDIR" all [ "$WITH_TESTS" = 0 ] || ninja -C "$BUILDDIR" test || ( RET="$?" ; cat "$BUILDDIR"/meson-logs/testlog.txt ; exit "$RET" ) ninja -C "$BUILDDIR" install diff --git a/src/basic/user-util.c b/src/basic/user-util.c index ed54578a66..abb0b76866 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -117,15 +117,14 @@ int get_user_creds( assert(username); assert(*username); - /* We enforce some special rules for uid=0: in order to avoid - * NSS lookups for root we hardcode its data. */ + /* We enforce some special rules for uid=0 and uid=65534: in order to avoid NSS lookups for root we hardcode + * their user record data. */ - if (streq(*username, "root") || streq(*username, "0")) { + if (STR_IN_SET(*username, "root", "0")) { *username = "root"; if (uid) *uid = 0; - if (gid) *gid = 0; @@ -138,6 +137,23 @@ int get_user_creds( return 0; } + if (STR_IN_SET(*username, NOBODY_USER_NAME, "65534")) { + *username = NOBODY_USER_NAME; + + if (uid) + *uid = UID_NOBODY; + if (gid) + *gid = GID_NOBODY; + + if (home) + *home = "/"; + + if (shell) + *shell = "/sbin/nologin"; + + return 0; + } + if (parse_uid(*username, &u) >= 0) { errno = 0; p = getpwuid(u); @@ -218,7 +234,7 @@ int get_group_creds(const char **groupname, gid_t *gid) { /* We enforce some special rules for gid=0: in order to avoid * NSS lookups for root we hardcode its data. */ - if (streq(*groupname, "root") || streq(*groupname, "0")) { + if (STR_IN_SET(*groupname, "root", "0")) { *groupname = "root"; if (gid) @@ -227,6 +243,15 @@ int get_group_creds(const char **groupname, gid_t *gid) { return 0; } + if (STR_IN_SET(*groupname, NOBODY_GROUP_NAME, "65534")) { + *groupname = NOBODY_GROUP_NAME; + + if (gid) + *gid = GID_NOBODY; + + return 0; + } + if (parse_gid(*groupname, &id) >= 0) { errno = 0; g = getgrgid(id); @@ -258,6 +283,8 @@ char* uid_to_name(uid_t uid) { /* Shortcut things to avoid NSS lookups */ if (uid == 0) return strdup("root"); + if (uid == UID_NOBODY) + return strdup(NOBODY_USER_NAME); if (uid_is_valid(uid)) { long bufsize; @@ -296,6 +323,8 @@ char* gid_to_name(gid_t gid) { if (gid == 0) return strdup("root"); + if (gid == GID_NOBODY) + return strdup(NOBODY_GROUP_NAME); if (gid_is_valid(gid)) { long bufsize; @@ -387,7 +416,7 @@ int get_home_dir(char **_h) { return 0; } - /* Hardcode home directory for root to avoid NSS */ + /* Hardcode home directory for root and nobody to avoid NSS */ u = getuid(); if (u == 0) { h = strdup("/root"); @@ -397,6 +426,14 @@ int get_home_dir(char **_h) { *_h = h; return 0; } + if (u == UID_NOBODY) { + h = strdup("/"); + if (!h) + return -ENOMEM; + + *_h = h; + return 0; + } /* Check the database... */ errno = 0; @@ -434,7 +471,7 @@ int get_shell(char **_s) { return 0; } - /* Hardcode home directory for root to avoid NSS */ + /* Hardcode shell for root and nobody to avoid NSS */ u = getuid(); if (u == 0) { s = strdup("/bin/sh"); @@ -444,6 +481,14 @@ int get_shell(char **_s) { *_s = s; return 0; } + if (u == UID_NOBODY) { + s = strdup("/sbin/nologin"); + if (!s) + return -ENOMEM; + + *_s = s; + return 0; + } /* Check the database... */ errno = 0; diff --git a/src/basic/user-util.h b/src/basic/user-util.h index f13367893e..79adf91ee9 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -60,17 +60,25 @@ int take_etc_passwd_lock(const char *root); #define UID_INVALID ((uid_t) -1) #define GID_INVALID ((gid_t) -1) -/* Let's pick a UIDs within the 16bit range, so that we are compatible with containers using 16bit - * user namespacing. At least on Fedora normal users are allocated until UID 60000, hence do not - * allocate from below this. Also stay away from the upper end of the range as that is often used - * for overflow/nobody users. */ -#define DYNAMIC_UID_MIN ((uid_t) UINT32_C(0x0000EF00)) -#define DYNAMIC_UID_MAX ((uid_t) UINT32_C(0x0000FFEF)) +#define UID_NOBODY ((uid_t) 65534U) +#define GID_NOBODY ((gid_t) 65534U) static inline bool uid_is_dynamic(uid_t uid) { return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX; } +static inline bool gid_is_dynamic(gid_t gid) { + return uid_is_dynamic((uid_t) gid); +} + +static inline bool uid_is_system(uid_t uid) { + return uid <= SYSTEM_UID_MAX; +} + +static inline bool gid_is_system(gid_t gid) { + return gid <= SYSTEM_GID_MAX; +} + /* The following macros add 1 when converting things, since UID 0 is a valid UID, while the pointer * NULL is special */ #define PTR_TO_UID(p) ((uid_t) (((uintptr_t) (p))-1)) diff --git a/src/core/systemd.pc.in b/src/core/systemd.pc.in index 22e8cbda8b..655773ea8a 100644 --- a/src/core/systemd.pc.in +++ b/src/core/systemd.pc.in @@ -29,6 +29,10 @@ modulesloaddir=@modulesloaddir@ catalogdir=@catalogdir@ systemuidmax=@systemuidmax@ systemgidmax=@systemgidmax@ +dynamicuidmin=@dynamicuidmin@ +dynamicuidmax=@dynamicuidmax@ +containeruidbasemin=@containeruidbasemin@ +containeruidbasemax=@containeruidbasemax@ Name: systemd Description: systemd System and Service Manager diff --git a/src/coredump/coredump.c b/src/coredump/coredump.c index d3533790a1..ead04d4ee1 100644 --- a/src/coredump/coredump.c +++ b/src/coredump/coredump.c @@ -165,7 +165,7 @@ static int fix_acl(int fd, uid_t uid) { assert(fd >= 0); - if (uid <= SYSTEM_UID_MAX) + if (uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY) return 0; /* Make sure normal users can read (but not write or delete) diff --git a/src/journal/journald-server.c b/src/journal/journald-server.c index 46bf2eb310..3da3dad230 100644 --- a/src/journal/journald-server.c +++ b/src/journal/journald-server.c @@ -248,7 +248,7 @@ static void server_add_acls(JournalFile *f, uid_t uid) { assert(f); #if HAVE_ACL - if (uid <= SYSTEM_UID_MAX) + if (uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY) return; r = add_acls_for_user(f->fd, uid); @@ -406,7 +406,7 @@ static JournalFile* find_journal(Server *s, uid_t uid) { if (s->runtime_journal) return s->runtime_journal; - if (uid <= SYSTEM_UID_MAX || uid_is_dynamic(uid)) + if (uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY) return s->system_journal; r = sd_id128_get_machine(&machine); diff --git a/src/login/logind-user.c b/src/login/logind-user.c index 43a9f58adc..01469438b1 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -617,7 +617,7 @@ int user_finalize(User *u) { * cases, as we shouldn't accidentally remove a system service's IPC objects while it is running, just because * a cronjob running as the same user just finished. Hence: exclude system users generally from IPC clean-up, * and do it only for normal users. */ - if (u->manager->remove_ipc && u->uid > SYSTEM_UID_MAX) { + if (u->manager->remove_ipc && !uid_is_system(u->uid)) { k = clean_ipc_by_uid(u->uid); if (k < 0) r = k; diff --git a/src/machine/machine.c b/src/machine/machine.c index 10e379238c..3d3c7cb6b8 100644 --- a/src/machine/machine.c +++ b/src/machine/machine.c @@ -43,6 +43,7 @@ #include "string-table.h" #include "terminal-util.h" #include "unit-name.h" +#include "user-util.h" #include "util.h" Machine* machine_new(Manager *manager, MachineClass class, const char *name) { @@ -656,7 +657,7 @@ int machine_get_uid_shift(Machine *m, uid_t *ret) { if (uid_base != 0) return -ENXIO; /* Insist that at least the nobody user is mapped, everything else is weird, and hence complex, and we don't support it */ - if (uid_range < (uid_t) 65534U) + if (uid_range < UID_NOBODY) return -ENXIO; /* If there's more than one line, then we don't support this mapping. */ diff --git a/src/nspawn/nspawn-def.h b/src/nspawn/nspawn-def.h index fc3c94064c..43a19d84f5 100644 --- a/src/nspawn/nspawn-def.h +++ b/src/nspawn/nspawn-def.h @@ -21,12 +21,6 @@ #include -/* Note that devpts's gid= parameter parses GIDs as signed values, hence we stay away from the upper half of the 32bit - * UID range here. We leave a bit of room at the lower end and a lot of room at the upper end, so that other subsystems - * may have their own allocation ranges too. */ -#define UID_SHIFT_PICK_MIN ((uid_t) UINT32_C(0x00080000)) -#define UID_SHIFT_PICK_MAX ((uid_t) UINT32_C(0x6FFF0000)) - /* While we are chmod()ing a directory tree, we set the top-level UID base to this "busy" base, so that we can always * recognize trees we are were chmod()ing recursively and got interrupted in */ #define UID_BUSY_BASE ((uid_t) UINT32_C(0xFFFE0000)) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index ccf5418ceb..4b82345d08 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -2862,7 +2862,7 @@ static int uid_shift_pick(uid_t *shift, LockFile *ret_lock_file) { if (--n_tries <= 0) return -EBUSY; - if (candidate < UID_SHIFT_PICK_MIN || candidate > UID_SHIFT_PICK_MAX) + if (candidate < CONTAINER_UID_BASE_MIN || candidate > CONTAINER_UID_BASE_MAX) goto next; if ((candidate & UINT32_C(0xFFFF)) != 0) goto next; @@ -2904,7 +2904,7 @@ static int uid_shift_pick(uid_t *shift, LockFile *ret_lock_file) { } else random_bytes(&candidate, sizeof(candidate)); - candidate = (candidate % (UID_SHIFT_PICK_MAX - UID_SHIFT_PICK_MIN)) + UID_SHIFT_PICK_MIN; + candidate = (candidate % (CONTAINER_UID_BASE_MAX - CONTAINER_UID_BASE_MIN)) + CONTAINER_UID_BASE_MIN; candidate &= (uid_t) UINT32_C(0xFFFF0000); } } diff --git a/src/nss-mymachines/nss-mymachines.c b/src/nss-mymachines/nss-mymachines.c index ddad109eee..b2f46e3db2 100644 --- a/src/nss-mymachines/nss-mymachines.c +++ b/src/nss-mymachines/nss-mymachines.c @@ -480,7 +480,7 @@ enum nss_status _nss_mymachines_getpwnam_r( pwd->pw_name = buffer; pwd->pw_uid = mapped; - pwd->pw_gid = 65534; /* nobody */ + pwd->pw_gid = GID_NOBODY; pwd->pw_gecos = buffer; pwd->pw_passwd = (char*) "*"; /* locked */ pwd->pw_dir = (char*) "/"; @@ -557,7 +557,7 @@ enum nss_status _nss_mymachines_getpwuid_r( pwd->pw_name = buffer; pwd->pw_uid = uid; - pwd->pw_gid = 65534; /* nobody */ + pwd->pw_gid = GID_NOBODY; pwd->pw_gecos = buffer; pwd->pw_passwd = (char*) "*"; /* locked */ pwd->pw_dir = (char*) "/"; diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c index d6de0a9878..cd6479573b 100644 --- a/src/nss-systemd/nss-systemd.c +++ b/src/nss-systemd/nss-systemd.c @@ -47,8 +47,8 @@ static const struct passwd root_passwd = { static const struct passwd nobody_passwd = { .pw_name = (char*) NOBODY_USER_NAME, .pw_passwd = (char*) "*", /* locked */ - .pw_uid = 65534, - .pw_gid = 65534, + .pw_uid = UID_NOBODY, + .pw_gid = GID_NOBODY, .pw_gecos = (char*) "User Nobody", .pw_dir = (char*) "/", .pw_shell = (char*) "/sbin/nologin", @@ -63,7 +63,7 @@ static const struct group root_group = { static const struct group nobody_group = { .gr_name = (char*) NOBODY_GROUP_NAME, - .gr_gid = 65534, + .gr_gid = GID_NOBODY, .gr_passwd = (char*) "*", /* locked */ .gr_mem = (char*[]) { NULL }, }; @@ -251,7 +251,7 @@ enum nss_status _nss_systemd_getpwuid_r( } } - if (uid <= SYSTEM_UID_MAX) + if (!uid_is_dynamic(uid)) goto not_found; if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) @@ -463,7 +463,7 @@ enum nss_status _nss_systemd_getgrgid_r( } } - if (gid <= SYSTEM_GID_MAX) + if (!gid_is_dynamic(gid)) goto not_found; if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) @@ -500,7 +500,6 @@ enum nss_status _nss_systemd_getgrgid_r( direct_lookup: if (bypass > 0) { - r = direct_lookup_uid(gid, &direct); if (r == -ENOENT) goto not_found; diff --git a/src/shared/condition.c b/src/shared/condition.c index f1e914cb2d..3f32dfb7b6 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -157,7 +157,7 @@ static int condition_test_user(Condition *c) { return id == getuid() || id == geteuid(); if (streq("@system", c->parameter)) - return getuid() <= SYSTEM_UID_MAX || geteuid() <= SYSTEM_UID_MAX; + return uid_is_system(getuid()) || uid_is_system(geteuid()); username = getusername_malloc(); if (!username) diff --git a/src/test/test-condition.c b/src/test/test-condition.c index 31e08b2318..d43db3a7cd 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -391,7 +391,7 @@ static void test_condition_test_user(void) { assert_se(condition); r = condition_test(condition); log_info("ConditionUser=@system → %i", r); - if (getuid() < SYSTEM_UID_MAX || geteuid() < SYSTEM_UID_MAX) + if (uid_is_system(getuid()) || uid_is_system(geteuid())) assert_se(r > 0); else assert_se(r == 0); diff --git a/src/test/test-execute.c b/src/test/test-execute.c index 2bff00a9f7..fba798e22b 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -324,7 +324,6 @@ static void test_exec_systemcallfilter_system(Manager *m) { log_notice("Seccomp not available, skipping %s", __func__); return; } - if (getpwnam("nobody")) test(m, "exec-systemcallfilter-system-user.service", 0, CLD_EXITED); else if (getpwnam("nfsnobody")) @@ -348,8 +347,10 @@ static void test_exec_group(Manager *m) { test(m, "exec-group.service", 0, CLD_EXITED); else if (getgrnam("nfsnobody")) test(m, "exec-group-nfsnobody.service", 0, CLD_EXITED); + else if (getgrnam("nogroup")) + test(m, "exec-group-nogroup.service", 0, CLD_EXITED); else - log_error_errno(errno, "Skipping %s, could not find nobody/nfsnobody group: %m", __func__); + log_error_errno(errno, "Skipping %s, could not find nobody/nfsnobody/nogroup group: %m", __func__); } static void test_exec_supplementarygroups(Manager *m) { diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index 639804f34a..17a8520741 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -23,6 +23,7 @@ #include "string-util.h" #include "user-util.h" #include "util.h" +#include "path-util.h" static void test_uid_to_name_one(uid_t uid, const char *name) { _cleanup_free_ char *t = NULL; @@ -144,17 +145,51 @@ static void test_valid_home(void) { assert_se(valid_home("/home/foo")); } +static void test_get_user_creds_one(const char *id, const char *name, uid_t uid, gid_t gid, const char *home, const char *shell) { + const char *rhome; + const char *rshell; + uid_t ruid; + gid_t rgid; + + assert_se(get_user_creds(&id, &ruid, &rgid, &rhome, &rshell) >= 0); + assert_se(streq_ptr(id, name)); + assert_se(ruid == uid); + assert_se(rgid == gid); + assert_se(path_equal(rhome, home)); + assert_se(path_equal(rshell, shell)); +} + +static void test_get_group_creds_one(const char *id, const char *name, gid_t gid) { + gid_t rgid; + + assert_se(get_group_creds(&id, &rgid) >= 0); + assert_se(streq_ptr(id, name)); + assert_se(rgid == gid); +} + int main(int argc, char*argv[]) { test_uid_to_name_one(0, "root"); + test_uid_to_name_one(UID_NOBODY, NOBODY_USER_NAME); test_uid_to_name_one(0xFFFF, "65535"); test_uid_to_name_one(0xFFFFFFFF, "4294967295"); test_gid_to_name_one(0, "root"); + test_gid_to_name_one(GID_NOBODY, NOBODY_GROUP_NAME); test_gid_to_name_one(TTY_GID, "tty"); test_gid_to_name_one(0xFFFF, "65535"); test_gid_to_name_one(0xFFFFFFFF, "4294967295"); + test_get_user_creds_one("root", "root", 0, 0, "/root", "/bin/sh"); + test_get_user_creds_one("0", "root", 0, 0, "/root", "/bin/sh"); + test_get_user_creds_one(NOBODY_USER_NAME, NOBODY_USER_NAME, UID_NOBODY, GID_NOBODY, "/", "/sbin/nologin"); + test_get_user_creds_one("65534", NOBODY_USER_NAME, UID_NOBODY, GID_NOBODY, "/", "/sbin/nologin"); + + test_get_group_creds_one("root", "root", 0); + test_get_group_creds_one("0", "root", 0); + test_get_group_creds_one(NOBODY_GROUP_NAME, NOBODY_GROUP_NAME, GID_NOBODY); + test_get_group_creds_one("65534", NOBODY_GROUP_NAME, GID_NOBODY); + test_parse_uid(); test_uid_ptr(); diff --git a/test/meson.build b/test/meson.build index 750f5c0379..5c533f4833 100644 --- a/test/meson.build +++ b/test/meson.build @@ -68,6 +68,7 @@ test_data_files = ''' test-execute/exec-environment.service test-execute/exec-environmentfile.service test-execute/exec-group-nfsnobody.service + test-execute/exec-group-nogroup.service test-execute/exec-group.service test-execute/exec-ignoresigpipe-no.service test-execute/exec-ignoresigpipe-yes.service diff --git a/test/test-execute/exec-group-nogroup.service b/test/test-execute/exec-group-nogroup.service new file mode 100644 index 0000000000..cf0773229e --- /dev/null +++ b/test/test-execute/exec-group-nogroup.service @@ -0,0 +1,7 @@ +[Unit] +Description=Test for Group + +[Service] +ExecStart=/bin/sh -x -c 'test "$$(id -n -g)" = "nogroup"' +Type=oneshot +Group=nogroup