Compare commits

...

9 Commits

Author SHA1 Message Date
anonymix007 09fcfa9ad1
Merge 18639361da into 52b0351a15 2024-11-20 12:11:37 +00:00
Yu Watanabe 52b0351a15
core/exec-invoke: suppress placeholder home only in build_environment() (#35219)
Alternative to https://github.com/systemd/systemd/pull/34789
Closes #34789
2024-11-20 17:34:25 +09:00
Luca Boccassi fe077a1a58 units: add initrd directory to list of conditions for systemd-confext
systemd-sysext has the same check, but it was forgotten for confexts.
Needed to activate confexts from the ESP in the initrd.
2024-11-20 09:12:24 +01:00
Mike Yuan b718b86e1b
core/exec-invoke: suppress placeholder home only in build_environment()
Currently, get_fixed_user() employs USER_CREDS_SUPPRESS_PLACEHOLDER,
meaning home path is set to NULL if it's empty or root. However,
the path is also used for applying WorkingDirectory=~, and we'd
spuriously use the invoking user's home as fallback even if
User= is changed in that case.

Let's instead delegate such suppression to build_environment(),
so that home is proper initialized for usage at other steps.
shell doesn't actually suffer from such problem, but it's changed
too for consistency.

Alternative to #34789
2024-11-19 00:38:18 +01:00
Mike Yuan d911778877
core/exec-invoke: minor cleanup for apply_working_directory() error handling
Assign exit_status at the same site where error log is emitted,
for readability.
2024-11-19 00:38:18 +01:00
Mike Yuan eea9d3eb10
basic/user-util: split out placeholder suppression from USER_CREDS_CLEAN into its own flag
No functional change, preparation for later commits.
2024-11-19 00:38:18 +01:00
Mike Yuan 579ce77ead
basic/user-util: introduce shell_is_placeholder() helper 2024-11-19 00:38:18 +01:00
anonymix007 18639361da man: Document ukify --hwids= and --devicetree-auto= options 2024-11-17 20:56:38 +03:00
anonymix007 07879e662b ukify: Switch to JSON HWID description format 2024-11-17 20:56:33 +03:00
8 changed files with 106 additions and 106 deletions

View File

@ -71,6 +71,8 @@
<varname>Cmdline=</varname>/<option>--cmdline=</option>, <varname>Cmdline=</varname>/<option>--cmdline=</option>,
<varname>OSRelease=</varname>/<option>--os-release=</option>, <varname>OSRelease=</varname>/<option>--os-release=</option>,
<varname>DeviceTree=</varname>/<option>--devicetree=</option>, <varname>DeviceTree=</varname>/<option>--devicetree=</option>,
<varname>DeviceTreeAuto=</varname>/<option>--devicetree-auto=</option>,
<varname>HWIDs=</varname>/<option>--hwids=</option>,
<varname>Splash=</varname>/<option>--splash=</option>, <varname>Splash=</varname>/<option>--splash=</option>,
<varname>PCRPKey=</varname>/<option>--pcrpkey=</option>, <varname>PCRPKey=</varname>/<option>--pcrpkey=</option>,
<varname>Uname=</varname>/<option>--uname=</option>, <varname>Uname=</varname>/<option>--uname=</option>,
@ -373,6 +375,30 @@
<xi:include href="version-info.xml" xpointer="v253"/></listitem> <xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>DeviceTreeAuto=<replaceable>PATH</replaceable>...</varname></term>
<term><option>--devicetree-auto=<replaceable>PATH</replaceable></option></term>
<listitem><para>Zero or more automatically selectable DeviceTree files. In the configuration file, items are separated by
whitespace. Each DeviceTree will be in a separate <literal>.dtbauto</literal> section.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>HWIDs=<replaceable>PATH</replaceable></varname></term>
<term><option>--hwids=<replaceable>PATH</replaceable></option></term>
<listitem><para>The hardware ID device table (the <literal>.hwids</literal> section). The argument is a
path to a directory with JSON HWID device description files. Each JSON file needs to contain <literal>name</literal>
and <literal>compatible</literal> strings and also array of CHIDs/HWIDs as <literal>hwids</literal>.
If not specified, the section will not be present. It is recommended to specify this parameter if automatically
selectable DeviceTrees are to be used.
</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><varname>Uname=<replaceable>VERSION</replaceable></varname></term> <term><varname>Uname=<replaceable>VERSION</replaceable></varname></term>
<term><option>--uname=<replaceable>VERSION</replaceable></option></term> <term><option>--uname=<replaceable>VERSION</replaceable></option></term>

View File

@ -220,9 +220,9 @@ static int synthesize_user_creds(
if (ret_gid) if (ret_gid)
*ret_gid = GID_NOBODY; *ret_gid = GID_NOBODY;
if (ret_home) if (ret_home)
*ret_home = FLAGS_SET(flags, USER_CREDS_CLEAN) ? NULL : "/"; *ret_home = FLAGS_SET(flags, USER_CREDS_SUPPRESS_PLACEHOLDER) ? NULL : "/";
if (ret_shell) if (ret_shell)
*ret_shell = FLAGS_SET(flags, USER_CREDS_CLEAN) ? NULL : NOLOGIN; *ret_shell = FLAGS_SET(flags, USER_CREDS_SUPPRESS_PLACEHOLDER) ? NULL : NOLOGIN;
return 0; return 0;
} }
@ -244,6 +244,7 @@ int get_user_creds(
assert(username); assert(username);
assert(*username); assert(*username);
assert((ret_home || ret_shell) || !(flags & (USER_CREDS_SUPPRESS_PLACEHOLDER|USER_CREDS_CLEAN)));
if (!FLAGS_SET(flags, USER_CREDS_PREFER_NSS) || if (!FLAGS_SET(flags, USER_CREDS_PREFER_NSS) ||
(!ret_home && !ret_shell)) { (!ret_home && !ret_shell)) {
@ -315,17 +316,14 @@ int get_user_creds(
if (ret_home) if (ret_home)
/* Note: we don't insist on normalized paths, since there are setups that have /./ in the path */ /* Note: we don't insist on normalized paths, since there are setups that have /./ in the path */
*ret_home = (FLAGS_SET(flags, USER_CREDS_CLEAN) && *ret_home = (FLAGS_SET(flags, USER_CREDS_SUPPRESS_PLACEHOLDER) && empty_or_root(p->pw_dir)) ||
(empty_or_root(p->pw_dir) || (FLAGS_SET(flags, USER_CREDS_CLEAN) && (!path_is_valid(p->pw_dir) || !path_is_absolute(p->pw_dir)))
!path_is_valid(p->pw_dir) || ? NULL : p->pw_dir;
!path_is_absolute(p->pw_dir))) ? NULL : p->pw_dir;
if (ret_shell) if (ret_shell)
*ret_shell = (FLAGS_SET(flags, USER_CREDS_CLEAN) && *ret_shell = (FLAGS_SET(flags, USER_CREDS_SUPPRESS_PLACEHOLDER) && shell_is_placeholder(p->pw_shell)) ||
(isempty(p->pw_shell) || (FLAGS_SET(flags, USER_CREDS_CLEAN) && (!path_is_valid(p->pw_shell) || !path_is_absolute(p->pw_shell)))
!path_is_valid(p->pw_shell) || ? NULL : p->pw_shell;
!path_is_absolute(p->pw_shell) ||
is_nologin_shell(p->pw_shell))) ? NULL : p->pw_shell;
if (patch_username) if (patch_username)
*username = p->pw_name; *username = p->pw_name;

View File

@ -12,6 +12,8 @@
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include "string-util.h"
/* Users managed by systemd-homed. See https://systemd.io/UIDS-GIDS for details how this range fits into the rest of the world */ /* Users managed by systemd-homed. See https://systemd.io/UIDS-GIDS for details how this range fits into the rest of the world */
#define HOME_UID_MIN ((uid_t) 60001) #define HOME_UID_MIN ((uid_t) 60001)
#define HOME_UID_MAX ((uid_t) 60513) #define HOME_UID_MAX ((uid_t) 60513)
@ -36,10 +38,20 @@ static inline int parse_gid(const char *s, gid_t *ret_gid) {
char* getlogname_malloc(void); char* getlogname_malloc(void);
char* getusername_malloc(void); char* getusername_malloc(void);
const char* default_root_shell_at(int rfd);
const char* default_root_shell(const char *root);
bool is_nologin_shell(const char *shell);
static inline bool shell_is_placeholder(const char *shell) {
return isempty(shell) || is_nologin_shell(shell);
}
typedef enum UserCredsFlags { typedef enum UserCredsFlags {
USER_CREDS_PREFER_NSS = 1 << 0, /* if set, only synthesize user records if database lacks them. Normally we bypass the userdb entirely for the records we can synthesize */ USER_CREDS_PREFER_NSS = 1 << 0, /* if set, only synthesize user records if database lacks them. Normally we bypass the userdb entirely for the records we can synthesize */
USER_CREDS_ALLOW_MISSING = 1 << 1, /* if a numeric UID string is resolved, be OK if there's no record for it */ USER_CREDS_ALLOW_MISSING = 1 << 1, /* if a numeric UID string is resolved, be OK if there's no record for it */
USER_CREDS_CLEAN = 1 << 2, /* try to clean up shell and home fields with invalid data */ USER_CREDS_CLEAN = 1 << 2, /* try to clean up shell and home fields with invalid data */
USER_CREDS_SUPPRESS_PLACEHOLDER = 1 << 3, /* suppress home and/or shell fields if value is placeholder (root/empty/nologin) */
} UserCredsFlags; } UserCredsFlags;
int get_user_creds(const char **username, uid_t *ret_uid, gid_t *ret_gid, const char **ret_home, const char **ret_shell, UserCredsFlags flags); int get_user_creds(const char **username, uid_t *ret_uid, gid_t *ret_gid, const char **ret_home, const char **ret_shell, UserCredsFlags flags);
@ -125,10 +137,6 @@ int fgetsgent_sane(FILE *stream, struct sgrp **sg);
int putsgent_sane(const struct sgrp *sg, FILE *stream); int putsgent_sane(const struct sgrp *sg, FILE *stream);
#endif #endif
bool is_nologin_shell(const char *shell);
const char* default_root_shell_at(int rfd);
const char* default_root_shell(const char *root);
int is_this_me(const char *username); int is_this_me(const char *username);
const char* get_home_root(void); const char* get_home_root(void);

View File

@ -855,9 +855,6 @@ static int get_fixed_user(
assert(user_or_uid); assert(user_or_uid);
assert(ret_username); assert(ret_username);
/* Note that we don't set $HOME or $SHELL if they are not particularly enlightening anyway
* (i.e. are "/" or "/bin/nologin"). */
r = get_user_creds(&user_or_uid, ret_uid, ret_gid, ret_home, ret_shell, USER_CREDS_CLEAN); r = get_user_creds(&user_or_uid, ret_uid, ret_gid, ret_home, ret_shell, USER_CREDS_CLEAN);
if (r < 0) if (r < 0)
return r; return r;
@ -1883,7 +1880,10 @@ static int build_environment(
} }
} }
if (home && set_user_login_env) { /* Note that we don't set $HOME or $SHELL if they are not particularly enlightening anyway
* (i.e. are "/" or "/bin/nologin"). */
if (home && set_user_login_env && !empty_or_root(home)) {
x = strjoin("HOME=", home); x = strjoin("HOME=", home);
if (!x) if (!x)
return -ENOMEM; return -ENOMEM;
@ -1892,7 +1892,7 @@ static int build_environment(
our_env[n_env++] = x; our_env[n_env++] = x;
} }
if (shell && set_user_login_env) { if (shell && set_user_login_env && !shell_is_placeholder(shell)) {
x = strjoin("SHELL=", shell); x = strjoin("SHELL=", shell);
if (!x) if (!x)
return -ENOMEM; return -ENOMEM;
@ -3471,20 +3471,16 @@ static int apply_working_directory(
const ExecContext *context, const ExecContext *context,
const ExecParameters *params, const ExecParameters *params,
ExecRuntime *runtime, ExecRuntime *runtime,
const char *home, const char *home) {
int *exit_status) {
const char *wd; const char *wd;
int r; int r;
assert(context); assert(context);
assert(exit_status);
if (context->working_directory_home) { if (context->working_directory_home) {
if (!home) { if (!home)
*exit_status = EXIT_CHDIR;
return -ENXIO; return -ENXIO;
}
wd = home; wd = home;
} else } else
@ -3503,13 +3499,7 @@ static int apply_working_directory(
if (r >= 0) if (r >= 0)
r = RET_NERRNO(fchdir(dfd)); r = RET_NERRNO(fchdir(dfd));
} }
return context->working_directory_missing_ok ? 0 : r;
if (r < 0 && !context->working_directory_missing_ok) {
*exit_status = EXIT_CHDIR;
return r;
}
return 0;
} }
static int apply_root_directory( static int apply_root_directory(
@ -3785,7 +3775,7 @@ static int acquire_home(const ExecContext *c, const char **home, char **ret_buf)
if (!c->working_directory_home) if (!c->working_directory_home)
return 0; return 0;
if (c->dynamic_user) if (c->dynamic_user || (c->user && is_this_me(c->user) <= 0))
return -EADDRNOTAVAIL; return -EADDRNOTAVAIL;
r = get_home_dir(ret_buf); r = get_home_dir(ret_buf);
@ -4543,7 +4533,7 @@ int exec_invoke(
r = acquire_home(context, &home, &home_buffer); r = acquire_home(context, &home, &home_buffer);
if (r < 0) { if (r < 0) {
*exit_status = EXIT_CHDIR; *exit_status = EXIT_CHDIR;
return log_exec_error_errno(context, params, r, "Failed to determine $HOME for user: %m"); return log_exec_error_errno(context, params, r, "Failed to determine $HOME for the invoking user: %m");
} }
/* If a socket is connected to STDIN/STDOUT/STDERR, we must drop O_NONBLOCK */ /* If a socket is connected to STDIN/STDOUT/STDERR, we must drop O_NONBLOCK */
@ -5382,9 +5372,11 @@ int exec_invoke(
* running this service might have the correct privilege to change to the working directory. Also, it * running this service might have the correct privilege to change to the working directory. Also, it
* is absolutely 💣 crucial 💣 we applied all mount namespacing rearrangements before this, so that * is absolutely 💣 crucial 💣 we applied all mount namespacing rearrangements before this, so that
* the cwd cannot be used to pin directories outside of the sandbox. */ * the cwd cannot be used to pin directories outside of the sandbox. */
r = apply_working_directory(context, params, runtime, home, exit_status); r = apply_working_directory(context, params, runtime, home);
if (r < 0) if (r < 0) {
*exit_status = EXIT_CHDIR;
return log_exec_error_errno(context, params, r, "Changing to the requested working directory failed: %m"); return log_exec_error_errno(context, params, r, "Changing to the requested working directory failed: %m");
}
if (needs_sandboxing) { if (needs_sandboxing) {
/* Apply other MAC contexts late, but before seccomp syscall filtering, as those should really be last to /* Apply other MAC contexts late, but before seccomp syscall filtering, as those should really be last to

View File

@ -2297,7 +2297,8 @@ static int start_transient_scope(sd_bus *bus) {
uid_t uid; uid_t uid;
gid_t gid; gid_t gid;
r = get_user_creds(&arg_exec_user, &uid, &gid, &home, &shell, USER_CREDS_CLEAN|USER_CREDS_PREFER_NSS); r = get_user_creds(&arg_exec_user, &uid, &gid, &home, &shell,
USER_CREDS_CLEAN|USER_CREDS_SUPPRESS_PLACEHOLDER|USER_CREDS_PREFER_NSS);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to resolve user %s: %m", arg_exec_user); return log_error_errno(r, "Failed to resolve user %s: %m", arg_exec_user);

View File

@ -42,6 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import textwrap import textwrap
import uuid
from collections.abc import Iterable, Iterator, Sequence from collections.abc import Iterable, Iterator, Sequence
from hashlib import sha256 from hashlib import sha256
from pathlib import Path from pathlib import Path
@ -1013,14 +1014,9 @@ def merge_sbat(input_pe: list[Path], input_text: list[str]) -> str:
) )
# Keep in sync with EFI_GUID (src/boot/efi.h)
# uint32_t Data1, uint16_t Data2, uint16_t Data3, uint8_t Data4[8]
EFI_GUID = tuple[int, int, int, tuple[int, int, int, int, int, int, int, int]]
EFI_GUID_STRUCT_SIZE = 4 + 2 + 2 + 1 * 8
# Keep in sync with Device (DEVICE_TYPE_DEVICETREE) from src/boot/chid.h # Keep in sync with Device (DEVICE_TYPE_DEVICETREE) from src/boot/chid.h
# uint32_t descriptor, EFI_GUID chid, uint32_t name_offset, uint32_t compatible_offset # uint32_t descriptor, EFI_GUID chid, uint32_t name_offset, uint32_t compatible_offset
DEVICE_STRUCT_SIZE = 4 + EFI_GUID_STRUCT_SIZE + 4 + 4 DEVICE_STRUCT_SIZE = 4 + 16 + 4 + 4
NULL_DEVICE = b'\0' * DEVICE_STRUCT_SIZE NULL_DEVICE = b'\0' * DEVICE_STRUCT_SIZE
DEVICE_TYPE_DEVICETREE = 1 DEVICE_TYPE_DEVICETREE = 1
@ -1029,29 +1025,21 @@ def device_make_descriptor(device_type: int, size: int) -> int:
return (size) | (device_type << 28) return (size) | (device_type << 28)
def pack_device(offsets: dict[str, int], name: str, compatible: str, chids: list[EFI_GUID]) -> bytes: DEVICETREE_DESCRIPTOR = device_make_descriptor(DEVICE_TYPE_DEVICETREE, DEVICE_STRUCT_SIZE)
def pack_device(offsets: dict[str, int], name: str, compatible: str, chids: list[uuid.UUID]) -> bytes:
data = b'' data = b''
for data1, data2, data3, data4 in chids: for chid in chids:
data += struct.pack( data += struct.pack('<I', DEVICETREE_DESCRIPTOR)
'<IIHH8BII', data += chid.bytes_le
device_make_descriptor(DEVICE_TYPE_DEVICETREE, DEVICE_STRUCT_SIZE), data += struct.pack('<II', offsets[name], offsets[compatible])
data1,
data2,
data3,
*data4,
offsets[name],
offsets[compatible],
)
assert len(data) == DEVICE_STRUCT_SIZE * len(chids) assert len(data) == DEVICE_STRUCT_SIZE * len(chids)
return data return data
def hex_pairs_list(string: str) -> list[int]:
return [int(string[i : i + 2], 16) for i in range(0, len(string), 2)]
def pack_strings(strings: set[str], base: int) -> tuple[bytes, dict[str, int]]: def pack_strings(strings: set[str], base: int) -> tuple[bytes, dict[str, int]]:
blob = b'' blob = b''
offsets = {} offsets = {}
@ -1064,56 +1052,22 @@ def pack_strings(strings: set[str], base: int) -> tuple[bytes, dict[str, int]]:
def parse_hwid_dir(path: Path) -> bytes: def parse_hwid_dir(path: Path) -> bytes:
hwid_files = path.rglob('*.txt') hwid_files = path.rglob('*.json')
strings: set[str] = set() strings: set[str] = set()
devices: collections.defaultdict[tuple[str, str], list[EFI_GUID]] = collections.defaultdict(list) devices: collections.defaultdict[tuple[str, str], list[uuid.UUID]] = collections.defaultdict(list)
uuid_regexp = re.compile(
r'\{[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}\}', re.I
)
for hwid_file in hwid_files: for hwid_file in hwid_files:
content = hwid_file.open().readlines() data = json.loads(hwid_file.read_text(encoding='UTF-8'))
data: dict[str, str] = { for k in ['name', 'compatible', 'hwids']:
'Manufacturer': '', if k not in data:
'Family': '',
'Compatible': '',
}
uuids: list[EFI_GUID] = []
for line in content:
for k in data:
if line.startswith(k):
data[k] = line.split(':')[1].strip()
break
else:
uuid = uuid_regexp.match(line)
if uuid is not None:
d1, d2, d3, d4, d5 = uuid.group(0)[1:-1].split('-')
data1 = int(d1, 16)
data2 = int(d2, 16)
data3 = int(d3, 16)
data4 = cast(
tuple[int, int, int, int, int, int, int, int],
tuple(hex_pairs_list(d4) + hex_pairs_list(d5)),
)
uuids.append((data1, data2, data3, data4))
for k, v in data.items():
if not v:
raise ValueError(f'hwid description file "{hwid_file}" does not contain "{k}"') raise ValueError(f'hwid description file "{hwid_file}" does not contain "{k}"')
name = data['Manufacturer'] + ' ' + data['Family'] strings |= set([data['name'], data['compatible']])
compatible = data['Compatible']
strings |= set([name, compatible]) # (name, compatible) pair uniquely identifies the device
devices[(data['name'], data['compatible'])] += [uuid.UUID(u) for u in data['hwids']]
# (compatible, name) pair uniquely identifies the device
devices[(compatible, name)] += uuids
total_device_structs = 1 total_device_structs = 1
for dev, uuids in devices.items(): for dev, uuids in devices.items():
@ -1122,7 +1076,7 @@ def parse_hwid_dir(path: Path) -> bytes:
strings_blob, offsets = pack_strings(strings, total_device_structs * DEVICE_STRUCT_SIZE) strings_blob, offsets = pack_strings(strings, total_device_structs * DEVICE_STRUCT_SIZE)
devices_blob = b'' devices_blob = b''
for (compatible, name), uuids in devices.items(): for (name, compatible), uuids in devices.items():
devices_blob += pack_device(offsets, name, compatible, uuids) devices_blob += pack_device(offsets, name, compatible, uuids)
devices_blob += NULL_DEVICE devices_blob += NULL_DEVICE

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
(! systemd-run --wait -p DynamicUser=yes \
-p EnvironmentFile=-/usr/lib/systemd/systemd-asan-env \
-p WorkingDirectory='~' true)
assert_eq "$(systemd-run --pipe --uid=root -p WorkingDirectory='~' pwd)" "/root"
assert_eq "$(systemd-run --pipe --uid=nobody -p WorkingDirectory='~' pwd)" "/"
assert_eq "$(systemd-run --pipe --uid=testuser -p WorkingDirectory='~' pwd)" "/home/testuser"
(! systemd-run --wait -p DynamicUser=yes -p User=testuser \
-p EnvironmentFile=-/usr/lib/systemd/systemd-asan-env \
-p WorkingDirectory='~' true)

View File

@ -16,6 +16,7 @@ ConditionDirectoryNotEmpty=|/run/confexts
ConditionDirectoryNotEmpty=|/var/lib/confexts ConditionDirectoryNotEmpty=|/var/lib/confexts
ConditionDirectoryNotEmpty=|/usr/local/lib/confexts ConditionDirectoryNotEmpty=|/usr/local/lib/confexts
ConditionDirectoryNotEmpty=|/usr/lib/confexts ConditionDirectoryNotEmpty=|/usr/lib/confexts
ConditionDirectoryNotEmpty=|/.extra/confext
DefaultDependencies=no DefaultDependencies=no
After=local-fs.target After=local-fs.target