1
0
mirror of https://github.com/systemd/systemd synced 2025-11-17 07:44:46 +01:00

Compare commits

...

15 Commits

Author SHA1 Message Date
Yu Watanabe
0885e4a6e7
Various --bind-user= fixes (#39498) 2025-10-31 20:43:54 +09:00
Yu Watanabe
b1ba55a8a7 network: do not restart DHCPv4 client on stopping/restarting networkd
Follow-up for fc35a9f8d1632c4e7a279228f869bfc77d8f5b9c (v255).
Fixes #39299.
2025-10-31 17:59:22 +09:00
Lennart Poettering
31c220d8f5
importd: support OS tree "mangling" unpriv too (#39406)
Split out of #38728 

(background: os tree "mangling" is what we do if a tarball with an OS
image inside it if is nested inside an extra top-level dir inside the
tarball, which we need to "mangle" and move everything inside one level
up)
2025-10-31 09:46:36 +01:00
Yu Watanabe
3f9db926e4 network: propagate error in link_carrier_lost()
Follow-up for 07021ed4f5ee5e34b06fcba97cab2c6214f601c9 (v258).
2025-10-31 17:33:58 +09:00
Daan De Meyer
def01c7efe nspawn/vmspawn: Add --bind-user-group= option
Useful to add the bound users to the wheel group.
2025-10-31 08:57:38 +01:00
Daan De Meyer
b430f2bc94 nspawn-bind-user: Write membership records 2025-10-31 08:57:38 +01:00
Daan De Meyer
3fbf4ac24b userdbctl: Write empty JSON object into membership files 2025-10-31 08:57:38 +01:00
Daan De Meyer
cfabf3eb3b TEST-87-AUX-UTILS-VM: Propagate SYSTEMD_PAGER at one more place 2025-10-31 08:57:38 +01:00
Daan De Meyer
472161f368 userdb: Add missing .membership extension to membership files
Follow up for fe0342edf4693ac14c8cb9a977afa09e4acd4daf

This also drops the mkosi testuser from the wheel and systemd-journal
groups as the integration tests rely on the testuser not being to read
the full journal.
2025-10-31 08:57:08 +01:00
Daan De Meyer
097b6d3f66 nspawn: Fix docs 2025-10-31 08:26:04 +01:00
Christoph Anton Mitterer
bfb365d924
man: clarify quoting of $ in command lines (#39494)
When the special executable prefix `:` is used, `$$` yield the literal `$$`.
2025-10-31 15:28:57 +09:00
dgengtek
4207abb6e1 man: clarify requirements for BridgeVLAN to work 2025-10-31 15:26:14 +09:00
Lennart Poettering
2348c56367 import: make sure image mangling works unpriv too 2025-10-30 22:57:43 +01:00
Lennart Poettering
7912b1ebe5 import-common: rework import_mangle_os_tree() to operate based on fd to tree 2025-10-30 22:56:19 +01:00
Lennart Poettering
70733160ee os-util: add fd_is_os_tree() which is like path_is_os_tree() but operates on an fd 2025-10-30 22:56:19 +01:00
22 changed files with 292 additions and 63 deletions

View File

@ -1601,7 +1601,7 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
<listitem><para>Binds the home directory of the specified user on the host into the container. Takes <listitem><para>Binds the home directory of the specified user on the host into the container. Takes
the name of an existing user on the host as argument. May be used multiple times to bind multiple the name of an existing user on the host as argument. May be used multiple times to bind multiple
users into the container. This does three things:</para> users into the container. This does two things:</para>
<orderedlist> <orderedlist>
<listitem><para>The user's home directory is bind mounted from the host into <listitem><para>The user's home directory is bind mounted from the host into
@ -1616,7 +1616,7 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
user/group databases.</para></listitem> user/group databases.</para></listitem>
</orderedlist> </orderedlist>
<para>The combination of the three operations above ensures that it is possible to log into the <para>The combination of the two operations above ensures that it is possible to log into the
container using the same account information as on the host. The user is only mapped transiently, container using the same account information as on the host. The user is only mapped transiently,
while the container is running, and the mapping itself does not result in persistent changes to the while the container is running, and the mapping itself does not result in persistent changes to the
container (except maybe for log messages generated at login time, and similar). Note that in container (except maybe for log messages generated at login time, and similar). Note that in
@ -1667,6 +1667,19 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
<xi:include href="version-info.xml" xpointer="v258"/></listitem> <xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><option>--bind-user-group=<replaceable>NAME</replaceable></option></term>
<listitem><para>When used with <option>--bind-user=</option>, includes the specified group as an
auxiliary group in the user records of users bound into the container. Takes a group name.</para>
<para>Note: This will not check whether the specified groups exist in the container.</para>
<para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><option>--inaccessible=</option></term> <term><option>--inaccessible=</option></term>

View File

@ -526,6 +526,20 @@
<xi:include href="version-info.xml" xpointer="v259"/></listitem> <xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><option>--bind-user-group=<replaceable>NAME</replaceable></option></term>
<listitem><para>When used with <option>--bind-user=</option>, includes the specified group as an
auxiliary group in the user records of users bound into the virtual machine. Takes a group name.</para>
<para>Note: This will not check whether the specified groups exist in the virtual machine.</para>
<para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
</variablelist> </variablelist>
</refsect2> </refsect2>

View File

@ -6347,15 +6347,17 @@ ServerAddress=192.168.0.1/24</programlisting>
<refsect1> <refsect1>
<title>[BridgeVLAN] Section Options</title> <title>[BridgeVLAN] Section Options</title>
<para> <para>
The [BridgeVLAN] section manages the VLAN ID configurations of a bridge master or port, and accepts the The [BridgeVLAN] section manages the VLAN ID configuration of a bridge master or enslaved device.
following keys. To make the settings in this section take an effect, To make the settings in this section take an effect,
<varname>VLANFiltering=</varname> option has to be enabled on the bridge master, see the [Bridge] <varname>VLANFiltering=</varname> option has to be enabled on the bridge master, see the [Bridge]
section in section in
<citerefentry><refentrytitle>systemd.netdev</refentrytitle><manvolnum>5</manvolnum></citerefentry>. <citerefentry><refentrytitle>systemd.netdev</refentrytitle><manvolnum>5</manvolnum></citerefentry>, and
If at least one valid settings specified in this section in a .network file for an interface, all each enslaved device needs to define the relevant VLAN IDs in its own [BridgeVLAN] section, which it
assigned VLAN IDs on the interface that are not configured in the .network file will be removed. If has in common with the bridge master.
All assigned VLAN IDs on the interface that are not configured in the .network file will be removed. If
VLAN IDs on an interface need to be managed by other tools, then the settings in this section cannot VLAN IDs on an interface need to be managed by other tools, then the settings in this section cannot
be used in the matching .network file. be used in the matching .network file and <varname>VLANFiltering=</varname> needs to be disabled on
the bridge master.
</para> </para>
<variablelist class='network-directives'> <variablelist class='network-directives'>

View File

@ -1485,7 +1485,8 @@ ExecStart=/bin/echo $ONE $TWO $THREE</programlisting>
<literal>too</literal>. <literal>too</literal>.
</para> </para>
<para>To pass a literal dollar sign, use <literal>$$</literal>. <para>Unless for commands with the special executable prefix <literal>:</literal>,
to pass a literal dollar sign, use <literal>$$</literal>.
Variables whose value is not known at expansion time are treated Variables whose value is not known at expansion time are treated
as empty strings. Note that the first argument (i.e. the program as empty strings. Note that the first argument (i.e. the program
to execute) may not be a variable.</para> to execute) may not be a variable.</para>

View File

@ -3,10 +3,6 @@
"uid": 4711, "uid": 4711,
"disposition": "regular", "disposition": "regular",
"enforcePasswordPolicy": false, "enforcePasswordPolicy": false,
"memberOf": [
"wheel",
"systemd-journal"
],
"shell": "/bin/bash", "shell": "/bin/bash",
"privileged": { "privileged": {
"hashedPassword": ["$1$kqp7NF1f$tNnQcshPX53CSfRKTQD0R1"] "hashedPassword": ["$1$kqp7NF1f$tNnQcshPX53CSfRKTQD0R1"]

View File

@ -114,6 +114,26 @@ int path_is_extension_tree(ImageClass image_class, const char *path, const char
return 1; return 1;
} }
int fd_is_os_tree(int fd) {
int r;
assert(fd >= 0);
r = open_extension_release_at(
fd,
IMAGE_MACHINE,
/* extension= */ NULL,
/* relax_extension_release_check= */ false,
/* ret_path= */ NULL,
/* ret_fd= */ NULL);
if (r == -ENOENT)
return false;
if (r < 0)
return r;
return true;
}
static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) { static int extension_release_strict_xattr_value(int extension_release_fd, const char *extension_release_dir_path, const char *filename) {
int r; int r;

View File

@ -28,6 +28,7 @@ int path_is_extension_tree(ImageClass image_class, const char *path, const char
static inline int path_is_os_tree(const char *path) { static inline int path_is_os_tree(const char *path) {
return path_is_extension_tree(_IMAGE_CLASS_INVALID, path, NULL, false); return path_is_extension_tree(_IMAGE_CLASS_INVALID, path, NULL, false);
} }
int fd_is_os_tree(int fd);
int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); int open_extension_release(const char *root, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd);
int open_extension_release_at(int rfd, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd); int open_extension_release_at(int rfd, ImageClass image_class, const char *extension, bool relax_extension_release_check, char **ret_path, int *ret_fd);

View File

@ -148,19 +148,27 @@ int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid) {
return TAKE_FD(pipefd[0]); return TAKE_FD(pipefd[0]);
} }
int import_mangle_os_tree(const char *path) { int import_mangle_os_tree_fd(int tree_fd, int userns_fd, ImportFlags flags) {
_cleanup_free_ char *child = NULL, *t = NULL, *joined = NULL; _cleanup_free_ char *child = NULL, *t = NULL, *joined = NULL;
_cleanup_closedir_ DIR *d = NULL, *cd = NULL; _cleanup_closedir_ DIR *d = NULL, *cd = NULL;
struct dirent *dent; struct dirent *dent;
struct stat st; struct stat st;
int r; int r;
assert(path); assert(tree_fd >= 0);
if (FLAGS_SET(flags, IMPORT_FOREIGN_UID) && userns_fd >= 0)
return import_mangle_os_tree_fd_foreign(tree_fd, userns_fd);
/* Some tarballs contain a single top-level directory that contains the actual OS directory tree. Try to /* Some tarballs contain a single top-level directory that contains the actual OS directory tree. Try to
* recognize this, and move the tree one level up. */ * recognize this, and move the tree one level up. */
r = path_is_os_tree(path); _cleanup_free_ char *path = NULL;
r = fd_get_path(tree_fd, &path);
if (r < 0)
return log_error_errno(r, "Failed to determine path of fd: %m");
r = fd_is_os_tree(tree_fd);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", path); return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", path);
if (r > 0) { if (r > 0) {
@ -170,9 +178,9 @@ int import_mangle_os_tree(const char *path) {
log_debug("Directory tree '%s' is not recognizable as OS tree, checking whether to rearrange it.", path); log_debug("Directory tree '%s' is not recognizable as OS tree, checking whether to rearrange it.", path);
d = opendir(path); d = xopendirat(tree_fd, /* path= */ NULL, /* flags= */ 0);
if (!d) if (!d)
return log_error_errno(r, "Failed to open directory '%s': %m", path); return log_error_errno(errno, "Failed to open directory '%s': %m", path);
errno = 0; errno = 0;
dent = readdir_no_dot(d); dent = readdir_no_dot(d);
@ -191,29 +199,29 @@ int import_mangle_os_tree(const char *path) {
errno = 0; errno = 0;
dent = readdir_no_dot(d); dent = readdir_no_dot(d);
if (dent) { if (dent) {
if (errno != 0)
return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path);
log_debug("Directory '%s' does not look like an OS tree, and has multiple children, leaving as it is.", path); log_debug("Directory '%s' does not look like an OS tree, and has multiple children, leaving as it is.", path);
return 0; return 0;
} else if (errno != 0)
return log_error_errno(errno, "Failed to iterate through directory '%s': %m", path);
_cleanup_close_ int child_fd = openat(dirfd(d), child, O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW|O_NONBLOCK);
if (child_fd < 0) {
if (IN_SET(errno, ENOTDIR, ELOOP)) {
log_debug_errno(errno, "Child '%s' of directory '%s' is not a directory, leaving things as they are.", child, path);
return 0;
}
return log_debug_errno(errno, "Failed to open file '%s/%s': %m", path, child);
} }
if (fstatat(dirfd(d), child, &st, AT_SYMLINK_NOFOLLOW) < 0) if (fstat(child_fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat file '%s/%s': %m", path, child); return log_debug_errno(errno, "Failed to stat file '%s/%s': %m", path, child);
r = stat_verify_directory(&st);
if (r < 0) {
log_debug_errno(r, "Child '%s' of directory '%s' is not a directory, leaving things as they are.", child, path);
return 0;
}
joined = path_join(path, child); joined = path_join(path, child);
if (!joined) if (!joined)
return log_oom(); return log_oom();
r = path_is_os_tree(joined);
if (r == -ENOTDIR) { r = fd_is_os_tree(child_fd);
log_debug("Directory '%s' does not look like an OS tree, and contains a single regular file only, leaving as it is.", path);
return 0;
}
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", joined); return log_error_errno(r, "Failed to determine whether '%s' is an OS tree: %m", joined);
if (r == 0) { if (r == 0) {
@ -230,7 +238,7 @@ int import_mangle_os_tree(const char *path) {
* *
* Let's now rearrange things, moving everything in the inner directory one level up */ * Let's now rearrange things, moving everything in the inner directory one level up */
cd = xopendirat(dirfd(d), child, O_NOFOLLOW); cd = take_fdopendir(&child_fd);
if (!cd) if (!cd)
return log_error_errno(errno, "Can't open directory '%s': %m", joined); return log_error_errno(errno, "Can't open directory '%s': %m", joined);
@ -238,7 +246,7 @@ int import_mangle_os_tree(const char *path) {
/* Let's rename the child to an unguessable name so that we can be sure all files contained in it can be /* Let's rename the child to an unguessable name so that we can be sure all files contained in it can be
* safely moved up and won't collide with the name. */ * safely moved up and won't collide with the name. */
r = tempfn_random(child, NULL, &t); r = tempfn_random(child, /* extra= */ NULL, &t);
if (r < 0) if (r < 0)
return log_oom(); return log_oom();
r = rename_noreplace(dirfd(d), child, dirfd(d), t); r = rename_noreplace(dirfd(d), child, dirfd(d), t);
@ -259,15 +267,68 @@ int import_mangle_os_tree(const char *path) {
r = futimens(dirfd(d), (struct timespec[2]) { st.st_atim, st.st_mtim }); r = futimens(dirfd(d), (struct timespec[2]) { st.st_atim, st.st_mtim });
if (r < 0) if (r < 0)
log_debug_errno(r, "Failed to adjust top-level timestamps '%s', ignoring: %m", path); log_debug_errno(errno, "Failed to adjust top-level timestamps '%s', ignoring: %m", path);
r = fchmod_and_chown(dirfd(d), st.st_mode, st.st_uid, st.st_gid); r = fchmod_and_chown(dirfd(d), st.st_mode, st.st_uid, st.st_gid);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to adjust top-level directory mode/ownership '%s': %m", path); return log_error_errno(r, "Failed to adjust top-level directory mode/ownership '%s': %m", path);
log_info("Successfully rearranged OS tree."); log_info("Successfully rearranged OS tree.");
return 0;
}
int import_mangle_os_tree(const char *path, int userns_fd, ImportFlags flags) {
assert(path);
_cleanup_close_ int fd = open(path, O_DIRECTORY|O_CLOEXEC|O_PATH);
if (fd < 0)
return log_error_errno(errno, "Failed to open '%s': %m", path);
return import_mangle_os_tree_fd(fd, userns_fd, flags);
}
int import_mangle_os_tree_fd_foreign(
int tree_fd,
int userns_fd) {
int r;
assert(tree_fd >= 0);
assert(userns_fd >= 0);
r = safe_fork_full(
"mangle-tree",
/* stdio_fds= */ NULL,
(int[]) { userns_fd, tree_fd }, 2,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT,
/* ret_pid= */ NULL);
if (r < 0)
return r;
if (r == 0) {
/* child */
r = namespace_enter(
/* pidns_fd= */ -EBADF,
/* mntns_fd= */ -EBADF,
/* netns_fd= */ -EBADF,
userns_fd,
/* root_fd= */ -EBADF);
if (r < 0) {
log_error_errno(r, "Failed to join user namespace: %m");
_exit(EXIT_FAILURE);
}
r = import_mangle_os_tree_fd(tree_fd, /* userns_fd= */ -EBADF, /* flags= */ 0);
if (r < 0) {
log_error_errno(r, "Failed to mangle OS tree in foreign UID mode: %m");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
return 0; return 0;
} }
bool import_validate_local(const char *name, ImportFlags flags) { bool import_validate_local(const char *name, ImportFlags flags) {

View File

@ -37,7 +37,9 @@ typedef enum ImportFlags {
int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid); int import_fork_tar_c(int tree_fd, int userns_fd, PidRef *ret_pid);
int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid); int import_fork_tar_x(int tree_fd, int userns_fd, PidRef *ret_pid);
int import_mangle_os_tree(const char *path); int import_mangle_os_tree_fd(int tree_fd, int userns_fd, ImportFlags flags);
int import_mangle_os_tree(const char *path, int userns_fd, ImportFlags flags);
int import_mangle_os_tree_fd_foreign(int tree_fd, int userns_fd);
bool import_validate_local(const char *name, ImportFlags flags); bool import_validate_local(const char *name, ImportFlags flags);

View File

@ -238,7 +238,7 @@ static int import_fs(int argc, char *argv[], void *userdata) {
return log_error_errno(r, "Failed to copy directory: %m"); return log_error_errno(r, "Failed to copy directory: %m");
} }
r = import_mangle_os_tree(dest); r = import_mangle_os_tree(dest, /* userns_fd= */ -EBADF, /* flags= */ 0);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -199,7 +199,7 @@ static int tar_import_finish(TarImport *i) {
assert_se(d = i->temp_path ?: i->local); assert_se(d = i->temp_path ?: i->local);
r = import_mangle_os_tree(d); r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -497,7 +497,7 @@ static void tar_pull_job_on_finished(PullJob *j) {
tar_pull_report_progress(i, TAR_FINALIZING); tar_pull_report_progress(i, TAR_FINALIZING);
r = import_mangle_os_tree(i->local); r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags);
if (r < 0) if (r < 0)
goto finish; goto finish;
@ -523,7 +523,7 @@ static void tar_pull_job_on_finished(PullJob *j) {
tar_pull_report_progress(i, TAR_FINALIZING); tar_pull_report_progress(i, TAR_FINALIZING);
r = import_mangle_os_tree(i->temp_path); r = import_mangle_os_tree_fd(i->tree_fd, i->userns_fd, i->flags);
if (r < 0) if (r < 0)
goto finish; goto finish;

View File

@ -1762,8 +1762,14 @@ int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) {
int r; int r;
assert(link); assert(link);
assert(link->manager);
assert(link->network); assert(link->network);
/* On stopping/restarting networkd, we may drop IPv6 connectivity (which depends on KeepConfiguration=
* setting). Do not (re)start DHCPv4 client in that case. See issue #39299. */
if (link->manager->state != MANAGER_RUNNING)
return 0;
if (!link->dhcp_client) if (!link->dhcp_client)
return 0; return 0;

View File

@ -2225,7 +2225,7 @@ static int link_update_flags(Link *link, sd_netlink_message *message) {
if (!had_carrier && link_has_carrier(link)) if (!had_carrier && link_has_carrier(link))
r = link_carrier_gained(link); r = link_carrier_gained(link);
else if (had_carrier && !link_has_carrier(link)) else if (had_carrier && !link_has_carrier(link))
link_carrier_lost(link); r = link_carrier_lost(link);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -5,12 +5,16 @@
#include "sd-json.h" #include "sd-json.h"
#include "alloc-util.h" #include "alloc-util.h"
#include "chase.h"
#include "fd-util.h"
#include "fileio.h" #include "fileio.h"
#include "format-util.h" #include "format-util.h"
#include "io-util.h"
#include "log.h" #include "log.h"
#include "nspawn.h" #include "nspawn.h"
#include "machine-bind-user.h" #include "machine-bind-user.h"
#include "nspawn-bind-user.h" #include "nspawn-bind-user.h"
#include "strv.h"
#include "user-record.h" #include "user-record.h"
#include "group-record.h" #include "group-record.h"
#include "path-util.h" #include "path-util.h"
@ -67,6 +71,40 @@ static int write_and_symlink(
return 0; return 0;
} }
static int write_membership(const char *root, const char *user, const char *group) {
int r;
assert(user);
assert(group);
_cleanup_free_ char *membership = strjoin(user, ":", group, ".membership");
if (!membership)
return log_oom();
_cleanup_free_ char *p = path_join("/run/host/userdb/", membership);
if (!p)
return log_oom();
_cleanup_close_ int fd = chase_and_open(
p,
root,
CHASE_PREFIX_ROOT|CHASE_NO_AUTOFS,
O_WRONLY|O_CREAT|O_CLOEXEC,
/* ret_path= */ NULL);
if (fd < 0)
return log_error_errno(errno, "Failed to create %s: %m", p);
r = userns_chown_at(fd, /* fname= */ NULL, /* uid= */ 0, /* gid= */ 0, /* flags= */ 0);
if (r < 0)
return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
r = loop_write(fd, "{}\n", SIZE_MAX);
if (r < 0)
return log_error_errno(r, "Failed to write empty JSON object into %s: %m", p);
return 0;
}
int bind_user_setup(const MachineBindUserContext *c, const char *root) { int bind_user_setup(const MachineBindUserContext *c, const char *root) {
static const UserRecordLoadFlags strip_flags = /* Removes privileged info */ static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
USER_RECORD_LOAD_MASK_PRIVILEGED| USER_RECORD_LOAD_MASK_PRIVILEGED|
@ -130,6 +168,12 @@ int bind_user_setup(const MachineBindUserContext *c, const char *root) {
if (r < 0) if (r < 0)
return r; return r;
STRV_FOREACH(u, stripped_group->members) {
r = write_membership(root, *u, stripped_group->group_name);
if (r < 0)
return r;
}
/* Third, write out user shadow data. i.e. extract privileged info from user record */ /* Third, write out user shadow data. i.e. extract privileged info from user record */
r = user_record_clone(d->payload_user, shadow_flags, &shadow_user); r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
if (r < 0) if (r < 0)
@ -161,6 +205,12 @@ int bind_user_setup(const MachineBindUserContext *c, const char *root) {
0); 0);
if (r < 0) if (r < 0)
return r; return r;
STRV_FOREACH(g, stripped_user->member_of) {
r = write_membership(root, stripped_user->user_name, *g);
if (r < 0)
return r;
}
} }
return 1; return 1;

View File

@ -240,6 +240,7 @@ static MachineCredentialContext arg_credentials = {};
static char **arg_bind_user = NULL; static char **arg_bind_user = NULL;
static char *arg_bind_user_shell = NULL; static char *arg_bind_user_shell = NULL;
static bool arg_bind_user_shell_copy = false; static bool arg_bind_user_shell_copy = false;
static char **arg_bind_user_groups = NULL;
static bool arg_suppress_sync = false; static bool arg_suppress_sync = false;
static char *arg_settings_filename = NULL; static char *arg_settings_filename = NULL;
static Architecture arg_architecture = _ARCHITECTURE_INVALID; static Architecture arg_architecture = _ARCHITECTURE_INVALID;
@ -283,6 +284,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_cpu_set, cpu_set_done);
STATIC_DESTRUCTOR_REGISTER(arg_sysctl, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_sysctl, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_groups, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_settings_filename, freep); STATIC_DESTRUCTOR_REGISTER(arg_settings_filename, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_background, freep); STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
@ -429,6 +431,8 @@ static int help(void) {
" --bind-user=NAME Bind user from host to container\n" " --bind-user=NAME Bind user from host to container\n"
" --bind-user-shell=BOOL|PATH\n" " --bind-user-shell=BOOL|PATH\n"
" Configure the shell to use for --bind-user= users\n" " Configure the shell to use for --bind-user= users\n"
" --bind-user-group=GROUP\n"
" Add an auxiliary group to --bind-user= users\n"
"\n%3$sInput/Output:%4$s\n" "\n%3$sInput/Output:%4$s\n"
" --console=MODE Select how stdin/stdout/stderr and /dev/console are\n" " --console=MODE Select how stdin/stdout/stderr and /dev/console are\n"
" set up for the container.\n" " set up for the container.\n"
@ -660,6 +664,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_LOAD_CREDENTIAL, ARG_LOAD_CREDENTIAL,
ARG_BIND_USER, ARG_BIND_USER,
ARG_BIND_USER_SHELL, ARG_BIND_USER_SHELL,
ARG_BIND_USER_GROUP,
ARG_SUPPRESS_SYNC, ARG_SUPPRESS_SYNC,
ARG_IMAGE_POLICY, ARG_IMAGE_POLICY,
ARG_BACKGROUND, ARG_BACKGROUND,
@ -738,6 +743,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL },
{ "bind-user", required_argument, NULL, ARG_BIND_USER }, { "bind-user", required_argument, NULL, ARG_BIND_USER },
{ "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL }, { "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL },
{ "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP },
{ "suppress-sync", required_argument, NULL, ARG_SUPPRESS_SYNC }, { "suppress-sync", required_argument, NULL, ARG_SUPPRESS_SYNC },
{ "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
{ "background", required_argument, NULL, ARG_BACKGROUND }, { "background", required_argument, NULL, ARG_BACKGROUND },
@ -1514,6 +1520,15 @@ static int parse_argv(int argc, char *argv[]) {
break; break;
} }
case ARG_BIND_USER_GROUP:
if (!valid_user_group_name(optarg, /* flags= */ 0))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg);
if (strv_extend(&arg_bind_user_groups, optarg) < 0)
return log_oom();
break;
case ARG_SUPPRESS_SYNC: case ARG_SUPPRESS_SYNC:
r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync); r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync);
if (r < 0) if (r < 0)
@ -1689,12 +1704,16 @@ static int verify_arguments(void) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "AmbientCapability= setting is not useful for boot mode."); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "AmbientCapability= setting is not useful for boot mode.");
} }
/* Drop duplicate --bind-user= entries */ /* Drop duplicate --bind-user= and --bind-user-group= entries */
strv_uniq(arg_bind_user); strv_uniq(arg_bind_user);
strv_uniq(arg_bind_user_groups);
if (arg_bind_user_shell && strv_isempty(arg_bind_user)) if (arg_bind_user_shell && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user="); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user=");
if (!strv_isempty(arg_bind_user_groups) && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-group= without --bind-user=");
r = custom_mount_check_all(); r = custom_mount_check_all();
if (r < 0) if (r < 0)
return r; return r;
@ -1733,7 +1752,7 @@ static int in_child_chown(void) {
return IN_SET(arg_userns_mode, USER_NAMESPACE_PICK, USER_NAMESPACE_FIXED); return IN_SET(arg_userns_mode, USER_NAMESPACE_PICK, USER_NAMESPACE_FIXED);
} }
static int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags) { int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags) {
assert(fd >= 0 || fd == AT_FDCWD); assert(fd >= 0 || fd == AT_FDCWD);
if (!in_child_chown()) if (!in_child_chown())
@ -1756,6 +1775,9 @@ static int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int
return -EOVERFLOW; return -EOVERFLOW;
} }
if (isempty(fname))
flags |= AT_EMPTY_PATH;
return RET_NERRNO(fchownat(fd, strempty(fname), uid, gid, flags)); return RET_NERRNO(fchownat(fd, strempty(fname), uid, gid, flags));
} }
@ -4020,6 +4042,7 @@ static int outer_child(
arg_bind_user_shell, arg_bind_user_shell,
arg_bind_user_shell_copy, arg_bind_user_shell_copy,
"/run/host/home", "/run/host/home",
arg_bind_user_groups,
&bind_user_context); &bind_user_context);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -3,6 +3,7 @@
#include "shared-forward.h" #include "shared-forward.h"
int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags);
int userns_lchown(const char *p, uid_t uid, gid_t gid); int userns_lchown(const char *p, uid_t uid, gid_t gid);
int userns_mkdir(const char *root, const char *path, mode_t mode, uid_t uid, gid_t gid); int userns_mkdir(const char *root, const char *path, mode_t mode, uid_t uid, gid_t gid);
int make_run_host(const char *root); int make_run_host(const char *root);

View File

@ -94,6 +94,7 @@ static int convert_user(
const char *shell, const char *shell,
bool shell_copy, bool shell_copy,
const char *home_mount_directory, const char *home_mount_directory,
char **groups,
UserRecord **ret_converted_user, UserRecord **ret_converted_user,
GroupRecord **ret_converted_group) { GroupRecord **ret_converted_group) {
@ -145,6 +146,7 @@ static int convert_user(
SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)), SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)),
SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")), SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
JSON_BUILD_PAIR_STRING_NON_EMPTY("shell", shell), JSON_BUILD_PAIR_STRING_NON_EMPTY("shell", shell),
SD_JSON_BUILD_PAIR_STRV("memberOf", groups),
SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT( SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)), SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)),
SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh)))))); SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh))))));
@ -212,6 +214,7 @@ int machine_bind_user_prepare(
const char *bind_user_shell, const char *bind_user_shell,
bool bind_user_shell_copy, bool bind_user_shell_copy,
const char *bind_user_home_mount_directory, const char *bind_user_home_mount_directory,
char **bind_user_groups,
MachineBindUserContext **ret) { MachineBindUserContext **ret) {
_cleanup_(machine_bind_user_context_freep) MachineBindUserContext *c = NULL; _cleanup_(machine_bind_user_context_freep) MachineBindUserContext *c = NULL;
@ -288,6 +291,7 @@ int machine_bind_user_prepare(
bind_user_shell, bind_user_shell,
bind_user_shell_copy, bind_user_shell_copy,
bind_user_home_mount_directory, bind_user_home_mount_directory,
bind_user_groups,
&cu, &cg); &cu, &cg);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -28,4 +28,5 @@ int machine_bind_user_prepare(
const char *bind_user_shell, const char *bind_user_shell,
bool bind_user_shell_copy, bool bind_user_shell_copy,
const char *bind_user_home_mount_directory, const char *bind_user_home_mount_directory,
char **bind_user_groups,
MachineBindUserContext **ret); MachineBindUserContext **ret);

View File

@ -18,6 +18,7 @@
#include "format-table.h" #include "format-table.h"
#include "format-util.h" #include "format-util.h"
#include "fs-util.h" #include "fs-util.h"
#include "io-util.h"
#include "log.h" #include "log.h"
#include "main-func.h" #include "main-func.h"
#include "mkdir.h" #include "mkdir.h"
@ -1196,6 +1197,31 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
return r; return r;
} }
static int write_membership(int dir_fd, const char *dir, const char *user, const char *group) {
int r;
assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
assert(dir);
assert(user);
assert(group);
_cleanup_free_ char *membership = strjoin(user, ":", group, ".membership");
if (!membership)
return log_oom();
_cleanup_close_ int fd = openat(dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
if (fd < 0)
return log_error_errno(errno, "Failed to create %s/%s: %m", dir, membership);
r = loop_write(fd, "{}\n", SIZE_MAX);
if (r < 0)
return log_error_errno(r, "Failed to write empty JSON object into %s/%s: %m", dir, membership);
log_info("Installed %s/%s from credential.", dir, membership);
return 0;
}
static int load_credential_one( static int load_credential_one(
int credential_dir_fd, int credential_dir_fd,
const char *name, const char *name,
@ -1430,27 +1456,15 @@ static int load_credential_one(
if (ur) if (ur)
STRV_FOREACH(g, ur->member_of) { STRV_FOREACH(g, ur->member_of) {
_cleanup_free_ char *membership = strjoin(ur->user_name, ":", *g); r = write_membership(*userdb_dir_fd, userdb_dir, ur->user_name, *g);
if (!membership) if (r < 0)
return log_oom(); return r;
_cleanup_close_ int fd = openat(*userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
if (fd < 0)
return log_error_errno(errno, "Failed to create %s: %m", membership);
log_info("Installed %s/%s from credential.", userdb_dir, membership);
} }
else else
STRV_FOREACH(u, gr->members) { STRV_FOREACH(u, gr->members) {
_cleanup_free_ char *membership = strjoin(*u, ":", gr->group_name); r = write_membership(*userdb_dir_fd, userdb_dir, *u, gr->group_name);
if (!membership) if (r < 0)
return log_oom(); return r;
_cleanup_close_ int fd = openat(*userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
if (fd < 0)
return log_error_errno(errno, "Failed to create %s: %m", membership);
log_info("Installed %s/%s from credential.", userdb_dir, membership);
} }
if (ur && user_record_disposition(ur) == USER_REGULAR) { if (ur && user_record_disposition(ur) == USER_REGULAR) {

View File

@ -143,6 +143,7 @@ static bool arg_notify_ready = true;
static char **arg_bind_user = NULL; static char **arg_bind_user = NULL;
static char *arg_bind_user_shell = NULL; static char *arg_bind_user_shell = NULL;
static bool arg_bind_user_shell_copy = false; static bool arg_bind_user_shell_copy = false;
static char **arg_bind_user_groups = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_directory, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
@ -164,6 +165,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm_state_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep); STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_groups, strv_freep);
static int help(void) { static int help(void) {
_cleanup_free_ char *link = NULL; _cleanup_free_ char *link = NULL;
@ -227,6 +229,8 @@ static int help(void) {
" --bind-user=NAME Bind user from host to virtual machine\n" " --bind-user=NAME Bind user from host to virtual machine\n"
" --bind-user-shell=BOOL|PATH\n" " --bind-user-shell=BOOL|PATH\n"
" Configure the shell to use for --bind-user= users\n" " Configure the shell to use for --bind-user= users\n"
" --bind-user-group=GROUP\n"
" Add an auxiliary group to --bind-user= users\n"
"\n%3$sIntegration:%4$s\n" "\n%3$sIntegration:%4$s\n"
" --forward-journal=FILE|DIR\n" " --forward-journal=FILE|DIR\n"
" Forward the VM's journal to the host\n" " Forward the VM's journal to the host\n"
@ -303,6 +307,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NOTIFY_READY, ARG_NOTIFY_READY,
ARG_BIND_USER, ARG_BIND_USER,
ARG_BIND_USER_SHELL, ARG_BIND_USER_SHELL,
ARG_BIND_USER_GROUP,
}; };
static const struct option options[] = { static const struct option options[] = {
@ -354,6 +359,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "notify-ready", required_argument, NULL, ARG_NOTIFY_READY }, { "notify-ready", required_argument, NULL, ARG_NOTIFY_READY },
{ "bind-user", required_argument, NULL, ARG_BIND_USER }, { "bind-user", required_argument, NULL, ARG_BIND_USER },
{ "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL }, { "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL },
{ "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP },
{} {}
}; };
@ -715,6 +721,15 @@ static int parse_argv(int argc, char *argv[]) {
break; break;
} }
case ARG_BIND_USER_GROUP:
if (!valid_user_group_name(optarg, /* flags= */ 0))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg);
if (strv_extend(&arg_bind_user_groups, optarg) < 0)
return log_oom();
break;
case '?': case '?':
return -EINVAL; return -EINVAL;
@ -722,12 +737,16 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached(); assert_not_reached();
} }
/* Drop duplicate --bind-user= entries */ /* Drop duplicate --bind-user= and --bind-user-group= entries */
strv_uniq(arg_bind_user); strv_uniq(arg_bind_user);
strv_uniq(arg_bind_user_groups);
if (arg_bind_user_shell && strv_isempty(arg_bind_user)) if (arg_bind_user_shell && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user="); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user=");
if (!strv_isempty(arg_bind_user_groups) && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-group= without --bind-user=");
if (argc > optind) { if (argc > optind) {
arg_kernel_cmdline_extra = strv_copy(argv + optind); arg_kernel_cmdline_extra = strv_copy(argv + optind);
if (!arg_kernel_cmdline_extra) if (!arg_kernel_cmdline_extra)
@ -1840,6 +1859,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
arg_bind_user_shell, arg_bind_user_shell,
arg_bind_user_shell_copy, arg_bind_user_shell_copy,
"/run/vmhost/home", "/run/vmhost/home",
arg_bind_user_groups,
&bind_user_context); &bind_user_context);
if (r < 0) if (r < 0)
return r; return r;

View File

@ -162,7 +162,7 @@ rm -f /tmp/core.{output,redirected}
# Unprivileged stuff # Unprivileged stuff
# Related issue: https://github.com/systemd/systemd/issues/26912 # Related issue: https://github.com/systemd/systemd/issues/26912
UNPRIV_CMD=(systemd-run --user --wait --pipe -M "testuser@.host" --) UNPRIV_CMD=(systemd-run --user --wait --pipe -M "testuser@.host" -E SYSTEMD_PAGER --)
# Trigger a couple of coredumps as an unprivileged user # Trigger a couple of coredumps as an unprivileged user
"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGTRAP" "${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGTRAP"
"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGABRT" "${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGABRT"