Merge pull request #30840 from AdrianVovk/homed-bulk-v2

Homed bulk directories v2
This commit is contained in:
Luca Boccassi
2024-02-19 13:39:26 +00:00
committed by GitHub
38 changed files with 1667 additions and 136 deletions
-4
View File
@@ -2245,10 +2245,6 @@ Features:
can easily set overall quota for all users
- on login, if we can't fallocate initially, but rebalance is on, then allow
login in discard mode, then immediately rebalance, then turn off discard
- extend user records with optional "bulk" data. Specifically, a user
avatar/photo or so. This data should be stored along with the user record,
but probably shouldn't be part of the record itself, since it might be
large.
- add "homectl unbind" command to remove local user record of an inactive
home dir
+23 -4
View File
@@ -74,6 +74,10 @@ the following extensions are envisioned:
Similar to JSON User Records there are also
[JSON Group Records](GROUP_RECORD.md) that encapsulate UNIX groups.
JSON User Records are not suitable for storing all identity information about
the user, such as binary data or large unstructured blobs of text. These parts
of a user's identity should be stored in the [Blob Directories](USER_RECORD_BLOB_DIRS.md).
JSON User Records may be transferred or written to disk in various protocols
and formats. To inquire about such records defined on the local system use the
[User/Group Lookup API via Varlink](USER_GROUP_API.md). User/group records may
@@ -230,6 +234,16 @@ optional, when unset the user should not be considered part of any realm. A
user record with a realm set is never compatible (for the purpose of updates,
see above) with a user record without one set, even if the `userName` field matches.
`blobDirectory` → The absolute path to a world-readable copy of the user's blob
directory. See [Blob Directories](USER_RECORD_BLOB_DIRS.md) for more details.
`blobManifest` → An object, which maps valid blob directory filenames (see
[Blob Directories](USER_RECORD_BLOB_DIRS.md) for requirements) to SHA256 hashes
formatted as hex strings. This exists for the purpose of including the contents
of the blob directory in the record's signature. Managers that support blob
directories and utilize signed user records (like `systemd-homed`) should use
this field to verify the contents of the blob directory whenever appropriate.
`realName` → The real name of the user, a string. This should contain the
user's real ("human") name, and corresponds loosely to the GECOS field of
classic UNIX user records. When converting a `struct passwd` to a JSON user
@@ -754,7 +768,7 @@ These two are the only two fields specific to this section. All other fields
that may be used in this section are identical to the equally named ones in the
`regular` section (i.e. at the top-level object). Specifically, these are:
`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
`blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
`preferredLanguage`, `additionalLanguages`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`,
`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`,
@@ -806,9 +820,9 @@ The following fields are defined in the `binding` section. They all have an
identical format and override their equally named counterparts in the `regular`
and `perMachine` sections:
`imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, `fileSystemUuid`,
`uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, `luksCipherMode`,
`luksVolumeKeySize`.
`blobDirectory`, `imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`,
`fileSystemUuid`, `uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`,
`luksCipherMode`, `luksVolumeKeySize`.
## Fields in the `status` section
@@ -1098,6 +1112,7 @@ A fully featured user record associated with a home directory managed by
"fileSystemUuid" : "758e88c8-5851-4a2a-b88f-e7474279c111",
"gid" : 60232,
"homeDirectory" : "/home/grobie",
"blobDirectory" : "/var/cache/systemd/homed/grobie/",
"imagePath" : "/home/grobie.home",
"luksCipher" : "aes",
"luksCipherMode" : "xts-plain64",
@@ -1108,6 +1123,10 @@ A fully featured user record associated with a home directory managed by
"uid" : 60232
}
},
"blobManifest" : {
"avatar" : "c0636851d25a62d817ff7da4e081d1e646e42c74d0ecb53425f75fcf1ba43b52",
"login-background" : "da7ad0222a6edbc6cd095149c72d38d92fd3114f606e4b57469857ef47fade18"
},
"disposition" : "regular",
"enforcePasswordPolicy" : false,
"lastChangeUSec" : 1565950024279735,
+128
View File
@@ -0,0 +1,128 @@
---
title: User Record Blob Directories
category: Users, Groups and Home Directories
layout: default
SPDX-License-Identifier: LGPL-2.1-or-later
---
# User Record Blob Directories
The blob directories are for storing binary or unstructured data that would
otherwise be stored in [JSON User Records](USER_RECORD.md). For instance,
this includes image files such as the user's avatar picture. This data,
like most of the user record, will be made publicly available to the
system.
The JSON User Record specifies the location of the blob directory via the
`blobDirectory` field. If the field is unset, then there is no blob directory
and thus no blob files to look for. Note that `blobDirectory` can exist in the
`regular`, `perMachine`, and `status` sections. The blob directory is completely
owned and managed by the service that owns the rest of the user record (as
specified in the `service` field).
For consistency, blob directories have certain restrictions placed on them
that may be enforced by their owning service. Services implementing blob
directories are free to ignore these restrictions, but software that wishes
to store some of its data in blob directories must adhere to the following:
* The directory only contains regular files; no sub-directories or any special
files are permitted.
* Filenames inside of the directory are restricted to
[URI Unreserved Characters](https://www.rfc-editor.org/rfc/rfc3986#section-2.3)
(alphanumeric, `-`, `.`, `_`, and `~`), and must not start with a dot.
* The total size of the directory should not exceed 64M.
* File ownership and permissions will not be preserved. The service may reset
the mode of the files to 0644, and ownership to whatever it wishes.
* Timestamps, xattrs, ACLs, or any other metadata on the files will not be preserved.
Services are required to ensure that the directory and its contents are
world-readable. Aside from this requirement, services are free to provide
the directory and its contents in whatever manner they like, including but
not limited to synthesizing the directory at runtime using external data
or keeping around multiple copies. Thus, only the service that owns the
directory is permitted to write to this directory in any way: for all
other software the directory is strictly read-only.
Services may choose to provide some way to change user records. Services
that provide this functionality should support changing the blob directory also.
Care must be taken to avoid exposing sensitive data to malicious clients. This
includes but is not limited to disallowing symlinks and using file descriptors
(excluding O_PATH!) to ensure that the client actually has permission to access
the data it wants the service to publish.
Services that make use of the `signature` section in the records they manage
should enforce `blobManifest`. This ensures that the contents of the blob directory
are part of the cryptographically signed data.
## Known Files
Various files in the blob directories have known semantic meanings.
The following files are currently defined:
`avatar` → An image file that should be used as the user's avatar picture.
The exact file type and resolution of this image are left unspecified,
and requirements will depend on the capabilities of the components that will
display it. However, we suggest the use of commonly-supported picture formats
(i.e. PNG or JPEG) and a resolution of 512 x 512. This image should not have any
transparency. If missing, of an incompatible file type, or otherwise unusable,
then the user does not have a profile picture and a default will be used instead.
`login-background` → An image file that will be used as the user's background on the
login screen (i.e. in GDM). The exact file type and resolution are left unspecified
and are ultimately up to the components that will render this background image. This
image should not have any transparency. If missing, of an incompatible file type, or
otherwise unusable, a fallback background of some kind will be used.
## Extending These Directories
Like JSON User Records, the blob directories are intended to be extendable for
various applications. In general, subsystems are free to introduce their own
files, as long as:
* The requirements listed above are all met.
* Care is taken to avoid namespace clashes. Please prefix your file names with
a short identifier of your project to avoid ambiguities and incompatibilities.
* This specification is supposed to be a living specification. If you need
additional files, please consider defining them upstream for inclusion in
this specification. If they are reasonably universally useful, it would be
best to list them here.
## Examples
The simplest way to define a user record is via the drop-in directories (as documented
in [nss-systemd(8)](https://www.freedesktop.org/software/systemd/man/latest/nss-systemd.html)
and [systemd-userdb.service(8)](https://www.freedesktop.org/software/systemd/man/latest/systemd-userdbd.service.html)).
Such records can have blob directories by simply referring to some persistent
place from the record, possibly next to the record itself. For instance,
`/etc/userdb/grobie.user` may contain:
```json
{
"userName": "grobie",
"disposition": "regular",
"homeDirectory": "/home/grobie",
"blobDirectory": "/etc/userdb/grobie.blob/",
}
```
In this case, `/etc/userdb/grobie.blob/` will be the blob directory for the
user `grobie`.
A more complicated case is a home directory managed by `systemd-homed.service`.
When it manages a home directory, it maintains and synchronizes two separate
blob directories: one belonging to the system in `/var/cache/systemd/home`,
and another belonging to the home directory in `~/.identity-blob`. The system
blob directory ensures that the blob data is available while the home directory
is encrypted or otherwise unavailable, and the home blob directory ensures that
the user account remains portable between systems. To implement this behavior,
`systemd-homed.service` always sets `blobDirectory` to the system blob directory
in the `binding` section of the user record (i.e. this is _not_ persisted to
`~/.identity`). If some client tries to update the user record with a new blob
directory, `systemd-homed.service` will copy the updated blob directory into both
the system and home blob locations.
+34
View File
@@ -507,6 +507,40 @@
<xi:include href="version-info.xml" xpointer="v247"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--blob=</option><replaceable>PATH</replaceable></term>
<term><option>-b</option> <replaceable>PATH</replaceable></term>
<term><option>--blob=</option><replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable></term>
<term><option>-b</option> <replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable></term>
<listitem><para>Accepts either a directory path, or a file name followed by a file path. If just a
directory path is specified, then the user's entire blob directory is replaced the specified path.
Note that this replacement is performed before per-file manipulations are applied, which means these per-file
manipulations will be applied on top of the specified directory. If a filename and file path are specified, then
the single specified blob file will be overwritten with the specified path. If completely blank, the entire blob
directory is emptied out (which also resets all previous blob-related flags up to this point). If a filename is
specified but the corresponding path is blank, that single file will be deleted from the blob directory. All changes
are performed in temporary copies of the specified files in directories, which means that the originals specified on
the command line are not modified. See <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink>
for more information about blob directories.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--avatar=</option><replaceable>PATH</replaceable></term>
<term><option>--login-background=</option><replaceable>PATH</replaceable></term>
<listitem><para>Accept a file path. If set, the specifified file is used to overwrite the
corresponding file in the user's blob directory. If blank, the corresponding file is deleted
from the blob directory. Essentially, these options are shortcuts to
<option>--blob=</option><replaceable>FILENAME</replaceable>=<replaceable>PATH</replaceable>
for the known filenames defined in
<ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink>.</para>
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--locked=</option><replaceable>BOOLEAN</replaceable></term>
+45 -7
View File
@@ -72,6 +72,9 @@ node /org/freedesktop/home1 {
RegisterHome(in s user_record);
UnregisterHome(in s user_name);
CreateHome(in s user_record);
CreateHomeEx(in s user_record,
in a{sh} blobs,
in t flags);
RealizeHome(in s user_name,
in s secret);
RemoveHome(in s user_name);
@@ -81,6 +84,9 @@ node /org/freedesktop/home1 {
AuthenticateHome(in s user_name,
in s secret);
UpdateHome(in s user_record);
UpdateHomeEx(in s user_record,
in a{sh} blobs,
in t flags);
ResizeHome(in s user_name,
in t size,
in s secret);
@@ -151,6 +157,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="CreateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="CreateHomeEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RealizeHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RemoveHome()"/>
@@ -161,6 +169,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="UpdateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UpdateHomeEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ResizeHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ChangePasswordHome()"/>
@@ -265,6 +275,19 @@ node /org/freedesktop/home1 {
the user record locally and creates a home directory matching it, depending on the settings specified
in the record in combination with local configuration.</para>
<para><function>CreateHomeEx()</function> is like <function>CreateHome()</function>, but it allows the
home directory to be created with a pre-populated blob directory (see
<ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink> for more info).
This can be done via the dictionary passed as the <varname>blobs</varname> argument to this method: the values
are open file descriptors to regular files, and the keys are the filenames that should contain their respective
file's data in the blob directory. Note that for security reasons, the file descriptors passed into this method
must have enough privileges to read their target file and thus cannot be <literal>O_PATH</literal>; this
is done to ensure the caller is actually permitted to read the file they are asking to publish in the
blob directories. If the user record passed as the first argument contains a <literal>blobManifest</literal>
field it will be enforced; otherwise, a <literal>blobManifest</literal> field will be generated and inserted
into the record. The <varname>flags</varname> argument may be used for future expansion, but for now
pass 0.</para>
<para><function>RealizeHome()</function> creates a home directory whose user record is already
registered locally. This takes a user name plus a user record consisting only of the
<literal>secret</literal> section. Invoking <function>RegisterHome()</function> followed by
@@ -308,6 +331,14 @@ node /org/freedesktop/home1 {
as the storage resized using <function>ResizeHome()</function>. This method is equivalent to
<function>Update()</function> on the <classname>org.freedesktop.home1.Home</classname> interface.</para>
<para><function>UpdateHomeEx()</function> is like <function>UpdateHome()</function>, but it allows for
changes to the blob directory (see <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob
Directories</ulink> for more info). The <varname>blobs</varname> argument works in the same way as
<function>CreateHomeEx()</function>, so check there for details. The new blob directory contents passed into
this method will completely replace the user's existing blob directory. The <varname>flags</varname> argument
may be used for future expansion, but for now pass 0. This method is equivalent to <function>UpdateEx()</function>
on the <classname>org.freedesktop.home1.Home</classname> interface.</para>
<para><function>ResizeHome()</function> resizes the storage associated with a user record. Takes a user
name, a disk size in bytes and a user record consisting only of the <literal>secret</literal> section
as argument. If the size is specified as <constant>UINT64_MAX</constant> the storage is resized to the
@@ -438,6 +469,9 @@ node /org/freedesktop/home1/home {
Fixate(in s secret);
Authenticate(in s secret);
Update(in s user_record);
UpdateEx(in s user_record,
in a{sh} blobs,
in t flags);
Resize(in t size,
in s secret);
ChangePassword(in s new_secret,
@@ -504,6 +538,8 @@ node /org/freedesktop/home1/home {
<variablelist class="dbus-method" generated="True" extra-ref="Update()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UpdateEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Resize()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ChangePassword()"/>
@@ -540,11 +576,11 @@ node /org/freedesktop/home1/home {
<para><function>Activate()</function>, <function>ActivateIfReferenced()</function>,
<function>Deactivate()</function>, <function>Unregister()</function>, <function>Realize()</function>,
<function>Remove()</function>, <function>Fixate()</function>, <function>Authenticate()</function>,
<function>Update()</function>, <function>Resize()</function>, <function>ChangePassword()</function>,
<function>Lock()</function>, <function>Unlock()</function>, <function>Acquire()</function>,
<function>Ref()</function>, <function>RefUnrestricted()</function>, <function>Release()</function>,
<function>InhibitSuspend()</function> operate like their matching counterparts on the
<classname>org.freedesktop.home1.Manager</classname> interface (see above). The main difference is that
<function>Update()</function>, <function>UpdateEx()</function>, <function>Resize()</function>,
<function>ChangePassword()</function>, <function>Lock()</function>, <function>Unlock()</function>,
<function>Acquire()</function>, <function>Ref()</function>, <function>RefUnrestricted()</function>,
<function>Release()</function>, <function>InhibitSuspend()</function> operate like their matching counterparts
on the <classname>org.freedesktop.home1.Manager</classname> interface (see above). The main difference is that
they are methods of the home directory objects, and hence carry no additional user name
parameter. Which of the two flavors of methods to call depends on the handles to the user known on the
client side: if only the user name is known, it's preferable to use the methods on the manager object
@@ -575,11 +611,13 @@ node /org/freedesktop/home1/home {
<title>History</title>
<refsect2>
<title>The Manager Object</title>
<para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function> wer added in version 256.</para>
<para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>
<para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function> and <function>RefUnrestricted()</function> were added in version 256.</para>
<para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function>, <function>RefUnrestricted()</function>, and
<function>UpdateEx()</function> were added in version 256.</para>
</refsect2>
</refsect1>
+4
View File
@@ -46,6 +46,10 @@
url="https://systemd.io/USER_GROUP_API">User/Group Record Lookup API via Varlink</ulink>, and thus may be
browsed with
<citerefentry><refentrytitle>userdbctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<para><filename>systemd-homed.service</filename> also manages blob directories for each home directory
it manages. See <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink>
for more details.</para>
</refsect1>
<refsect1>
+6 -3
View File
@@ -106,7 +106,10 @@ _homectl() {
--cifs-service
--stop-delay
--kill-processes
--auto-login'
--auto-login
-b --blob
--avatar
--login-background'
)
if __contains_word "$prev" ${OPTS[ARG]}; then
@@ -117,7 +120,7 @@ _homectl() {
--machine|-M)
comps=$( __get_machines )
;;
--identity|--image-path)
--identity|--image-path|--avatar|--login-background)
comps=$(compgen -A file -- "$cur" )
compopt -o filenames
;;
@@ -130,7 +133,7 @@ _homectl() {
--locked|--enforce-password-policy|--password-change-now|--luks-discard|--luks-offline-discard|--nosuid|--nodev|--noexec|--kill-processes|--auto-login)
comps='yes no'
;;
-d|--home-dir|--skel)
-d|--home-dir|--skel|-b|--blob)
comps=$(compgen -A directory -- "$cur" )
compopt -o dirnames
;;
+47
View File
@@ -464,6 +464,53 @@ int close_all_fds(const int except[], size_t n_except) {
return r;
}
int pack_fds(int fds[], size_t n_fds) {
if (n_fds <= 0)
return 0;
/* Shifts around the fds in the provided array such that they
* all end up packed next to each-other, in order, starting
* from SD_LISTEN_FDS_START. This must be called after close_all_fds();
* it is likely to freeze up otherwise. You should probably use safe_fork_full
* with FORK_CLOSE_ALL_FDS|FORK_PACK_FDS set, to ensure that this is done correctly.
* The fds array is modified in place with the new FD numbers. */
assert(fds);
for (int start = 0;;) {
int restart_from = -1;
for (int i = start; i < (int) n_fds; i++) {
int nfd;
/* Already at right index? */
if (fds[i] == i + 3)
continue;
nfd = fcntl(fds[i], F_DUPFD, i + 3);
if (nfd < 0)
return -errno;
safe_close(fds[i]);
fds[i] = nfd;
/* Hmm, the fd we wanted isn't free? Then
* let's remember that and try again from here */
if (nfd != i + 3 && restart_from < 0)
restart_from = i;
}
if (restart_from < 0)
break;
start = restart_from;
}
assert(fds[0] == 3);
return 0;
}
int same_fd(int a, int b) {
struct stat sta, stb;
pid_t pid;
+2
View File
@@ -77,6 +77,8 @@ int get_max_fd(void);
int close_all_fds(const int except[], size_t n_except);
int close_all_fds_without_malloc(const int except[], size_t n_except);
int pack_fds(int fds[], size_t n);
int same_fd(int a, int b);
void cmsg_close_all(struct msghdr *mh);
+16 -3
View File
@@ -1468,7 +1468,7 @@ static int fork_flags_to_signal(ForkFlags flags) {
int safe_fork_full(
const char *name,
const int stdio_fds[3],
const int except_fds[],
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
pid_t *ret_pid) {
@@ -1697,6 +1697,19 @@ int safe_fork_full(
}
}
if (flags & FORK_PACK_FDS) {
/* FORK_CLOSE_ALL_FDS ensures that except_fds are the only FDs >= 3 that are
* open, this is including the log. This is required by pack_fds, which will
* get stuck in an infinite loop of any FDs other than except_fds are open. */
assert(FLAGS_SET(flags, FORK_CLOSE_ALL_FDS));
r = pack_fds(except_fds, n_except_fds);
if (r < 0) {
log_full_errno(prio, r, "Failed to pack file descriptors: %m");
_exit(EXIT_FAILURE);
}
}
if (flags & FORK_CLOEXEC_OFF) {
r = fd_cloexec_many(except_fds, n_except_fds, false);
if (r < 0) {
@@ -1736,7 +1749,7 @@ int safe_fork_full(
int pidref_safe_fork_full(
const char *name,
const int stdio_fds[3],
const int except_fds[],
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
PidRef *ret_pid) {
@@ -1760,7 +1773,7 @@ int pidref_safe_fork_full(
int namespace_fork(
const char *outer_name,
const char *inner_name,
const int except_fds[],
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
int pidns_fd,
+15 -3
View File
@@ -185,12 +185,13 @@ typedef enum ForkFlags {
FORK_KEEP_NOTIFY_SOCKET = 1 << 17, /* Unless this specified, $NOTIFY_SOCKET will be unset. */
FORK_DETACH = 1 << 18, /* Double fork if needed to ensure PID1/subreaper is parent */
FORK_NEW_NETNS = 1 << 19, /* Run child in its own network namespace 💣 DO NOT USE IN THREADED PROGRAMS! 💣 */
FORK_PACK_FDS = 1 << 20, /* Rearrange the passed FDs to be FD 3,4,5,etc. Updates the array in place (combine with FORK_CLOSE_ALL_FDS!) */
} ForkFlags;
int safe_fork_full(
const char *name,
const int stdio_fds[3],
const int except_fds[],
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
pid_t *ret_pid);
@@ -202,7 +203,7 @@ static inline int safe_fork(const char *name, ForkFlags flags, pid_t *ret_pid) {
int pidref_safe_fork_full(
const char *name,
const int stdio_fds[3],
const int except_fds[],
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
PidRef *ret_pid);
@@ -211,7 +212,18 @@ static inline int pidref_safe_fork(const char *name, ForkFlags flags, PidRef *re
return pidref_safe_fork_full(name, NULL, NULL, 0, flags, ret_pid);
}
int namespace_fork(const char *outer_name, const char *inner_name, const int except_fds[], size_t n_except_fds, ForkFlags flags, int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd, pid_t *ret_pid);
int namespace_fork(
const char *outer_name,
const char *inner_name,
int except_fds[],
size_t n_except_fds,
ForkFlags flags,
int pidns_fd,
int mntns_fd,
int netns_fd,
int userns_fd,
int root_fd,
pid_t *ret_pid);
int set_oom_score_adjust(int value);
int get_oom_score_adjust(int *ret);
+1 -41
View File
@@ -66,46 +66,6 @@
#define SNDBUF_SIZE (8*1024*1024)
static int shift_fds(int fds[], size_t n_fds) {
if (n_fds <= 0)
return 0;
/* Modifies the fds array! (sorts it) */
assert(fds);
for (int start = 0;;) {
int restart_from = -1;
for (int i = start; i < (int) n_fds; i++) {
int nfd;
/* Already at right index? */
if (fds[i] == i+3)
continue;
nfd = fcntl(fds[i], F_DUPFD, i + 3);
if (nfd < 0)
return -errno;
safe_close(fds[i]);
fds[i] = nfd;
/* Hmm, the fd we wanted isn't free? Then
* let's remember that and try again from here */
if (nfd != i+3 && restart_from < 0)
restart_from = i;
}
if (restart_from < 0)
break;
start = restart_from;
}
return 0;
}
static int flag_fds(
const int fds[],
size_t n_socket_fds,
@@ -4900,7 +4860,7 @@ int exec_invoke(
r = close_all_fds(keep_fds, n_keep_fds);
if (r >= 0)
r = shift_fds(params->fds, n_fds);
r = pack_fds(params->fds, n_fds);
if (r >= 0)
r = flag_fds(params->fds, n_socket_fds, n_fds, context->non_blocking);
if (r < 0) {
+7
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "dns-domain.h"
#include "fd-util.h"
#include "home-util.h"
#include "libcrypt-util.h"
#include "memory-util.h"
@@ -9,6 +10,8 @@
#include "strv.h"
#include "user-util.h"
DEFINE_HASH_OPS_FULL(blob_fd_hash_ops, char, path_hash_func, path_compare, free, void, close_fd_ptr);
bool suitable_user_name(const char *name) {
/* Checks whether the specified name is suitable for management via homed. Note that client-side
@@ -137,3 +140,7 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) {
const char *home_record_dir(void) {
return secure_getenv("SYSTEMD_HOME_RECORD_DIR") ?: "/var/lib/systemd/home/";
}
const char *home_system_blob_dir(void) {
return secure_getenv("SYSTEMD_HOME_SYSTEM_BLOB_DIR") ?: "/var/cache/systemd/home/";
}
+4
View File
@@ -5,6 +5,7 @@
#include "sd-bus.h"
#include "hash-funcs.h"
#include "time-util.h"
#include "user-record.h"
@@ -20,6 +21,8 @@
/* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */
assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U);
extern const struct hash_ops blob_fd_hash_ops;
bool suitable_user_name(const char *name);
int suitable_realm(const char *realm);
int suitable_image_path(const char *path);
@@ -35,3 +38,4 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret);
#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
const char *home_record_dir(void);
const char *home_system_blob_dir(void);
+250 -6
View File
@@ -12,7 +12,9 @@
#include "cap-list.h"
#include "capability-util.h"
#include "cgroup-util.h"
#include "copy.h"
#include "creds-util.h"
#include "dirent-util.h"
#include "dns-domain.h"
#include "env-util.h"
#include "fd-util.h"
@@ -20,6 +22,7 @@
#include "format-table.h"
#include "fs-util.h"
#include "glyph-util.h"
#include "hashmap.h"
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
@@ -40,8 +43,10 @@
#include "process-util.h"
#include "recurse-dir.h"
#include "rlimit-util.h"
#include "rm-rf.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
#include "tmpfile-util.h"
#include "uid-classification.h"
#include "user-record.h"
#include "user-record-password-quality.h"
@@ -85,6 +90,9 @@ static enum {
static uint64_t arg_capability_bounding_set = UINT64_MAX;
static uint64_t arg_capability_ambient_set = UINT64_MAX;
static bool arg_prompt_new_user = false;
static char *arg_blob_dir = NULL;
static bool arg_blob_clear = false;
static Hashmap *arg_blob_files = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -94,6 +102,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
static const BusLocator *bus_mgr;
@@ -107,7 +117,10 @@ static bool identity_properties_specified(void) {
!strv_isempty(arg_identity_filter) ||
!strv_isempty(arg_identity_filter_rlimits) ||
!strv_isempty(arg_pkcs11_token_uri) ||
!strv_isempty(arg_fido2_device);
!strv_isempty(arg_fido2_device) ||
arg_blob_dir ||
arg_blob_clear ||
!hashmap_isempty(arg_blob_files);
}
static int acquire_bus(sd_bus **bus) {
@@ -1266,9 +1279,132 @@ static int acquire_new_password(
}
}
static int acquire_merged_blob_dir(UserRecord *hr, bool existing, Hashmap **ret) {
_cleanup_free_ char *sys_blob_path = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
_cleanup_closedir_ DIR *d = NULL;
const char *src_blob_path, *filename;
void *fd_ptr;
int r;
assert(ret);
HASHMAP_FOREACH_KEY(fd_ptr, filename, arg_blob_files) {
_cleanup_free_ char *filename_dup = NULL;
_cleanup_close_ int fd_dup = -EBADF;
filename_dup = strdup(filename);
if (!filename_dup)
return log_oom();
if (PTR_TO_FD(fd_ptr) != -EBADF) {
fd_dup = fcntl(PTR_TO_FD(fd_ptr), F_DUPFD_CLOEXEC, 3);
if (fd_dup < 0)
return log_error_errno(errno, "Failed to duplicate fd of %s: %m", filename);
}
r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, filename_dup, FD_TO_PTR(fd_dup));
if (r < 0)
return r;
TAKE_PTR(filename_dup); /* Ownership transferred to hashmap */
TAKE_FD(fd_dup);
}
if (arg_blob_dir)
src_blob_path = arg_blob_dir;
else if (existing && !arg_blob_clear) {
if (hr->blob_directory)
src_blob_path = hr->blob_directory;
else {
/* This isn't technically a correct thing to do for generic user records,
* so anyone looking at this code for reference shouldn't replicate it.
* However, since homectl is tied to homed, this is OK. This adds robustness
* for situations where the user record is coming directly from the CLI and
* thus doesn't have a blobDirectory set */
sys_blob_path = path_join(home_system_blob_dir(), hr->user_name);
if (!sys_blob_path)
return log_oom();
src_blob_path = sys_blob_path;
}
} else
goto nodir; /* Shortcut: no dir to merge with, so just return copy of arg_blob_files */
d = opendir(src_blob_path);
if (!d)
return log_error_errno(errno, "Failed to open %s: %m", src_blob_path);
FOREACH_DIRENT_ALL(de, d, return log_error_errno(errno, "Failed to read %s: %m", src_blob_path)) {
_cleanup_free_ char *name = NULL;
_cleanup_close_ int fd = -EBADF;
if (dot_or_dot_dot(de->d_name))
continue;
if (hashmap_contains(blobs, de->d_name))
continue; /* arg_blob_files should override the base dir */
if (!suitable_blob_filename(de->d_name)) {
log_warning("File %s in blob directory %s has an invalid filename. Skipping.", de->d_name, src_blob_path);
continue;
}
name = strdup(de->d_name);
if (!name)
return log_oom();
fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NOCTTY);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s in %s: %m", de->d_name, src_blob_path);
r = fd_verify_regular(fd);
if (r < 0) {
log_warning_errno(r, "Entry %s in blob directory %s is not a regular file. Skipping.", de->d_name, src_blob_path);
continue;
}
r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, name, FD_TO_PTR(fd));
if (r < 0)
return r;
TAKE_PTR(name); /* Ownership transferred to hashmap */
TAKE_FD(fd);
}
nodir:
*ret = TAKE_PTR(blobs);
return 0;
}
static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) {
const char *filename;
void *fd_ptr;
int r;
assert(m);
r = sd_bus_message_open_container(m, 'a', "{sh}");
if (r < 0)
return r;
HASHMAP_FOREACH_KEY(fd_ptr, filename, blobs) {
int fd = PTR_TO_FD(fd_ptr);
if (fd == -EBADF) /* File marked for deletion */
continue;
r = sd_bus_message_append(m, "{sh}", filename, fd);
if (r < 0)
return r;
}
return sd_bus_message_close_container(m);
}
static int create_home_common(JsonVariant *input) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
int r;
r = acquire_bus(&bus);
@@ -1281,6 +1417,10 @@ static int create_home_common(JsonVariant *input) {
if (r < 0)
return r;
r = acquire_merged_blob_dir(hr, false, &blobs);
if (r < 0)
return r;
/* If the JSON record carries no plain text password (besides the recovery key), then let's query it
* manually. */
if (strv_length(hr->password) <= arg_recovery_key) {
@@ -1327,7 +1467,7 @@ static int create_home_common(JsonVariant *input) {
if (r < 0)
return log_error_errno(r, "Failed to format user record: %m");
r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1337,6 +1477,14 @@ static int create_home_common(JsonVariant *input) {
if (r < 0)
return bus_log_create_error(r);
r = bus_message_append_blobs(m, blobs);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "t", 0);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0) {
if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
@@ -1492,7 +1640,7 @@ static int acquire_updated_home_record(
reply = sd_bus_message_unref(reply);
r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
if (r < 0)
return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
}
@@ -1562,6 +1710,7 @@ static int update_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL;
_cleanup_free_ char *buffer = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
const char *username;
int r;
@@ -1595,6 +1744,10 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
r = acquire_merged_blob_dir(hr, true, &blobs);
if (r < 0)
return r;
/* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
* authentication might be confusing. */
@@ -1606,7 +1759,7 @@ static int update_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
_cleanup_free_ char *formatted = NULL;
r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1620,6 +1773,14 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_create_error(r);
r = bus_message_append_blobs(m, blobs);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "t", 0);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0) {
if (arg_and_change_password &&
@@ -2451,7 +2612,13 @@ static int help(int argc, char *argv[], void *userdata) {
" Whether to require user verification to unlock\n"
" the account\n"
" --recovery-key=BOOL Add a recovery key\n"
"\n%4$sAccount Management User Record Properties:%5$s\n"
"\n%4$sBlob Directory User Record Properties:%5$s\n"
" -b --blob=[FILENAME=]PATH\n"
" Path to a replacement blob directory, or replace\n"
" an individual files in the blob directory.\n"
" --avatar=PATH Path to user avatar picture\n"
" --login-background=PATH Path to user login background picture\n"
"\n%4$sAccount Management User Record Properties:%5$s\n"
" --locked=BOOL Set locked account state\n"
" --not-before=TIMESTAMP Do not allow logins before\n"
" --not-after=TIMESTAMP Do not allow logins after\n"
@@ -2626,6 +2793,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_CAPABILITY_BOUNDING_SET,
ARG_CAPABILITY_AMBIENT_SET,
ARG_PROMPT_NEW_USER,
ARG_AVATAR,
ARG_LOGIN_BACKGROUND,
};
static const struct option options[] = {
@@ -2719,6 +2888,9 @@ static int parse_argv(int argc, char *argv[]) {
{ "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET },
{ "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET },
{ "prompt-new-user", no_argument, NULL, ARG_PROMPT_NEW_USER },
{ "blob", required_argument, NULL, 'b' },
{ "avatar", required_argument, NULL, ARG_AVATAR },
{ "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND },
{}
};
@@ -2730,7 +2902,7 @@ static int parse_argv(int argc, char *argv[]) {
for (;;) {
int c;
c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL);
if (c < 0)
break;
@@ -4027,6 +4199,78 @@ static int parse_argv(int argc, char *argv[]) {
arg_prompt_new_user = true;
break;
case 'b':
case ARG_AVATAR:
case ARG_LOGIN_BACKGROUND: {
_cleanup_close_ int fd = -EBADF;
_cleanup_free_ char *path = NULL, *filename = NULL;
if (c == 'b') {
char *eq;
if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
hashmap_clear(arg_blob_files);
arg_blob_dir = mfree(arg_blob_dir);
arg_blob_clear = true;
break;
}
eq = strrchr(optarg, '=');
if (!eq) { /* --blob=/some/path replaces the blob dir */
r = parse_path_argument(optarg, false, &arg_blob_dir);
if (r < 0)
return log_error_errno(r, "Failed to parse path %s: %m", optarg);
break;
}
/* --blob=filename=/some/path replaces the file "filename" with /some/path */
filename = strndup(optarg, eq - optarg);
if (!filename)
return log_oom();
if (isempty(filename))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
if (!suitable_blob_filename(filename))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
r = parse_path_argument(eq + 1, false, &path);
if (r < 0)
return log_error_errno(r, "Failed to parse path %s: %m", eq + 1);
} else {
const char *well_known_filename =
c == ARG_AVATAR ? "avatar" :
c == ARG_LOGIN_BACKGROUND ? "login-background" :
NULL;
assert(well_known_filename);
filename = strdup(well_known_filename);
if (!filename)
return log_oom();
r = parse_path_argument(optarg, false, &path);
if (r < 0)
return log_error_errno(r, "Failed to parse path %s: %m", optarg);
}
if (path) {
fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s: %m", path);
if (fd_verify_regular(fd) < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
} else
fd = -EBADF; /* Delete the file */
r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
if (r < 0)
return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
TAKE_PTR(filename); /* hashmap takes ownership */
TAKE_FD(fd);
break;
}
case '?':
return -EINVAL;
+73
View File
@@ -1,6 +1,10 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "fd-util.h"
#include "home-util.h"
#include "homed-bus.h"
#include "missing_fcntl.h"
#include "stat-util.h"
#include "strv.h"
int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) {
@@ -64,3 +68,72 @@ int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, U
*ret = TAKE_PTR(hr);
return 0;
}
int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error) {
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
int r;
assert(m);
assert(ret);
/* We want to differentiate between blobs being NULL (not passed at all)
* and empty (passed from dbus, but it was empty) */
r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops);
if (r < 0)
return r;
r = sd_bus_message_enter_container(m, 'a', "{sh}");
if (r < 0)
return r;
for (;;) {
_cleanup_free_ char *filename = NULL;
_cleanup_close_ int fd = -EBADF;
const char *_filename = NULL;
int _fd, flags;
r = sd_bus_message_read(m, "{sh}", &_filename, &_fd);
if (r < 0)
return r;
if (r == 0)
break;
filename = strdup(_filename);
if (!filename)
return -ENOMEM;
fd = fcntl(_fd, F_DUPFD_CLOEXEC, 3);
if (fd < 0)
return -errno;
r = suitable_blob_filename(filename);
if (r < 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid blob directory filename: %s", filename);
r = fd_verify_regular(fd);
if (r < 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for %s is not a regular file", filename);
flags = fcntl(fd, F_GETFL);
if (flags < 0)
return -errno;
/* Refuse fds w/ unexpected flags set. In particular, we don't want to permit O_PATH FDs, since
* those don't actually guarentee that the client has access to the file. */
if ((flags & ~(O_ACCMODE|RAW_O_LARGEFILE)) != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for %s has unexpected flags set", filename);
r = hashmap_put(blobs, filename, FD_TO_PTR(fd));
if (r < 0)
return r;
TAKE_PTR(filename); /* Ownership transferred to hashmap */
TAKE_FD(fd);
}
r = sd_bus_message_exit_container(m);
if (r < 0)
return r;
*ret = TAKE_PTR(blobs);
return 0;
}
+3 -1
View File
@@ -3,8 +3,10 @@
#include "sd-bus.h"
#include "user-record.h"
#include "hashmap.h"
#include "json.h"
#include "user-record.h"
int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error);
int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error);
int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error);
+30 -4
View File
@@ -295,7 +295,7 @@ int bus_home_method_realize(
if (r == 0)
return 1; /* Will call us back */
r = home_create(h, secret, error);
r = home_create(h, secret, NULL, 0, error);
if (r < 0)
return r;
@@ -416,7 +416,13 @@ int bus_home_method_authenticate(
return 1;
}
int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) {
int bus_home_method_update_record(
Home *h,
sd_bus_message *message,
UserRecord *hr,
Hashmap *blobs,
uint64_t flags,
sd_bus_error *error) {
int r;
assert(h);
@@ -427,6 +433,9 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *
if (r < 0)
return r;
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Provided flags are unsupported.");
r = home_verify_polkit_async(
h,
message,
@@ -438,7 +447,7 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *
if (r == 0)
return 1; /* Will call us back */
r = home_update(h, hr, error);
r = home_update(h, hr, blobs, flags, error);
if (r < 0)
return r;
@@ -458,6 +467,8 @@ int bus_home_method_update(
sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
uint64_t flags = 0;
Home *h = ASSERT_PTR(userdata);
int r;
@@ -467,7 +478,17 @@ int bus_home_method_update(
if (r < 0)
return r;
return bus_home_method_update_record(h, message, hr, error);
if (endswith(sd_bus_message_get_member(message), "Ex")) {
r = bus_message_read_blobs(message, &blobs, error);
if (r < 0)
return r;
r = sd_bus_message_read(message, "t", &flags);
if (r < 0)
return r;
}
return bus_home_method_update_record(h, message, hr, blobs, flags, error);
}
int bus_home_method_resize(
@@ -892,6 +913,11 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_NO_RESULT,
bus_home_method_update,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("UpdateEx",
SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
SD_BUS_NO_RESULT,
bus_home_method_update,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("Resize",
SD_BUS_ARGS("t", size, "s", secret),
SD_BUS_NO_RESULT,
+1 -1
View File
@@ -17,7 +17,7 @@ int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error
int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error);
int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error);
+98 -21
View File
@@ -33,6 +33,7 @@
#include "process-util.h"
#include "quota-util.h"
#include "resize-fs.h"
#include "rm-rf.h"
#include "set.h"
#include "signal-util.h"
#include "stat-util.h"
@@ -54,7 +55,13 @@
assert_cc(HOME_UID_MIN <= HOME_UID_MAX);
assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1));
static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret);
static int home_start_work(
Home *h,
const char *verb,
UserRecord *hr,
UserRecord *secret,
Hashmap *blobs,
uint64_t flags);
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref);
@@ -96,7 +103,7 @@ static int suitable_home_record(UserRecord *hr) {
int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
_cleanup_(home_freep) Home *home = NULL;
_cleanup_free_ char *nm = NULL, *ns = NULL;
_cleanup_free_ char *nm = NULL, *ns = NULL, *blob = NULL;
int r;
assert(m);
@@ -162,6 +169,13 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
if (r < 0)
return r;
blob = path_join(home_system_blob_dir(), hr->user_name);
if (!blob)
return -ENOMEM;
r = mkdir_safe(blob, 0755, 0, 0, MKDIR_IGNORE_EXISTING);
if (r < 0)
log_warning_errno(r, "Failed to create blob dir for user '%s': %m", home->user_name);
(void) bus_manager_emit_auto_login_changed(m);
(void) bus_home_emit_change(home);
(void) manager_schedule_rebalance(m, /* immediately= */ false);
@@ -323,7 +337,9 @@ int home_save_record(Home *h) {
}
int home_unlink_record(Home *h) {
_cleanup_free_ char *blob = NULL;
const char *fn;
int r;
assert(h);
@@ -335,6 +351,13 @@ int home_unlink_record(Home *h) {
if (unlink(fn) < 0 && errno != ENOENT)
return -errno;
blob = path_join(home_system_blob_dir(), h->user_name);
if (!blob)
return -ENOMEM;
r = rm_rf(blob, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
if (r < 0)
return r;
return 0;
}
@@ -721,7 +744,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) {
r = home_start_work(h, "activate", h->record, secret);
r = home_start_work(h, "activate", h->record, secret, NULL, 0);
if (r < 0) {
h->current_operation = operation_result_unref(h->current_operation, r, NULL);
home_set_state(h, _HOME_STATE_INVALID);
@@ -1161,10 +1184,17 @@ static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void
return 0;
}
static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
static int home_start_work(
Home *h,
const char *verb,
UserRecord *hr,
UserRecord *secret,
Hashmap *blobs,
uint64_t flags) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *fdmap = NULL;
_cleanup_(erase_and_freep) char *formatted = NULL;
_cleanup_close_ int stdin_fd = -EBADF, stdout_fd = -EBADF;
_cleanup_free_ int *blob_fds = NULL;
pid_t pid = 0;
int r;
@@ -1192,6 +1222,37 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
return r;
}
if (blobs) {
const char *blob_filename = NULL;
void *fd_ptr;
size_t i = 0;
blob_fds = new(int, hashmap_size(blobs));
if (!blob_fds)
return -ENOMEM;
/* homework needs to be able to tell the difference between blobs being null
* (the fdmap field is completely missing) and it being empty (the field is an
* empty object) */
r = json_variant_new_object(&fdmap, NULL, 0);
if (r < 0)
return r;
HASHMAP_FOREACH_KEY(fd_ptr, blob_filename, blobs) {
blob_fds[i] = PTR_TO_FD(fd_ptr);
r = json_variant_set_field_integer(&fdmap, blob_filename, i);
if (r < 0)
return r;
i++;
}
r = json_variant_set_field(&v, HOMEWORK_BLOB_FDMAP_FIELD, fdmap);
if (r < 0)
return r;
}
r = json_variant_format(v, 0, &formatted);
if (r < 0)
return r;
@@ -1208,8 +1269,9 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
r = safe_fork_full("(sd-homework)",
(int[]) { stdin_fd, stdout_fd, STDERR_FILENO },
NULL, 0,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
blob_fds, hashmap_size(blobs),
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_CLOEXEC_OFF|FORK_PACK_FDS|FORK_DEATHSIG_SIGTERM|
FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
if (r < 0)
return r;
if (r == 0) {
@@ -1254,6 +1316,10 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
if (r < 0)
log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m");
r = setenv_systemd_log_level();
if (r < 0)
log_warning_errno(r, "Failed to update $SYSTEMD_LOG_LEVEL, ignoring: %m");
/* Allow overriding the homework path via an environment variable, to make debugging
* easier. */
homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH;
@@ -1326,7 +1392,7 @@ static int home_fixate_internal(
assert(secret);
assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
r = home_start_work(h, "inspect", h->record, secret);
r = home_start_work(h, "inspect", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1375,7 +1441,7 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta
assert(secret);
assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
r = home_start_work(h, "activate", h->record, secret);
r = home_start_work(h, "activate", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1427,7 +1493,7 @@ static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for
assert(secret);
assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
r = home_start_work(h, "inspect", h->record, secret);
r = home_start_work(h, "inspect", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1472,7 +1538,7 @@ static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) {
home_unpin(h); /* unpin so that we can deactivate */
r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL);
r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL, NULL, 0);
if (r < 0)
/* Operation failed before it even started, reacquire pin fd, if state still dictates so */
home_update_pin_fd(h, _HOME_STATE_INVALID);
@@ -1509,7 +1575,7 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) {
return home_deactivate_internal(h, force, error);
}
int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error) {
int r;
assert(h);
@@ -1552,7 +1618,7 @@ int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
return r;
}
r = home_start_work(h, "create", h->record, secret);
r = home_start_work(h, "create", h->record, secret, blobs, flags);
if (r < 0)
return r;
@@ -1582,7 +1648,7 @@ int home_remove(Home *h, sd_bus_error *error) {
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
}
r = home_start_work(h, "remove", h->record, NULL);
r = home_start_work(h, "remove", h->record, NULL, NULL, 0);
if (r < 0)
return r;
@@ -1626,6 +1692,8 @@ static int home_update_internal(
const char *verb,
UserRecord *hr,
UserRecord *secret,
Hashmap *blobs,
uint64_t flags,
sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL;
@@ -1649,6 +1717,15 @@ static int home_update_internal(
secret = saved_secret;
}
if (blobs) {
const char *failed = NULL;
r = user_record_ensure_blob_manifest(hr, blobs, &failed);
if (r == -EINVAL)
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
}
r = manager_verify_user_record(h->manager, hr);
switch (r) {
@@ -1691,14 +1768,14 @@ static int home_update_internal(
return sd_bus_error_set(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing.");
}
r = home_start_work(h, verb, new_hr, secret);
r = home_start_work(h, verb, new_hr, secret, blobs, flags);
if (r < 0)
return r;
return 0;
}
int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
int home_update(Home *h, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error) {
HomeState state;
int r;
@@ -1726,7 +1803,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
if (r < 0)
return r;
r = home_update_internal(h, "update", hr, NULL, error);
r = home_update_internal(h, "update", hr, NULL, blobs, flags, error);
if (r < 0)
return r;
@@ -1813,7 +1890,7 @@ int home_resize(Home *h,
c = TAKE_PTR(signed_c);
}
r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, error);
r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, NULL, 0, error);
if (r < 0)
return r;
@@ -1929,7 +2006,7 @@ int home_passwd(Home *h,
return r;
}
r = home_update_internal(h, "passwd", signed_c, merged_secret, error);
r = home_update_internal(h, "passwd", signed_c, merged_secret, NULL, 0, error);
if (r < 0)
return r;
@@ -1986,7 +2063,7 @@ int home_lock(Home *h, sd_bus_error *error) {
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
}
r = home_start_work(h, "lock", h->record, NULL);
r = home_start_work(h, "lock", h->record, NULL, NULL, 0);
if (r < 0)
return r;
@@ -2001,7 +2078,7 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state
assert(secret);
assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
r = home_start_work(h, "unlock", h->record, secret);
r = home_start_work(h, "unlock", h->record, secret, NULL, 0);
if (r < 0)
return r;
+3 -2
View File
@@ -3,6 +3,7 @@
typedef struct Home Home;
#include "hashmap.h"
#include "homed-manager.h"
#include "homed-operation.h"
#include "list.h"
@@ -193,9 +194,9 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error);
int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_deactivate(Home *h, bool force, sd_bus_error *error);
int home_create(Home *h, UserRecord *secret, sd_bus_error *error);
int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
int home_remove(Home *h, sd_bus_error *error);
int home_update(Home *h, UserRecord *new_record, sd_bus_error *error);
int home_update(Home *h, UserRecord *new_record, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, bool automatic, sd_bus_error *error);
int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error);
int home_unregister(Home *h, sd_bus_error *error);
+51 -9
View File
@@ -340,7 +340,7 @@ static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bu
return generic_home_method(userdata, message, bus_home_method_deactivate, error);
}
static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) {
static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs, Home **ret, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL;
bool signed_locally;
Home *other;
@@ -370,6 +370,15 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd
if (r != -ESRCH)
return r;
if (blobs) {
const char *failed = NULL;
r = user_record_ensure_blob_manifest(hr, blobs, &failed);
if (r == -EINVAL)
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
}
r = manager_verify_user_record(m, hr);
switch (r) {
@@ -458,7 +467,7 @@ static int method_register_home(
if (r == 0)
return 1; /* Will call us back */
r = validate_and_allocate_home(m, hr, &h, error);
r = validate_and_allocate_home(m, hr, NULL, &h, error);
if (r < 0)
return r;
@@ -475,12 +484,11 @@ static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bu
return generic_home_method(userdata, message, bus_home_method_unregister, error);
}
static int method_create_home(
sd_bus_message *message,
void *userdata,
sd_bus_error *error) {
static int method_create_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
uint64_t flags = 0;
Manager *m = ASSERT_PTR(userdata);
Home *h;
int r;
@@ -491,6 +499,18 @@ static int method_create_home(
if (r < 0)
return r;
if (endswith(sd_bus_message_get_member(message), "Ex")) {
r = bus_message_read_blobs(message, &blobs, error);
if (r < 0)
return r;
r = sd_bus_message_read(message, "t", &flags);
if (r < 0)
return r;
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Provided flags are unsupported.");
}
r = bus_verify_polkit_async(
message,
"org.freedesktop.home1.create-home",
@@ -502,11 +522,11 @@ static int method_create_home(
if (r == 0)
return 1; /* Will call us back */
r = validate_and_allocate_home(m, hr, &h, error);
r = validate_and_allocate_home(m, hr, blobs, &h, error);
if (r < 0)
return r;
r = home_create(h, hr, error);
r = home_create(h, hr, blobs, flags, error);
if (r < 0)
goto fail;
@@ -544,6 +564,8 @@ static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_
static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
uint64_t flags = 0;
Manager *m = ASSERT_PTR(userdata);
Home *h;
int r;
@@ -554,13 +576,23 @@ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_er
if (r < 0)
return r;
if (endswith(sd_bus_message_get_member(message), "Ex")) {
r = bus_message_read_blobs(message, &blobs, error);
if (r < 0)
return r;
r = sd_bus_message_read(message, "t", &flags);
if (r < 0)
return r;
}
assert(hr->user_name);
h = hashmap_get(m->homes_by_name, hr->user_name);
if (!h)
return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name);
return bus_home_method_update_record(h, message, hr, error);
return bus_home_method_update_record(h, message, hr, blobs, flags, error);
}
static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
@@ -769,6 +801,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_NO_RESULT,
method_create_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("CreateHomeEx",
SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
SD_BUS_NO_RESULT,
method_create_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
/* Create $HOME for already registered JSON entry */
SD_BUS_METHOD_WITH_ARGS("RealizeHome",
@@ -804,6 +841,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_NO_RESULT,
method_update_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("UpdateHomeEx",
SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
SD_BUS_NO_RESULT,
method_update_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("ResizeHome",
SD_BUS_ARGS("s", user_name, "t", size, "s", secret),
+28
View File
@@ -42,6 +42,7 @@
#include "quota-util.h"
#include "random-util.h"
#include "resize-fs.h"
#include "rm-rf.h"
#include "socket-util.h"
#include "sort-util.h"
#include "stat-util.h"
@@ -79,6 +80,7 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, pat
static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata);
static int manager_gc_images(Manager *m);
static int manager_gc_blob(Manager *m);
static int manager_enumerate_images(Manager *m);
static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name);
static void manager_revalidate_image(Manager *m, Home *h);
@@ -1627,6 +1629,9 @@ int manager_startup(Manager *m) {
/* Let's clean up home directories whose devices got removed while we were not running */
(void) manager_enqueue_gc(m, NULL);
/* Let's clean up blob directories for home dirs that no longer exist */
(void) manager_gc_blob(m);
return 0;
}
@@ -1715,6 +1720,29 @@ int manager_gc_images(Manager *m) {
return 0;
}
static int manager_gc_blob(Manager *m) {
_cleanup_closedir_ DIR *d = NULL;
int r;
assert(m);
d = opendir(home_system_blob_dir());
if (!d) {
if (errno == ENOENT)
return 0;
return log_error_errno(errno, "Failed to open %s: %m", home_system_blob_dir());
}
FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read system blob directory: %m"))
if (!hashmap_contains(m->homes_by_name, de->d_name)) {
r = rm_rf_at(dirfd(d), de->d_name, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
if (r < 0)
log_warning_errno(r, "Failed to delete blob dir for missing user '%s', ignoring: %m", de->d_name);
}
return 0;
}
static int on_deferred_rescan(sd_event_source *s, void *userdata) {
Manager *m = ASSERT_PTR(userdata);
+301
View File
@@ -0,0 +1,301 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "copy.h"
#include "fileio.h"
#include "fd-util.h"
#include "format-util.h"
#include "fs-util.h"
#include "home-util.h"
#include "homework-blob.h"
#include "homework.h"
#include "install-file.h"
#include "macro.h"
#include "path-util.h"
#include "recurse-dir.h"
#include "rm-rf.h"
#include "sha256.h"
#include "string-util.h"
#include "tmpfile-util.h"
#include "umask-util.h"
#include "utf8.h"
static int copy_one_blob(
int src_fd,
int dest_dfd,
const char *name,
uint64_t *total_size,
uid_t uid,
Hashmap *manifest) {
_cleanup_(unlink_and_freep) char *dest_tmpname = NULL;
_cleanup_close_ int dest = -EBADF;
uint8_t hash[SHA256_DIGEST_SIZE], *known_hash;
off_t initial, size;
int r;
assert(src_fd >= 0);
assert(dest_dfd >= 0);
assert(name);
assert(total_size);
assert(uid_is_valid(uid));
assert(manifest);
if (!suitable_blob_filename(name)) {
log_warning("Blob %s has invalid filename. Skipping.", name);
return 0;
}
known_hash = hashmap_get(manifest, name);
if (!known_hash) {
log_warning("Blob %s is missing from manifest. Skipping.", name);
return 0;
}
r = fd_verify_regular(src_fd);
if (r < 0) {
log_warning_errno(r, "Blob %s is not a regular file. Skipping.", name);
return 0;
}
initial = lseek(src_fd, 0, SEEK_CUR);
if (initial < 0)
return log_debug_errno(errno, "Failed to get initial pos on fd for blob %s: %m", name);
if (initial > 0)
log_debug("Blob %s started offset %s into file", name, FORMAT_BYTES(initial));
/* Hashing is relatively cheaper compared to copying, especially since we're possibly copying across
* filesystems or even devices here. So first we check the hash and bail early if the file's contents
* don't match what's in the manifest. */
r = sha256_fd(src_fd, BLOB_DIR_MAX_SIZE, hash);
if (r == -EFBIG)
return log_warning_errno(r, "Blob %s is larger than blob directory size limit. Not copying any further.", name);
if (r < 0)
return log_debug_errno(r, "Failed to compute sha256 for blob %s: %m", name);
if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) {
log_warning("Blob %s has incorrect hash. Skipping.", name);
return 0;
}
size = lseek(src_fd, 0, SEEK_CUR);
if (size < 0)
return log_debug_errno(errno, "Failed to get final pos on fd for blob %s: %m", name);
if (!DEC_SAFE(&size, initial))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid seek position on fd for %s. Couldn't get size.", name);
if (!INC_SAFE(total_size, size))
*total_size = UINT64_MAX;
log_debug("Blob %s size is %s, making the new total dir size %s", name, FORMAT_BYTES(size),
*total_size != UINT64_MAX ? FORMAT_BYTES(*total_size) : "overflow!");
if (*total_size > BLOB_DIR_MAX_SIZE)
return log_warning_errno(SYNTHETIC_ERRNO(EFBIG),
"Blob %s will cause blob directory to exceed its size limit. Not copying any further.", name);
/* Next we copy but don't yet link the file into the blob directory */
if (lseek(src_fd, initial, SEEK_SET) < 0)
return log_debug_errno(errno, "Failed to rewind fd for blob %s: %m", name);
dest = open_tmpfile_linkable_at(dest_dfd, name, O_RDWR|O_CLOEXEC, &dest_tmpname);
if (dest < 0)
return log_debug_errno(dest, "Failed to create dest tmpfile for blob %s: %m", name);
if (fchmod(dest, 0644) < 0)
return log_debug_errno(errno, "Failed to chmod blob %s: %m", name);
if (fchown(dest, uid, uid) < 0)
return log_debug_errno(errno, "Failed to chown blob %s: %m", name);
r = copy_bytes(src_fd, dest, BLOB_DIR_MAX_SIZE, 0);
if (r < 0)
return log_debug_errno(r, "Failed to copy blob %s: %m", name);
/* The source FD might have changed while we were busy copying, thus invalidating the hash.
* So, we re-hash the data we just copied to make sure that this didn't happen. */
if (lseek(dest, 0, SEEK_SET) < 0)
return log_debug_errno(errno, "Failed to rewind blob %s for rehash: %m", name);
r = sha256_fd(dest, BLOB_DIR_MAX_SIZE, hash);
if (r < 0)
return log_debug_errno(r, "Failed to rehash blob %s: %m", name);
if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) {
log_warning("Blob %s has changed while we were copying it. Skipping.", name);
return 0;
}
/* The file's contents still match the blob manifest, so it's safe to expose it in the directory */
r = link_tmpfile_at(dest, dest_dfd, dest_tmpname, name, 0);
if (r < 0)
return log_debug_errno(r, "Failed to link blob %s: %m", name);
dest_tmpname = mfree(dest_tmpname);
return 0;
}
static int replace_blob_at(
int src_base_dfd,
const char *src_name,
int dest_base_dfd,
const char *dest_name,
Hashmap *manifest,
mode_t mode,
uid_t uid) {
_cleanup_free_ char *fn = NULL;
_cleanup_close_ int src_dfd = -EBADF, dest_dfd = -EBADF;
_cleanup_free_ DirectoryEntries *de = NULL;
uint64_t total_size = 0;
int r;
assert(src_base_dfd >= 0);
assert(src_name);
assert(dest_base_dfd >= 0);
assert(dest_name);
assert(uid_is_valid(uid));
src_dfd = openat(src_base_dfd, src_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
if (src_dfd < 0) {
if (errno == ENOENT)
return 0;
return log_debug_errno(errno, "Failed to open src blob dir: %m");
}
r = tempfn_random(dest_name, NULL, &fn);
if (r < 0)
return r;
dest_dfd = open_mkdir_at(dest_base_dfd, fn, O_EXCL|O_CLOEXEC, mode);
if (dest_dfd < 0)
return log_debug_errno(dest_dfd, "Failed to create/open dest blob dir: %m");
r = readdir_all(src_dfd, RECURSE_DIR_SORT, &de);
if (r < 0) {
log_debug_errno(r, "Failed to read src blob dir: %m");
goto fail;
}
for (size_t i = 0; i < de->n_entries; i++) {
const char *name = de->entries[i]->d_name;
_cleanup_close_ int src_fd = -EBADF;
src_fd = openat(src_dfd, name, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW);
if (src_fd < 0) {
r = log_debug_errno(errno, "Failed to open %s in src blob dir: %m", name);
goto fail;
}
r = copy_one_blob(src_fd, dest_dfd, name, &total_size, uid, manifest);
if (r == -EFBIG)
break;
if (r < 0)
goto fail;
}
if (fchown(dest_dfd, uid, uid) < 0) {
r = log_debug_errno(errno, "Failed to chown dest blob dir: %m");
goto fail;
}
r = install_file(dest_base_dfd, fn, dest_base_dfd, dest_name, INSTALL_REPLACE);
if (r < 0) {
log_debug_errno(r, "Failed to move dest blob dir into place: %m");
goto fail;
}
return 0;
fail:
(void) rm_rf_at(dest_base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
return r;
}
int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled) {
_cleanup_close_ int sys_base_dfd = -EBADF;
int r;
assert(h);
assert(root_fd >= 0);
assert(reconciled >= 0);
if (reconciled == USER_RECONCILE_IDENTICAL)
return 0;
sys_base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
if (sys_base_dfd < 0)
return log_error_errno(errno, "Failed to open system blob dir: %m");
if (reconciled == USER_RECONCILE_HOST_WON) {
r = replace_blob_at(sys_base_dfd, h->user_name, root_fd, ".identity-blob",
h->blob_manifest, 0700, h->uid);
if (r < 0)
return log_error_errno(r, "Failed to replace embedded blobs with system blobs: %m");
log_info("Replaced embedded blob dir with contents of system blob dir.");
} else {
assert(reconciled == USER_RECONCILE_EMBEDDED_WON);
r = replace_blob_at(root_fd, ".identity-blob", sys_base_dfd, h->user_name,
h->blob_manifest, 0755, 0);
if (r < 0)
return log_error_errno(r, "Failed to replace system blobs with embedded blobs: %m");
log_info("Replaced system blob dir with contents of embedded blob dir.");
}
return 0;
}
int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs) {
_cleanup_free_ char *fn = NULL;
_cleanup_close_ int base_dfd = -EBADF, dfd = -EBADF;
uint64_t total_size = 0;
const char *filename;
const void *v;
int r;
assert(h);
if (!blobs) /* Shortcut: If no blobs are passed from dbus, we have nothing to do. */
return 0;
base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
if (base_dfd < 0)
return log_error_errno(errno, "Failed to open system blob base dir: %m");
if (hashmap_isempty(blobs)) {
/* Shortcut: If blobs was passed but empty, we can simply delete the contents
* of the directory. */
r = rm_rf_at(base_dfd, h->user_name, REMOVE_PHYSICAL|REMOVE_MISSING_OK);
if (r < 0)
return log_error_errno(errno, "Failed to empty out system blob dir: %m");
return 0;
}
r = tempfn_random(h->user_name, NULL, &fn);
if (r < 0)
return r;
dfd = open_mkdir_at(base_dfd, fn, O_EXCL|O_CLOEXEC, 0755);
if (dfd < 0)
return log_error_errno(errno, "Failed to create system blob dir: %m");
HASHMAP_FOREACH_KEY(v, filename, blobs) {
r = copy_one_blob(PTR_TO_FD(v), dfd, filename, &total_size, 0, h->blob_manifest);
if (r == -EFBIG)
break;
if (r < 0) {
log_error_errno(r, "Failed to copy %s into system blob dir: %m", filename);
goto fail;
}
}
r = install_file(base_dfd, fn, base_dfd, h->user_name, INSTALL_REPLACE);
if (r < 0) {
log_error_errno(r, "Failed to move system blob dir into place: %m");
goto fail;
}
log_info("Replaced system blob directory.");
return 0;
fail:
(void) rm_rf_at(base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
return r;
}
+9
View File
@@ -0,0 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "user-record.h"
int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled);
int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs);
+9 -4
View File
@@ -4,6 +4,7 @@
#include "btrfs-util.h"
#include "fd-util.h"
#include "homework-blob.h"
#include "homework-directory.h"
#include "homework-mount.h"
#include "homework-quota.h"
@@ -265,7 +266,7 @@ int home_resize_directory(
UserRecord **ret_home) {
_cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
int r;
int r, reconciled;
assert(h);
assert(setup);
@@ -276,9 +277,9 @@ int home_resize_directory(
if (r < 0)
return r;
r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
if (r < 0)
return r;
reconciled = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
if (reconciled < 0)
return reconciled;
r = home_maybe_shift_uid(h, flags, setup);
if (r < 0)
@@ -294,6 +295,10 @@ int home_resize_directory(
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
r = home_extend_embedded_identity(new_home, h, setup);
if (r < 0)
return r;
+13 -4
View File
@@ -33,6 +33,7 @@
#include "glyph-util.h"
#include "gpt.h"
#include "home-util.h"
#include "homework-blob.h"
#include "homework-luks.h"
#include "homework-mount.h"
#include "io-util.h"
@@ -3129,7 +3130,7 @@ int home_resize_luks(
struct fdisk_partition *partition = NULL;
_cleanup_close_ int opened_image_fd = -EBADF;
_cleanup_free_ char *whole_disk = NULL;
int r, resize_type, image_fd = -EBADF;
int r, resize_type, image_fd = -EBADF, reconciled = USER_RECONCILE_IDENTICAL;
sd_id128_t disk_uuid;
const char *ip, *ipo;
struct statfs sfs;
@@ -3235,9 +3236,9 @@ int home_resize_luks(
return r;
if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
if (r < 0)
return r;
reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
if (reconciled < 0)
return reconciled;
}
r = home_maybe_shift_uid(h, flags, setup);
@@ -3451,6 +3452,10 @@ int home_resize_luks(
r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
}
if (S_ISREG(st.st_mode)) {
@@ -3539,6 +3544,10 @@ int home_resize_luks(
r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
}
}
+84 -14
View File
@@ -11,6 +11,7 @@
#include "filesystems.h"
#include "fs-util.h"
#include "home-util.h"
#include "homework-blob.h"
#include "homework-cifs.h"
#include "homework-directory.h"
#include "homework-fido2.h"
@@ -24,6 +25,7 @@
#include "memory-util.h"
#include "missing_magic.h"
#include "mount-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "recovery-key.h"
#include "rm-rf.h"
@@ -695,7 +697,7 @@ int home_load_embedded_identity(
if (ret_new_home)
*ret_new_home = TAKE_PTR(new_home);
return 0;
return r; /* We pass along who won the reconciliation */
}
int home_store_embedded_identity(UserRecord *h, int root_fd, UserRecord *old_home) {
@@ -826,7 +828,7 @@ int home_refresh(
UserRecord **ret_new_home) {
_cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
int r;
int r, reconciled;
assert(h);
assert(setup);
@@ -835,9 +837,9 @@ int home_refresh(
/* When activating a home directory, does the identity work: loads the identity from the $HOME
* directory, reconciles it with our idea, chown()s everything. */
r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home);
if (r < 0)
return r;
reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home);
if (reconciled < 0)
return reconciled;
r = home_maybe_shift_uid(h, flags, setup);
if (r < 0)
@@ -851,6 +853,10 @@ int home_refresh(
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
r = chown_recursive_directory(setup->root_fd, h->uid);
if (r < 0)
return r;
@@ -1071,6 +1077,10 @@ int home_populate(UserRecord *h, int dir_fd) {
if (r < 0)
return r;
r = home_reconcile_blob_dirs(h, dir_fd, USER_RECONCILE_HOST_WON);
if (r < 0)
return r;
r = chown_recursive_directory(dir_fd, h->uid);
if (r < 0)
return r;
@@ -1305,7 +1315,7 @@ static int determine_default_storage(UserStorage *ret) {
return 0;
}
static int home_create(UserRecord *h, UserRecord **ret_home) {
static int home_create(UserRecord *h, Hashmap *blobs, UserRecord **ret_home) {
_cleanup_strv_free_erase_ char **effective_passwords = NULL;
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
@@ -1367,6 +1377,10 @@ static int home_create(UserRecord *h, UserRecord **ret_home) {
if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h));
r = home_apply_new_blob_dir(h, blobs);
if (r < 0)
return r;
switch (user_record_storage(h)) {
case USER_LUKS:
@@ -1572,7 +1586,7 @@ static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags
return has_mount; /* return true if the home record is already active */
}
static int home_update(UserRecord *h, UserRecord **ret) {
static int home_update(UserRecord *h, Hashmap *blobs, UserRecord **ret) {
_cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL;
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(password_cache_free) PasswordCache cache = {};
@@ -1591,6 +1605,10 @@ static int home_update(UserRecord *h, UserRecord **ret) {
if (r < 0)
return r;
r = home_apply_new_blob_dir(h, blobs);
if (r < 0)
return r;
r = home_setup(h, flags, &setup, &cache, &header_home);
if (r < 0)
return r;
@@ -1611,6 +1629,10 @@ static int home_update(UserRecord *h, UserRecord **ret) {
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup.root_fd, USER_RECONCILE_HOST_WON);
if (r < 0)
return r;
r = home_extend_embedded_identity(new_home, h, &setup);
if (r < 0)
return r;
@@ -1682,7 +1704,7 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(password_cache_free) PasswordCache cache = {};
HomeSetupFlags flags = 0;
int r;
int r, reconciled;
assert(h);
assert(ret_home);
@@ -1702,9 +1724,9 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
if (r < 0)
return r;
r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home);
if (r < 0)
return r;
reconciled = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home);
if (reconciled < 0)
return reconciled;
r = home_maybe_shift_uid(h, flags, &setup);
if (r < 0)
@@ -1736,6 +1758,10 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
if (r < 0)
return r;
r = home_reconcile_blob_dirs(new_home, setup.root_fd, reconciled);
if (r < 0)
return r;
r = home_extend_embedded_identity(new_home, h, &setup);
if (r < 0)
return r;
@@ -1850,10 +1876,12 @@ static int run(int argc, char *argv[]) {
_cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_fclose_ FILE *opened_file = NULL;
_cleanup_hashmap_free_ Hashmap *blobs = NULL;
unsigned line = 0, column = 0;
const char *json_path = NULL;
const char *json_path = NULL, *blob_filename;
FILE *json_file;
usec_t start;
JsonVariant *fdmap, *blob_fd_variant;
int r;
start = now(CLOCK_MONOTONIC);
@@ -1884,6 +1912,48 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column);
fdmap = json_variant_by_key(v, HOMEWORK_BLOB_FDMAP_FIELD);
if (fdmap) {
r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops);
if (r < 0)
return log_oom();
JSON_VARIANT_OBJECT_FOREACH(blob_filename, blob_fd_variant, fdmap) {
_cleanup_free_ char *filename = NULL;
_cleanup_close_ int fd = -EBADF;
assert(json_variant_is_integer(blob_fd_variant));
assert(json_variant_integer(blob_fd_variant) >= 0);
assert(json_variant_integer(blob_fd_variant) <= INT_MAX - SD_LISTEN_FDS_START);
fd = SD_LISTEN_FDS_START + (int) json_variant_integer(blob_fd_variant);
if (DEBUG_LOGGING) {
_cleanup_free_ char *resolved = NULL;
r = fd_get_path(fd, &resolved);
log_debug("Got blob from daemon: %s (%d) → %s",
blob_filename, fd, resolved ?: STRERROR(r));
}
filename = strdup(blob_filename);
if (!filename)
return log_oom();
r = fd_cloexec(fd, true);
if (r < 0)
return log_error_errno(r, "Failed to enable O_CLOEXEC on blob %s: %m", filename);
r = hashmap_put(blobs, filename, FD_TO_PTR(fd));
if (r < 0)
return log_error_errno(r, "Failed to insert blob %s into map: %m", filename);
TAKE_PTR(filename); /* Ownership transfers to hashmap */
TAKE_FD(fd);
}
r = json_variant_filter(&v, STRV_MAKE(HOMEWORK_BLOB_FDMAP_FIELD));
if (r < 0)
return log_error_errno(r, "Failed to strip internal fdmap from JSON: %m");
}
home = user_record_new();
if (!home)
return log_oom();
@@ -1927,11 +1997,11 @@ static int run(int argc, char *argv[]) {
else if (streq(argv[1], "deactivate-force"))
r = home_deactivate(home, true);
else if (streq(argv[1], "create"))
r = home_create(home, &new_home);
r = home_create(home, blobs, &new_home);
else if (streq(argv[1], "remove"))
r = home_remove(home);
else if (streq(argv[1], "update"))
r = home_update(home, &new_home);
r = home_update(home, blobs, &new_home);
else if (streq(argv[1], "resize")) /* Resize on user request */
r = home_resize(home, false, &new_home);
else if (streq(argv[1], "resize-auto")) /* Automatic resize */
+1
View File
@@ -2,6 +2,7 @@
systemd_homework_sources = files(
'home-util.c',
'homework-blob.c',
'homework-cifs.c',
'homework-directory.c',
'homework-fscrypt.c',
+12
View File
@@ -77,6 +77,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="CreateHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="CreateHomeEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="RealizeHome"/>
@@ -97,6 +101,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="UpdateHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="UpdateHomeEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ResizeHome"/>
@@ -183,6 +191,10 @@
send_interface="org.freedesktop.home1.Home"
send_member="Update"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="UpdateEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="Resize"/>
+119 -1
View File
@@ -3,6 +3,7 @@
#include <sys/xattr.h>
#include "errno-util.h"
#include "fd-util.h"
#include "home-util.h"
#include "id128-util.h"
#include "libcrypt-util.h"
@@ -10,6 +11,7 @@
#include "recovery-key.h"
#include "mountpoint-util.h"
#include "path-util.h"
#include "sha256.h"
#include "stat-util.h"
#include "user-record-util.h"
#include "user-util.h"
@@ -282,7 +284,7 @@ int user_record_add_binding(
gid_t gid) {
_cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL;
_cleanup_free_ char *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL;
_cleanup_free_ char *blob = NULL, *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL;
sd_id128_t mid;
int r;
@@ -291,6 +293,10 @@ int user_record_add_binding(
if (!h->json)
return -EUNATCH;
blob = path_join(home_system_blob_dir(), h->user_name);
if (!blob)
return -ENOMEM;
r = sd_id128_get_machine(&mid);
if (r < 0)
return r;
@@ -331,6 +337,7 @@ int user_record_add_binding(
r = json_build(&new_binding_entry,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("blobDirectory", JSON_BUILD_STRING(blob)),
JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)),
JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(partition_uuid))),
JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(luks_uuid))),
@@ -370,6 +377,8 @@ int user_record_add_binding(
if (r < 0)
return r;
free_and_replace(h->blob_directory, blob);
if (storage >= 0)
h->storage = storage;
@@ -1383,6 +1392,15 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
if (hr->service && !streq(hr->service, "io.systemd.Home"))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");
if (hr->blob_directory) {
/* This function is always called w/o binding section, so if hr->blob_dir is set then the caller set it themselves */
assert((hr->mask & USER_RECORD_BINDING) == 0);
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage custom blob directories.");
}
if (json_variant_by_key(hr->json, HOMEWORK_BLOB_FDMAP_FIELD))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record contains unsafe internal fields.");
return 0;
}
@@ -1511,3 +1529,103 @@ int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) {
h->mask |= USER_RECORD_PER_MACHINE;
return 0;
}
int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_hashmap_free_ Hashmap *manifest = NULL;
const char *filename;
void *key, *value;
uint64_t total_size = 0;
int r;
assert(h);
assert(h->json);
assert(blobs);
assert(ret_failed);
/* Ensures that blobManifest exists (possibly creating it using the
* contents of blobs), and that the set of keys in both hashmaps are
* exactly the same. If it fails to handle one blob file, the filename
* is put it ret_failed for nicer error reporting. ret_failed is a pointer
* to the same memory blobs uses to store its keys, so it is valid for
* as long as blobs is valid and the corresponding key isn't removed! */
if (h->blob_manifest) {
/* blobManifest already exists. In this case we verify
* that the sets of keys are equal and that's it */
HASHMAP_FOREACH_KEY(value, key, h->blob_manifest)
if (!hashmap_contains(blobs, key))
return -EINVAL;
HASHMAP_FOREACH_KEY(value, key, blobs)
if (!hashmap_contains(h->blob_manifest, key))
return -EINVAL;
return 0;
}
/* blobManifest doesn't exist, so we need to create it */
HASHMAP_FOREACH_KEY(value, filename, blobs) {
_cleanup_free_ char *filename_dup = NULL;
_cleanup_free_ uint8_t *hash = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *hash_json = NULL;
int fd = PTR_TO_FD(value);
off_t initial, size;
*ret_failed = filename;
filename_dup = strdup(filename);
if (!filename_dup)
return -ENOMEM;
hash = malloc(SHA256_DIGEST_SIZE);
if (!hash)
return -ENOMEM;
initial = lseek(fd, 0, SEEK_CUR);
if (initial < 0)
return -errno;
r = sha256_fd(fd, BLOB_DIR_MAX_SIZE, hash);
if (r < 0)
return r;
size = lseek(fd, 0, SEEK_CUR);
if (size < 0)
return -errno;
if (!DEC_SAFE(&size, initial))
return -EOVERFLOW;
if (!INC_SAFE(&total_size, size))
total_size = UINT64_MAX;
if (total_size > BLOB_DIR_MAX_SIZE)
return -EFBIG;
if (lseek(fd, initial, SEEK_SET) < 0)
return -errno;
r = json_variant_new_hex(&hash_json, hash, SHA256_DIGEST_SIZE);
if (r < 0)
return r;
r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename_dup, hash);
if (r < 0)
return r;
TAKE_PTR(filename_dup); /* Ownership transfers to hashmap */
TAKE_PTR(hash);
r = json_variant_set_field(&v, filename, hash_json);
if (r < 0)
return r;
*ret_failed = NULL;
}
r = json_variant_set_field_non_null(&h->json, "blobManifest", v);
if (r < 0)
return r;
h->blob_manifest = TAKE_PTR(manifest);
return 0;
}
+7
View File
@@ -6,6 +6,11 @@
#include "user-record.h"
#include "group-record.h"
/* We intentionally use snake_case instead of the usual camelCase here to further
* reduce the chance of collision with a field any legitimate user record may ever
* want to set. */
#define HOMEWORK_BLOB_FDMAP_FIELD "__systemd_homework_internal_blob_fdmap"
int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid);
int group_record_synthesize(GroupRecord *g, UserRecord *u);
@@ -63,3 +68,5 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error);
bool user_record_shall_rebalance(UserRecord *h);
int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight);
int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed);
+1 -1
View File
@@ -539,7 +539,7 @@ int fork_agent(const char *name, const int except[], size_t n_except, pid_t *ret
r = safe_fork_full(name,
NULL,
except,
(int*) except, /* safe_fork_full only changes except if you pass in FORK_PACK_FDS, which we don't */
n_except,
FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG|FORK_RLIMIT_NOFILE_SAFE,
ret_pid);
+37
View File
@@ -3,8 +3,14 @@
#include "cap-list.h"
#include "format-util.h"
#include "fs-util.h"
#include "glyph-util.h"
#include "hashmap.h"
#include "hexdecoct.h"
#include "path-util.h"
#include "pretty-print.h"
#include "process-util.h"
#include "rlimit-util.h"
#include "sha256.h"
#include "strv.h"
#include "terminal-util.h"
#include "user-record-show.h"
@@ -213,6 +219,37 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
printf("\n");
}
if (hr->blob_directory) {
_cleanup_free_ char **filenames = NULL;
size_t n_filenames = 0;
r = hashmap_dump_keys_sorted(hr->blob_manifest, (void***) &filenames, &n_filenames);
if (r < 0) {
errno = -r;
printf(" Blob Dir.: %s (can't iterate: %m)\n", hr->blob_directory);
} else
printf(" Blob Dir.: %s\n", hr->blob_directory);
for (size_t i = 0; i < n_filenames; i++) {
_cleanup_free_ char *path = NULL, *link = NULL, *hash = NULL;
const char *filename = filenames[i];
const uint8_t *hash_bytes = hashmap_get(hr->blob_manifest, filename);
bool last = i == n_filenames - 1;
path = path_join(hr->blob_directory, filename);
if (path)
(void) terminal_urlify_path(path, filename, &link);
hash = hexmem(hash_bytes, SHA256_DIGEST_SIZE);
printf(" %s %s %s(%s)%s\n",
special_glyph(last ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH),
link ?: filename,
ansi_grey(),
hash ?: "can't display hash",
ansi_normal());
}
}
storage = user_record_storage(hr);
if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */
printf(" Storage: %s%s\n", user_storage_to_string(storage),
+63
View File
@@ -15,11 +15,13 @@
#include "path-util.h"
#include "pkcs11-util.h"
#include "rlimit-util.h"
#include "sha256.h"
#include "string-table.h"
#include "strv.h"
#include "uid-classification.h"
#include "user-record.h"
#include "user-util.h"
#include "utf8.h"
#define DEFAULT_RATELIMIT_BURST 30
#define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE)
@@ -142,6 +144,9 @@ static UserRecord* user_record_free(UserRecord *h) {
free(h->location);
free(h->icon_name);
free(h->blob_directory);
hashmap_free(h->blob_manifest);
free(h->shell);
strv_free(h->environment);
@@ -1074,6 +1079,7 @@ static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispa
static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch binding_dispatch_table[] = {
{ "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 },
{ "imagePath", JSON_VARIANT_STRING, json_dispatch_image_path, offsetof(UserRecord, image_path), 0 },
{ "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 },
{ "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
@@ -1110,6 +1116,52 @@ static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatch
return json_dispatch(m, binding_dispatch_table, flags, userdata);
}
static int dispatch_blob_manifest(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
_cleanup_hashmap_free_ Hashmap *manifest = NULL;
Hashmap **ret = ASSERT_PTR(userdata);
JsonVariant *value;
const char *key;
int r;
if (!variant)
return 0;
if (!json_variant_is_object(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
JSON_VARIANT_OBJECT_FOREACH(key, value, variant) {
_cleanup_free_ char *filename = NULL;
_cleanup_free_ uint8_t *hash = NULL;
if (!json_variant_is_string(value))
return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid hash.", key);
if (!suitable_blob_filename(key))
return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Blob entry '%s' has invalid filename.", key);
filename = strdup(key);
if (!filename)
return json_log_oom(value, flags);
hash = malloc(SHA256_DIGEST_SIZE);
if (!hash)
return json_log_oom(value, flags);
r = parse_sha256(json_variant_string(value), hash);
if (r < 0)
return json_log(value, flags, r, "Blob entry '%s' has invalid hash: %s", filename, json_variant_string(value));
r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename, hash);
if (r < 0)
return json_log(value, flags, r, "Failed to insert blob manifest entry '%s': %m", filename);
TAKE_PTR(filename); /* Ownership transfers to hashmap */
TAKE_PTR(hash);
}
hashmap_free_and_replace(*ret, manifest);
return 0;
}
int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) {
sd_id128_t mid;
int r;
@@ -1226,6 +1278,8 @@ static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDisp
static const JsonDispatch per_machine_dispatch_table[] = {
{ "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
{ "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
{ "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 },
{ "blobManifest", JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 },
{ "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE },
{ "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 },
{ "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 },
@@ -1560,6 +1614,8 @@ int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_fla
static const JsonDispatch user_dispatch_table[] = {
{ "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), JSON_RELAX},
{ "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 },
{ "blobDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), 0 },
{ "blobManifest", JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 },
{ "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 },
{ "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE },
{ "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE },
@@ -2373,6 +2429,13 @@ int user_record_test_password_change_required(UserRecord *h) {
return change_permitted ? 0 : -EROFS;
}
int suitable_blob_filename(const char *name) {
/* Enforces filename requirements as described in docs/USER_RECORD_BULK_DIRS.md */
return filename_is_valid(name) &&
in_charset(name, URI_UNRESERVED) &&
name[0] != '.';
}
static const char* const user_storage_table[_USER_STORAGE_MAX] = {
[USER_CLASSIC] = "classic",
[USER_LUKS] = "luks",
+7
View File
@@ -6,6 +6,7 @@
#include "sd-id128.h"
#include "hashmap.h"
#include "json.h"
#include "missing_resource.h"
#include "time-util.h"
@@ -243,6 +244,9 @@ typedef struct UserRecord {
char *icon_name;
char *location;
char *blob_directory;
Hashmap *blob_manifest;
UserDisposition disposition;
uint64_t last_change_usec;
uint64_t last_password_change_usec;
@@ -449,6 +453,9 @@ int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags);
int per_machine_match(JsonVariant *entry, JsonDispatchFlags flags);
int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask);
#define BLOB_DIR_MAX_SIZE (UINT64_C(64) * U64_MB)
int suitable_blob_filename(const char *name);
const char* user_storage_to_string(UserStorage t) _const_;
UserStorage user_storage_from_string(const char *s) _pure_;
+134 -3
View File
@@ -156,9 +156,7 @@ PASSWORD=xEhErW0ndafV4s homectl with test-user -- test ! -f /home/test-user/xyz
(! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz)
PASSWORD=xEhErW0ndafV4s homectl with test-user -- touch /home/test-user/xyz
PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz
PASSWORD=xEhErW0ndafV4s homectl with test-user -- rm /home/test-user/xyz
PASSWORD=xEhErW0ndafV4s homectl with test-user -- test ! -f /home/test-user/xyz
(! PASSWORD=xEhErW0ndafV4s homectl with test-user -- test -f /home/test-user/xyz)
# CAREFUL adding more `homectl with` tests here. Auth can get rate-limited and cause the tests to fail.
wait_for_state test-user inactive
homectl remove test-user
@@ -170,6 +168,139 @@ if ! systemd-detect-virt -cq ; then
homectl remove test-user2
fi
# blob directory tests
# See docs/USER_RECORD_BLOB_DIRS.md
checkblob() {
test -f "/var/cache/systemd/home/blob-user/$1"
stat -c "%u %#a" "/var/cache/systemd/home/blob-user/$1" | grep "^0 0644"
test -f "/home/blob-user/.identity-blob/$1"
stat -c "%u %#a" "/home/blob-user/.identity-blob/$1" | grep "^12345 0644"
diff "/var/cache/systemd/home/blob-user/$1" "$2"
diff "/var/cache/systemd/home/blob-user/$1" "/home/blob-user/.identity-blob/$1"
}
mkdir /tmp/blob1 /tmp/blob2
echo data1 blob1 > /tmp/blob1/test1
echo data1 blob2 > /tmp/blob2/test1
echo data2 blob1 > /tmp/blob1/test2
echo data2 blob2 > /tmp/blob2/test2
echo invalid filename > /tmp/blob1/файл
echo data3 > /tmp/external-test3
echo avatardata > /tmp/external-avatar
ln -s /tmp/external-avatar /tmp/external-avatar-lnk
dd if=/dev/urandom of=/tmp/external-barely-fits bs=1M count=64
dd if=/dev/urandom of=/tmp/external-toobig bs=1M count=65
# create w/ prepopulated blob dir
NEWPASSWORD=EMJuc3zQaMibJo homectl create blob-user \
--disk-size=min --luks-discard=yes \
--luks-pbkdf-type=pbkdf2 --luks-pbkdf-time-cost=1ms \
--uid=12345 \
--blob=/tmp/blob1
inspect blob-user
PASSWORD=EMJuc3zQaMibJo homectl activate blob-user
inspect blob-user
test -d /var/cache/systemd/home/blob-user
stat -c "%u %#a" /var/cache/systemd/home/blob-user | grep "^0 0755"
test -d /home/blob-user/.identity-blob
stat -c "%u %#a" /home/blob-user/.identity-blob | grep "^12345 0700"
checkblob test1 /tmp/blob1/test1
(! checkblob test1 /tmp/blob2/test1 )
checkblob test2 /tmp/blob1/test2
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
(! checkblob test3 /tmp/external-test3 )
(! checkblob avatar /tmp/external-avatar )
# append files to existing blob, both well-known and other
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b test3=/tmp/external-test3 --avatar=/tmp/external-avatar
inspect blob-user
checkblob test1 /tmp/blob1/test1
(! checkblob test1 /tmp/blob2/test1 )
checkblob test2 /tmp/blob1/test2
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
checkblob test3 /tmp/external-test3
checkblob avatar /tmp/external-avatar
# delete files from existing blob, both well-known and other
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b test3= --avatar=
inspect blob-user
checkblob test1 /tmp/blob1/test1
(! checkblob test1 /tmp/blob2/test1 )
checkblob test2 /tmp/blob1/test2
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
(! checkblob test3 /tmp/external-test3 )
(! checkblob avatar /tmp/external-avatar )
# swap entire blob directory
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b /tmp/blob2
inspect blob-user
(! checkblob test1 /tmp/blob1/test1 )
checkblob test1 /tmp/blob2/test1
(! checkblob test2 /tmp/blob1/test2 )
checkblob test2 /tmp/blob2/test2
(! checkblob фаил /tmp/blob1/фаил )
(! checkblob test3 /tmp/external-test3 )
(! checkblob avatar /tmp/external-avatar )
# create and delete files while swapping blob directory. Also symlinks.
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b /tmp/blob1 -b test2= -b test3=/tmp/external-test3 --avatar=/tmp/external-avatar-lnk
inspect blob-user
checkblob test1 /tmp/blob1/test1
(! checkblob test1 /tmp/blob2/test1 )
(! checkblob test2 /tmp/blob1/test2 )
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
checkblob test3 /tmp/external-test3
checkblob avatar /tmp/external-avatar # target of the link
# clear the blob directory
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b /tmp/blob2 -b test3=/tmp/external-test3 --blob=
inspect blob-user
(! checkblob test1 /tmp/blob1/test1 )
(! checkblob test1 /tmp/blob2/test1 )
(! checkblob test2 /tmp/blob1/test2 )
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
(! checkblob test3 /tmp/external-test3 )
(! checkblob avatar /tmp/external-avatar )
# file that's exactly 64M still fits
PASSWORD=EMJuc3zQaMibJo homectl update blob-user \
-b barely-fits=/tmp/external-barely-fits
(! checkblob test1 /tmp/blob1/test1 )
(! checkblob test1 /tmp/blob2/test1 )
(! checkblob test2 /tmp/blob1/test2 )
(! checkblob test2 /tmp/blob2/test2 )
(! checkblob фаил /tmp/blob1/фаил )
(! checkblob test3 /tmp/external-test3 )
(! checkblob avatar /tmp/external-avatar )
checkblob barely-fits /tmp/external-barely-fits
# error out if the file is too big
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b huge=/tmp/external-toobig )
# error out if filenames are invalid
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b .hidden=/tmp/external-test3 )
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b "with spaces=/tmp/external-test3" )
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b with=equals=/tmp/external-test3 )
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b файл=/tmp/external-test3 )
(! PASSWORD=EMJuc3zQaMibJo homectl update blob-user -b special@chars=/tmp/external-test3 )
homectl deactivate blob-user
wait_for_state blob-user inactive
homectl remove blob-user
# userdbctl tests
export PAGER=
+1
View File
@@ -30,6 +30,7 @@ RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_ALG AF_INET AF_INET6
RestrictNamespaces=mnt user
RestrictRealtime=yes
StateDirectory=systemd/home
CacheDirectory=systemd/home
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service @mount quotactl