diff --git a/.mkosi/mkosi.fedora b/.mkosi/mkosi.fedora
index b0673f707a399..2de0976bc38de 100644
--- a/.mkosi/mkosi.fedora
+++ b/.mkosi/mkosi.fedora
@@ -38,10 +38,12 @@ BuildPackages=
libblkid-devel
libcap-devel
libcurl-devel
+ libfdisk-devel
libgcrypt-devel
libidn2-devel
libmicrohttpd-devel
libmount-devel
+ libpwquality-devel
libseccomp-devel
libselinux-devel
libtool
@@ -51,6 +53,7 @@ BuildPackages=
lz4-devel
m4
meson
+ openssl-devel
pam-devel
pcre2-devel
pkgconfig
@@ -61,6 +64,7 @@ BuildPackages=
xz-devel
Packages=
+ e2fsprogs
libidn2
BuildDirectory=mkosi.builddir
diff --git a/CRYPTSETUP b/CRYPTSETUP
new file mode 100644
index 0000000000000..bca0e07b80615
--- /dev/null
+++ b/CRYPTSETUP
@@ -0,0 +1,63 @@
+# Using a Yubikey for unlocking arbitrary LUKS devices.
+#
+# A few notes on the used parameters:
+#
+# → We use RSA (and not ECC), since Yubikeys support PKCS#11 Decrypt() only for
+# RSA keys (ECC keys can only be used for signing?)
+# → We use RSA2048, which is the longest key size current Yubikeys support for
+# RSA
+# → LUKS key size must be << 2048bit due to RSA padding, hence we use 128 bytes
+# → We use Yubikey key slot 9d, since that's apparently the keyslot to use for
+# decryption purposes, see:
+# https://developers.yubico.com/PIV/Introduction/Certificate_slots.html
+
+# Make sure noone can read the files we generate, but us
+umask 077
+
+# Clear the Yubikey from any old keys
+ykman piv reset
+
+# Generate a new private/public key pair on th device, store the public key in
+# 'pubkey.pem'.
+ykman piv generate-key -a RSA2048 9d pubkey.pem
+
+# Create a self-signed certificate from this public key, and store it on the
+# device.
+ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem
+
+# Check if the newly create key on the Yubikey shows up as token in
+# PKCS#11. Have a look at the output, and copy the resulting token URI to the
+# clipboard.
+p11tool --list-tokens
+
+# Generate a (secret) random key to use as LUKS decryption key.
+dd if=/dev/urandom of=plaintext.bin bs=128 count=1
+
+# Encrypt this newly generated LUKS decryption key using the public key whose
+# private key is on the Yubikey, store the result in
+# /etc/encrypted-luks-key.bin, where we'll look for it during boot.
+openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/encrypted-luks-key.bin
+
+# Configure the LUKS decryption key on the LUKS device. We use very low pbkdf
+# settings since the key already has quite a high quality (it comes directly
+# from /dev/urandom after all), and thus we don't need to do much key
+# derivation.
+cryptsetup luksAddKey /dev/sda1 plaintext.bin --pbkdf-memory=32 --pbkdf-parallel=1 --pbkdf-force-iterations=4
+
+# Now securely delete the plain text LUKS key, we don't need it anymore, and
+# since it contains secret key material it should be removed from disk
+# thoroughly.
+shred -u plaintext.bin
+
+# We don't need the public key anymore either, let's remove it too. Since this
+# one is not security sensitive we just do a regular "rm" here.
+rm pubkey.pem
+
+# Test: Let's run systemd-cryptsetup to test if this all worked. The option
+# string should contain the full PKCS#11 URI we have in the clipboard, it tells
+# the tool how to decypher the encrypted LUKS key.
+systemd-cryptsetup attach mytest /dev/sda1 /etc/encrypted-luks-key.bin 'pkcs11-uri=pkcs11:…'
+
+# If that worked, let's now add the same line persistently to /etc/crypttab,
+# for the future.
+echo "mytest /dev/sda1 /etc/encrypted-luks-key 'pkcs11-uri=pkcs11:…' >> /etc/crypttab
diff --git a/TODO b/TODO
index c5b5b86057ba3..1538e15d10d49 100644
--- a/TODO
+++ b/TODO
@@ -19,6 +19,14 @@ Janitorial Clean-ups:
Features:
+* socket units: allow creating a udev monitor socket with ListenDevices= or so,
+ with matches, then actviate app thorugh that passing socket oveer
+
+* cryptsetup: allow encoding key directly in /etc/crypttab, maybe with a
+ "base64:" prefix. Useful in particular for pkcs11 mode.
+
+* cryptsetup: add logic to wait for pkcs11 token
+
* coredump: maybe when coredumping read a new xattr from /proc/$PID/exe that
may be used to mark a whole binary as non-coredumpable. Would fix:
https://bugs.freedesktop.org/show_bug.cgi?id=69447
@@ -395,15 +403,6 @@ Features:
* maybe introduce gpt auto discovery for /var/tmp?
-* maybe add gpt-partition-based user management: each user gets his own
- LUKS-encrypted GPT partition with a new GPT type. A small nss module
- enumerates users via udev partition enumeration. UIDs are assigned in a fixed
- way: the partition index is added as offset to some fixed base uid. User name
- is stored in GPT partition name. A PAM module authenticates the user via the
- LUKS partition password. Benefits: strong per-user security, compatibility
- with stateless/read-only/verity-enabled root. (other idea: do this based on
- loopback files in /home, without GPT involvement)
-
* gpt-auto logic: introduce support for discovering /var matching an image. For
that, use a partition type UUID that is hashed from the OS name (as encoded
in /etc/os-release), the architecture, and 4 new bits from the gpt flags
diff --git a/TODO-HOME b/TODO-HOME
new file mode 100644
index 0000000000000..4263f637e0826
--- /dev/null
+++ b/TODO-HOME
@@ -0,0 +1,51 @@
+PRIMARILY:
+
+- pkcs11 smart card support: pin certificate in record, then for auth, generate random data, ask smartcard to sign it, then verify signature. For LUKS key store encrypted key in record, decrypt it with smartcard.
+- write fscrypt key into client's keyring
+- don't use argon2 with pkcs#11 cryptsetup mode
+- move the ssh authorized keys stuff from homectl to userdbctl
+- honour passwordChangeNow, and unset it in homectl passwd
+- fix borked yubikey cryptsetup stuff?
+
+Before first release:
+- unit file lockdown (caps, …)
+- extend test case to cover more
+- man pages:
+ cryptsetup pkcs11
+ pam_systemd update
+ nss-systemd update
+- blog story
+- fix userwork and homework paths
+- drop LOG_DEBUG being forced in userdbd and homed
+- drop debug=true being forced in pam_systemd_home
+- performance data
+
+Later:
+- when user tries to log into record signed by unrecognized key, automatically add key to our chain after polkit auth
+- hook up machined/nspawn users with a varlink user query interface
+- rollback when resize fails mid-operation
+- forget key on suspend (requires gnome rework so that lock screen runs outside of uid)
+- resize on login?
+- update LUKS password on login if we find there's a password that unlocks the JSON record but not the LUKS device.
+- always fstrim on logout?
+- compare with accounts daemon
+- create on activate?
+- properties: icon url?, preferred session type?, administrator bool (which translates to 'wheel' membership)?, address?, telephone?, vcard?, samba stuff?, parental controls?
+- communicate clearly when usb stick is safe to remove. probably involves
+ beefing up logind to make pam session close hook synchronous and wait until
+ systemd --user is shut down.
+- logind: maybe keep a "busy fd" as long as there's a non-released session around or the user@.service
+- fscrypt key mgmt (maybe in xattr?)
+- shrink fs on logout?
+- maybe make automatic, read-only, time-based reflink-copies of LUKS disk images (think: time machine)
+- distuingish destroy / remove (i.e. currently we can unregister a user, unregister+remove their home directory, but not just remove their home directory)
+- in systemd's PAMName= logic: query passwords with ssh-askpassword, so that we can make "loginctl set-linger" mode work
+- fingerprint authentication, pattern authentication, …
+- make sure "classic" user records can also be managed by homed
+- description field for groups
+- make size of $XDG_RUNTIME_DIR configurable in user record
+- reuse pwquality magic in firstboot
+- query password from kernel keyring first
+- update in absence
+- add a "access mode" + "fstype" field to the "status" section of json identity records reflecting the actually used access mode and fstype, even on non-luks backends
+- move acct mgmt stuff from pam_systemd_home to pam_systemd?
diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md
new file mode 100644
index 0000000000000..55cb2f064974a
--- /dev/null
+++ b/docs/GROUP_RECORD.md
@@ -0,0 +1,156 @@
+---
+title: JSON Group Records
+---
+
+# JSON Group Records
+
+Long story short: JSON Group Records are to `struct group` what [JSON User
+Records](https://systemd.io/USER_RECORD.md) are to `struct passwd`.
+
+Conceptually, much of what applies to JSON user records also applies to JSON
+group records. They also consist of seven sections, with similar properties and
+they carry some identical (or at least very similar) fields.
+
+## Fields in the `regular` section
+
+`groupName` → A string with the UNIX group name. Matches the `gr_name` field of
+UNIX/glibc NSS `struct group`, or the shadow structure `struct sgrp`'s
+`sg_namp` field.
+
+`realm` → The "realm" the group belongs to, conceptually identical to the same
+field of user records. A string in DNS domain name syntax.
+
+`disposition` → The disposition of the group, conceptually identical to the
+same field of user records. A string.
+
+`service` → A string, an identifier for the service managing this group record
+(this field is typically in reverse domain name syntax.)
+
+`lastChangeUSec` → An unsigned 64bit integer, a timestamp (in µs since the UNIX
+epoch 1970) of the last time the group record has been modified. (Covers only
+the `regular`, `perMachine` and `privileged` sections).
+
+`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group
+ID (GID) to use for the group. This corresponds to the `gr_gid` field of
+`struct group`.
+
+`members` → An array of strings, listing user names that are members of this
+group. Note that JSON user records also contain a `memberOf` field, or in other
+words a group membership can either be denoted in the JSON user record or in
+the JSON record, or in both. The list of memberships should be determined as
+the combination of both lists (plus optionally others). If a user is listed as
+member of a group and doesn't exist it should be ignored. This field
+corresponds to the `gr_mem` field of `struct group` and the `sg_mem` field of
+`struct sgrp`.
+
+`administrators` → Similarly, an array of strings, listing user names that
+shall be considered "administrators" of this group. This field corresponds to
+the `sg_adm` field of `struct sgrp`.
+
+`privileged`/`perMachine`/`binding`/`status`/`signature`/`secret` → The
+objects/arrays for the other six group record sections. These are organized the
+same way as for the JSON user records, and have the same semantics.
+
+## Fields in the `privileged` section
+
+The following fields are defined:
+
+`hashedPassword` → An array of strings with UNIX hashed passwords; see the
+matching field for user records for details. This field corresponds to the
+`sg_passwd` field of `struct sgrp` (and `gr_passwd` of `struct group` in a
+way).
+
+## Fields in the `perMachine` section
+
+`matchMachineId`/`matchHostname` → Strings, match expressions similar as for
+user records, see the user record documentation for details.
+
+The following fields are defined for the `perMachine` section and are defined
+equivalent to the fields of the same name in the `regular` section, and
+override those:
+
+`gid`, `members`, `administrators`
+
+## Fields in the `binding` section
+
+The following fields are defined for the `binding` section, and are equivalent
+to the fields of the same name in the `regular` and `perMachine` sections:
+
+`gid`
+
+## Fields in the `status` section
+
+The following fields are defined in the `status` section, and are mostly
+equivalent to the fields of the same name in the `regular` section, though with
+slightly different conceptual semantics, see the same fields in the user record
+documentation:
+
+`service`
+
+## Fields in the `signature` section
+
+The fields in this section are defined identically to those in the matching
+section in the user record.
+
+## Fields in the `secret` section
+
+Currently no fields are defined in this section for group records.
+
+## Mapping to `struct group` and `struct sgrp`
+
+When mapping classic UNIX group records (i.e. `struct group` and `struct sgrp`)
+to JSON group records the following mappings should be applied:
+
+| Structure | Field | Section | Field | Condition |
+|----------------|-------------|--------------|------------------|----------------------------|
+| `struct group` | `gr_name` | `regular` | `groupName` | |
+| `struct group` | `gr_passwd` | `privileged` | `password` | (See notes below) |
+| `struct group` | `gr_gid` | `regular` | `gid` | |
+| `struct group` | `gr_mem` | `regular` | `members` | |
+| `struct sgrp` | `sg_namp` | `regular` | `groupName` | |
+| `struct sgrp` | `sg_passwd` | `privileged` | `password` | (See notes below) |
+| `struct sgrp` | `sg_adm` | `regular` | `administrators` | |
+| `struct sgrp` | `sg_mem` | `regular` | `members` | |
+
+At this time almost all Linux machines employ shadow passwords, thus the
+`gr_passwd` field in `struct group` is set to `"x"`, and the actual password
+is stored in the shadow entry `struct sgrp`'s field `sg_passwd`.
+
+## Extending These Records
+
+The same logic and recommendations apply as for JSON user records
+
+## Examples
+
+A reasonable group record for a system group might look like this:
+
+```json
+{
+ "groupName" : "systemd-resolve",
+ "gid" : 193,
+ "status" : {
+ "6b18704270e94aa896b003b4340978f1" : {
+ "service" : "io.systemd.NameServiceSwitch"
+ }
+ }
+}
+```
+
+And here's a more complete one for a regular group:
+
+```json
+{
+ "groupName" : "grobie",
+ "binding" : {
+ "6b18704270e94aa896b003b4340978f1" : {
+ "gid" : 60232
+ }
+ },
+ "disposition" : "regular",
+ "status" : {
+ "6b18704270e94aa896b003b4340978f1" : {
+ "service" : "io.systemd.Home"
+ }
+ }
+}
+```
diff --git a/docs/HOME_DIRECTORY.md b/docs/HOME_DIRECTORY.md
new file mode 100644
index 0000000000000..b07e0c4886f64
--- /dev/null
+++ b/docs/HOME_DIRECTORY.md
@@ -0,0 +1,163 @@
+---
+title: Home Directories
+---
+
+# Home Directories
+
+[`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html)
+manages home directories of regular ("human") users. Each directory it manages
+encapsulates both the data store and the user record of the user so that it
+comprehensively describes the user account, and is thus naturally portable
+between systems without any further, external metadata. This document describes
+the format used by these home directories, in context of the storage mechanism
+used.
+
+## General Structure
+
+Inside of the home directory a file `~/.identity` contains the JSON formatted
+user record of the user. It follows the format defined in [`JSON User
+Records`](https://systemd.io/USER_RECORDS). It is recommended to bring the
+record into 'normalized' form (i.e. all objects should contain their fields
+sorted alphabetically by their key) before storing it there, though this is not
+required nor enforced. Since the user record is cryptographically signed the
+user cannot make modifications to the file on their own (at least not without
+corrupting it, or knowing the private key used for signing the record). Note
+that user records are stored here without their `binding`, `status` and
+`secret` sections, i.e. only with the sections included in the signature plus
+the signature section itself.
+
+## Storage Mechanism: Plain Directory/`btrfs` Subvolume
+
+If the plain directory or `btrfs` subvolume storage mechanism of
+`systemd-homed` is used (i.e. `--storage=directory` or `--storage=subvolume` on
+the
+[`homectl(1)`](https://www.freedesktop.org/software/systemd/man/homectl.html)
+command line) the home directory requires no special set-up besides including
+the user record in the `~/.identity` file.
+
+It is recommended to name home directories managed this way by
+`systemd-homed.service` by the user name, suffixed with `.homedir` (example:
+`lennart.homedir` for a user `lennart`) but this is not enforced. When the user
+is logged in the directory is generally mounted to `/home/$USER` (in our
+example: `/home/lennart`), thus dropping the suffix while the home directory is
+active. `systemd-homed` will automatically discover home directories named this
+way in `/home/*.homedir` and synthesize NSS user records for them as they show
+up.
+
+## Storage Mechanism: `fscrypt` Directories
+
+This storage mechanism is mostly identical to the plain directory storage
+mechanism, except that the home directory is encrypted using `fscrypt`. (Use
+`--storage=fscrypt` on the `homectl` command line.) There is currently no key
+management in place for this, the `fscrypt` encryption key is directly derived
+from the supplied user password using PBKDF2. Due to this password changes are
+currently not supported if this storage mechanism is used. The salt value for
+the key derivation function is stored in the `trusted.fscrypt_salt` extended
+attribute, encoded in base64.
+
+## Storage Mechanism: `cifs` Home Directories
+
+In this storage mechanism the home directory is mounted from a CIFS server and
+service at login, configured inside the user record. (Use `--storage=cifs` on
+the `homectl` command line.) The local password of the user is used to log into
+the CIFS service. The directory share needs to contain the user record in
+`~/.identity` as well. Note that this means that the user record needs to be
+registered locally before it can be mounted for the first time, since CIFS
+domain and server information needs to be known *before* the mount. Note that
+for all other storage mechanisms it is entirely sufficient if the directories
+or storage artifacts are placed at the right locations — all information to
+activate them can be derived automatically from their mere availability.
+
+## Storage Mechanism: `luks` Home Directories
+
+This is the most advanced and most secure storage mechanism and consists of a
+Linux file system inside a LUKS2 volume inside a loopback file (or on removable
+media). (Use `--storage=luks` on the `homectl` command line.) Specifically:
+
+* The image contains a GPT partition table. For now it should only contain a
+ single partition, and that partition must have the type UUID
+ `773f91ef-66d4-49b5-bd83-d683bf40ad16`. It's partition label must be the
+ user name.
+
+* This partition must contain a LUKS2 volume, whose label must be the user
+ name. The LUKS2 volume must contain a LUKS2 token field of type
+ `systemd-homed`. The JSON data of this token must have a `record` field,
+ containing a string with base64-encoded data. This data is the JSON user
+ record, in the same serialization as in `~/.identity`, though encrypted. The
+ JSON data of this token must also have an `iv` field, which contains a
+ base64-encoded binary initialization vector for the encryption. The
+ encryption used is the same as the LUKS2 volume itself uses, unlocked by the
+ same volume key, but based on its own IV.
+
+* Inside of this LUKS2 volume must be a Linux file system, one of `ext4`,
+ `btrfs` and `xfs`. The file system label must be the user name.
+
+* This file system should contain a single directory named after the user. This
+ directory will become the home directory of the user when activated. It
+ contains a second copy of the user record in the `~/.identity` file, like in
+ the other storage mechanisms.
+
+The image file should either reside in a directory `/home/` on the system,
+named after the user, suffixed with `.home`. When activated the container home
+directory is mounted to the same path, though with the `.home` suffix
+dropped. (e.g.: the loopback file `/home/waldo.home` is mounted to
+`/home/waldo` while activated.) When the image is stored on removable media
+(such as a USB stick) the image file can be directly `dd`'ed onto it, the
+format is unchanged. The GPT envelope should ensure the image is properly
+recognizable as a home directory both when used in a loopback file and on a
+removable USB stick.
+
+Rationale for the GPT partition table envelope: this way the image is nicely
+discoverable and recognizable already by partition managers as a home
+directory. Moreover, when copied onto a USB stick the GPT envelope makes sure
+the stick is properly recognizable as a portable home directory
+medium. (Moreover it allows to embed additional partitions later on, for
+example for allowing a multi-purpose USB stick that contains both a home
+directory and a generic storage volume.)
+
+Rationale for including the encrypted user record in the the LUKS2 header:
+Linux kernel file system implementations are generally not robust towards
+maliciously formatted file systems; there's a good chance that file system
+images can be used as attack vectors, exploiting the kernel. Thus it is
+necessary to validate the home directory image *before* mounting it and
+establishing a minimal level of trust. Since the user record data is
+cryptographically signed and user records not signed with a recognized private
+key are not accepted a minimal level of trust between the system and the home
+directory image is established.
+
+Rationale for storing the home directory one level below to root directory of
+the contained file system: this way special directories such as `lost+found/`
+do not show up in the user's home directory.
+
+## Algorithm
+
+Regardless of the storage mechanism used, an activated home directory
+necessarily involves a mount point to be established. In case of the
+directory-based storage mechanisms (`directory`, `subvolume` and `fscrypt`)
+this is a bind mount, in case of `cifs` this is a CIFS network mount, and in
+case of the LUKS2 backend a regular block device mount of the file system
+contained in the LUKS2 image. By requiring a mount for all cases (even for
+those that already are a directory) a clear logic is defined to distuingish
+active and inactive home directories, so that the directories become
+inaccessible under their regular path the instant they are
+deactivated. Moreover, the `nosuid`, `nodev` and `noexec` flags configured in
+the user record are applied when the bind mount is established.
+
+During activation, the user records retained on the host, the user record
+stored in the LUKS2 header (in case of the LUKS2 storage mechanism) and the
+user record stored inside the home directory in `~/.identity` are
+compared. Activation is only permitted if they match the same user and are
+signed by a recognized key. When the three instances differ in `lastChangeUSec`
+field, the newest record wins, and is propagated to the other two locations.
+
+During activation the file system checker (`fsck`) appropriate for the
+selected file system is automatically invoked, ensuring the file system is in a
+healthy state before it is mounted.
+
+If the UID assigned to a user does not match the owner of the home directory in
+the file system, the home directory is automatically and recursively `chown()`ed
+to the correct UID.
+
+Depending on the `discard` setting of the user record either the backing
+loopback file is `fallocate()`ed during activation, or the mounted file system
+is `FITRIM`ed after mounting, to ensure the setting is correctly enforced.
diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md
index 480ee231e7431..6cb0e144fbebc 100644
--- a/docs/UIDS-GIDS.md
+++ b/docs/UIDS-GIDS.md
@@ -94,7 +94,15 @@ but downstreams are strongly advised against doing that.)
`systemd` defines a number of special UID ranges:
-1. 61184…65519 → UIDs for dynamic users are allocated from this range (see the
+1. 60001…60513 → UIDs for home directories managed by
+ [`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html). UIDs
+ from this range are automatically assigned to any home directory discovered,
+ and persisted locally on first login. On different systems the same user
+ might get different UIDs assigned in case of conflict, though it is
+ attempted to make UID assignments stable, by hashing them from the user
+ name.
+
+2. 61184…65519 → UIDs for dynamic users are allocated from this range (see the
`DynamicUser=` documentation in
[`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)). This
range has been chosen so that it is below the 16bit boundary (i.e. below
@@ -109,7 +117,7 @@ but downstreams are strongly advised against doing that.)
user record resolving works correctly without those users being in
`/etc/passwd`.
-2. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of
+3. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of
per-container UID ranges. When the `--private-users=pick` switch is used (or
`-U`) then it will automatically find a so far unused 16bit subrange of this
range and assign it to the container. The range is picked so that the upper
@@ -230,7 +238,8 @@ the artifacts the container manager persistently leaves in the system.
| 5 | `tty` group | `systemd` | `/etc/passwd` |
| 6…999 | System users | Distributions | `/etc/passwd` |
| 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… |
-| 60001…61183 | Unused | | |
+| 60001…60513 | Human Users (homed) | `systemd` | `nss-systemd`
+| 60514…61183 | Unused | | |
| 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` |
| 65520…65533 | Unused | | |
| 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` |
diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md
new file mode 100644
index 0000000000000..9d56085a8f9a2
--- /dev/null
+++ b/docs/USER_GROUP_API.md
@@ -0,0 +1,235 @@
+---
+title: Varlink User/Group Lookup API
+---
+
+# Varlink User/Group Lookup API
+
+JSON User/Group Records (as described in the [JSON User
+Records](https://systemd.io/USER_RECORD) and [JSON Group
+Records](https://systemd.io/GROUP_RECORD) documents) that are defined on the
+local system may be queried with a [Varlink](https://varlink.org/) API. This
+API takes both the role of what
+[`getpwnam(3)`](http://man7.org/linux/man-pages/man3/getpwnam.3.html) and
+related calls are for `struct passwd`, as well as the interfaces modules
+implementing the [glibc Name Service Switch
+(NSS)](https://www.gnu.org/software/libc/manual/html_node/Name-Service-Switch.html)
+expose. Or in other words, it both allows applications to efficiently query
+user/group records from local services, and allows local subsystems to provide
+user/group records efficiently to local applications.
+
+This simple API exposes only three method calls, and requires only a small
+subset of the Varlink functionality.
+
+## Why Varlink?
+
+The API described in this document is based on a simple subset of the
+mechanisms described by [Varlink](https://varlink.org/). The choice of
+preferring Varlink over D-Bus and other IPCs in this context was made for three
+reasons:
+
+1. User/Group record resolution should work during early boot and late shutdown
+ without special handling. This is very hard to do with D-Bus, as the broker
+ service for D-Bus generally runs as a regular system daemon and is hence only
+ available at the latest boot stage.
+
+2. The JSON user/group records are native JSON data, hence picking an IPC
+ system that natively operates with JSON data is natural and clean.
+
+3. IPC systems such as D-Bus do not provide flow control and are thus unusable
+ for streaming data. They are useful to pass around short control messages,
+ but as soon as potentially many and large objects shall be transferred D-Bus
+ is not suitable, as any such streaming of messages would be considered
+ flooding in D-Bus' logic, and thus possibly result in termination of
+ communication. Since the APIs defined in this document need to support
+ enumerating potentially large numbers of users and groups, D-Bus is simply
+ not an appropriate option.
+
+## Concepts
+
+Each subsystem that needs to define users and groups on the local system is
+supposed to implement this API, and offer its interfaces on a Varlink
+`AF_UNIX`/`SOCK_STREAM` file system socket bound into the
+`/run/systemd/userdb/` directory. When a client wants to look up a user or
+group record, it contacts all sockets bound in this directory in parallel, and
+enqueues the same query to each. The first positive reply is then returned to
+the application, or if all fail the last seen error is returned instead.
+
+Unlike with glibc NSS there's no order or programmatic expression language
+defined in which queries are issued to the various services. Instead, all
+queries are always enqueued in parallel to all defined services, in order to
+make look-ups efficient, and the simple rule of "first successful lookup wins"
+is unconditionally followed for user and group look-ups (though not for
+membership lookups, see below).
+
+This simple scheme only works safely as long as every service providing
+user/group records carefully makes sure not to answer with conflicting
+records. This API does not define any mechanisms for dealing with user/group
+name/ID collisions during look-up nor during record registration. It assumes
+the various subsystems that want to offer user and group records to the rest of
+the system have made sufficiently sure in advance that their definitions do not
+collide with those of other services. Clients are not expected to merge
+multiple definitions for the same user or group, and will also not be able to
+detect conflicts and suppress such conflicting records.
+
+It is recommended to name the sockets in the directory in reverse domain name
+notation, but this is neither required nor enforced.
+
+## Well-Known Services
+
+Any subsystem that wants to provide user/group records can do so, simply by
+binding a socket in the aforementioned directory. By default two
+services are listening there, that have special relevance:
+
+1. `io.systemd.NameServiceSwitch` → This service makes the classic UNIX/glibc
+ NSS user/group records available as JSON User/Group records. Any such
+ records are automatically converted as needed, and possibly augmented with
+ information from the shadow databases.
+
+2. `io.systemd.Multiplexer` → This service multiplexes client queries to all
+ other running services. It's supposed to simplify client development: in
+ order to look up or enumerate user/group records it's sufficient to talk to
+ one service instead of all of them in parallel.
+
+Both these services are implemented by the same daemon
+`systemd-userdbd.service`.
+
+Note that these services currently implement a subset of Varlink only. For
+example, introspection is not available, and the resolver logic is not used.
+
+## Compatibility with NSS
+
+Two-way compatibility with classic UNIX/glibc NSS user/group records is
+provided. When using the Varlink API, lookups into databases provided only via
+NSS (and not natively via Varlink) are handled by the
+`io.systemd.NameServiceSwitch` service (see above). When using the NSS API
+(i.e. `getpwnam()` and friends) the `nss-systemd` module will automatically
+synthesize NSS records for users/groups natively defined via a Varlink
+API. Special care is taken to avoid recursion between these two compatibility
+mechanisms.
+
+Subsystems that shall provide user/group records to the system may choose
+between offering them via an NSS module or via this Varlink API, either way
+all records are accessible via both APIs, due to the two-way compatibility. It
+is also possible to provide the same records via both APIs directly, but in
+that case the compatibility logic must be turned off. There are mechanisms in
+place for this, please contact the systemd project for details, as these are
+currently not documented.
+
+## Caching of User Records
+
+This API defines no concepts for caching records. If caching is desired it
+should be implemented in the subsystems that provide the user records, not in
+the clients consuming them.
+
+## Method Calls
+
+```
+interface io.systemd.UserDatabase
+
+method GetUserRecord(
+ uid : ?int,
+ userName : ?string,
+ service : string
+) -> (
+ record : object,
+ incomplete : boolean
+)
+
+method GetGroupRecord(
+ gid : ?int,
+ groupName : ?string,
+ service : string
+) -> (
+ record : object,
+ incomplete : boolean
+)
+
+method GetMemberships(
+ userName : ?string,
+ groupName : ?string,
+ service : string
+) -> (
+ userName : string,
+ groupName : string
+)
+
+error NoRecordFound()
+error BadService()
+error ServiceNotAvailable()
+error ConflictingRecordFound()
+```
+
+The `GetUserRecord` method looks up or enumerates a user record. If the `uid`
+parameter is set it specifies the numeric UNIX UID to search for. If the
+`userName` parameter is set it specifies the name of the user to search
+for. Typically, only one of the two parameters are set, depending on whether a
+look-up by UID or by name is desired. However, clients may also specify both
+parameters, in which case a record matching both will be returned, and if only
+one exists that matches one of the two parameters but not the other an error of
+`ConflictingRecordFound` is returned. If neither of the two parameters are set
+the whole user database is enumerated. In this case the method call needs to be
+made with `more` set, so that multiple method call replies may be generated as
+effect, each carrying one user record.
+
+The `service` parameter is mandatory and should be set to the service name
+being talked to (i.e. to the same name as the `AF_UNIX` socket path, with the
+`/run/systemd/userdb/` prefix removed). This is useful to allow implementation
+of multiple services on the same socket (which is used by
+`systemd-userdbd.service`).
+
+The method call returns one or more user records, depending on which type of
+query is used (see above). The record is returned in the `record` field. The
+`incomplete` field indicates whether the record is complete. Services providing
+user record lookup should only pass the `privileged` section of user records to
+clients that either match the user the record is about or to sufficiently
+privileged clients, for all others the section must be removed so that no
+sensitive data is leaked this way. The `incomplete` parameter should indicate
+whether the record has been modified like this or not (i.e. it is `true` if a
+`privileged` section existed in the user record and was removed, and `false` if
+no `privileged` section existed or one existed but hasn't been removed).
+
+If no user record matching the specified UID or name is known the error
+`NoRecordFound` is returned (this is also returned if neither UID nor name are
+specified, and hence enumeration requested but the subsystem currently has no
+users defined).
+
+If a method call with an incorrectly set `service` field is received
+(i.e. either not set at all, or not to the service's own name) a `BadService`
+error is generated. Finally, `ServiceNotAvailable` should be returned when the
+backing subsystem is not operational for some reason and hence no information
+about the existence or non-existence of a record can be returned nor any user
+record at all.
+
+The `GetGroupRecord` method call works analogously but for groups.
+
+The `GetMemberships` method call may be used to inquire about group
+memberships. The `userName` and `groupName` arguments take what the name
+suggests. If one of the two is specified all matching memberships are returned,
+if neither is specified all known memberships of any user and any group are
+returned. The return value is a pair of user name and group name, where the
+user is a member of the group. If both arguments are specified the specified
+membership will be tested for, but no others. Unless both arguments are
+specified the method call needs to be made with `more` set, so that multiple
+replies can be returned (since typically there are multiple members per
+group and also multiple groups a user is member of). As with `GetUserRecord`
+and `GetGroupRecord` the `service` parameter needs to contain the name of the
+service being talked to, in order to allow implementation of multiple services
+within the same IPC socket. In case no matching membership is known
+`NoRecordFound` is returned. The other two errors are also generated in the
+same cases as for `GetUserRecord` and `GetGroupRecord`.
+
+Unlike with `GetUserRecord` and `GetGroupRecord` the lists of memberships
+returned by services are always combined. Thus unlike the other two calls a
+membership lookup query has to wait for the last parallel query to complete
+before the complete list is acquired.
+
+Note that only the `GetMemberships` call is authoritative about memberships of
+users in groups. i.e. it should not be considered sufficient to check the
+`memberOf` field of user records and the `members` field of group records to
+acquire the full list of memberships. The full list can only be determined by
+`GetMemberships`, and as mentioned requires merging of these lists of all local
+services. Result of this is that it can be one service that defines a user A,
+and another service that defines a group B, and a third service that declares
+that A is a member of B.
+
+And that's really all there is to it.
diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md
new file mode 100644
index 0000000000000..b7338e96f6c34
--- /dev/null
+++ b/docs/USER_RECORD.md
@@ -0,0 +1,902 @@
+---
+title: JSON User Records
+---
+
+# JSON User Records
+
+systemd optionally processes user records that go beyond the classic UNIX (or
+glibc NSS) `struct passwd`. Various components of systemd are able to provide
+and consume records in a more advanced JSON-based format. Specifically:
+
+1. [`systemd-homed.service`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html)
+ manages `human` user home directories and embeds these JSON records
+ directly in the home directory images (see [Home
+ Directories](https://systemd.io/HOME_DIRECTORY)) for details.
+
+2. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+ processes these JSON records for users that log in, and applies various
+ settings to the activated session, including environment variables, nice
+ levels and more.
+
+3. [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html)
+ processes these JSON records users that log in, and applies various resource
+ management settings to the per-user slice units it manages. This allows
+ setting global limits on resource consumption by a specific user.
+
+4. [`nss-systemd`](https://www.freedesktop.org/software/systemd/man/nss-systemd.html)
+ is a glibc NSS module that synthesizes classic NSS records from these JSON
+ records, providing full backwards compatibility with the classic UNIX APIs
+ both for look-up and enumeration.
+
+5. The service manager (PID 1) exposes dynamic users (i.e. users synthesized as
+ effect of `DynamicUser=` in service unit files) as these advanced JSON
+ records, making them discoverable to the rest of the system.
+
+6. [`systemd-userdbd.service`](https://www.freedesktop.org/software/systemd/man/systemd-userdbd.service.html)
+ is a small service that can translate UNIX/glibc NSS records to these JSON
+ user records. It also provides a unified [Varlink](https://varlink.org/) API
+ for querying and enumerating records of this type, optionally acquiring them
+ from various other services.
+
+JSON user records may contain various fields that are not available in `struct
+passwd`, and are extensible for other applications. For example, the record may
+contain information about:
+
+1. Additional security credentials (PKCS#11 security token information,
+ biometrical authentication information, SSH public key information)
+
+2. Additional user metadata, such as a picture, email address, location string,
+ preferred language or timezone
+
+3. Resource Management settings (such as CPU/IO weights, memory and tasks
+ limits, classic UNIX resource limits or nice levels)
+
+4. Runtime parameters such as environment variables or the `nodev`, `noexec`,
+ `nosuid` flags to use for the home directory
+
+5. Information about where to mount the home directory from
+
+And various other things. The record is intended to be extensible, for example
+the following extensions are thinkable:
+
+1. Windows network credential information
+
+2. Information about default IMAP, SMTP servers to use for this user
+
+3. Parental control information to enforce on this user
+
+4. Default parameters for backup applications and similar
+
+Similar to JSON User Records there are also [JSON Group
+Records](https://systemd.io/GROUP_RECORD.md) that encapsulate UNIX groups.
+
+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
+[Varlink User/Group Lookup API](https://systemd.io/USER_GROUP_API.md).
+
+## Why JSON?
+
+JSON is nicely extensible and widely used. In particular it's easy to
+synthesize and process is with numerous programming languages. It's
+particularly popular in the web communities, which hopefully should make it
+easy to link user credential data from the web and from local systems more
+closely together.
+
+## General Structure
+
+The JSON user records generated and processed by systemd follow a general
+structure, consisting of seven distinct "sections". Specifically:
+
+1. Various fields are placed at the top-level of user record (the `regular`
+ section). These are generally fields that shall apply unconditionally to the
+ user in all contexts, are portable and not security sensitive.
+
+2. A number of fields are located in the `privileged` section (a sub-object of
+ the user record). Fields contained in this object are security sensitive,
+ i.e. contain information that the user and the administrator should be able
+ to see, but other users should not. In many ways this matches the data
+ stored in `/etc/shadow` in classic Linux user accounts, i.e. includes
+ password hashes and more. Algorithmically, when a user record is passed to
+ an untrusted client, by monopolizing such sensitive records in a single
+ object field we can easily remove it from view.
+
+3. A number of fields are located in objects inside the `perMachine` section
+ (an array field of the user record). Primarily these are resource
+ management-related fields, as those tend to make sense on a specific system
+ only, e.g. limiting a user's memory use to 1G only makes sense on a specific
+ system that has more than 1G of memory. Each object inside the `perMachine`
+ array comes with a `matchMachineId` or `matchHostname` field which indicate
+ which systems to apply the listed settings to. Note that many fields
+ accepted in the `perMachine` section can also be set at the top level (the
+ `regular` section), where they define the fallback if no matching object in
+ `perMachine` is found.
+
+4. Various fields are located in the `binding` section (a sub-sub-object of the
+ user record; an intermediary object is insert which is keyed by the machine
+ ID of the host). Fields included in this section "bind" the object to a
+ specific system. They generally include non-portable information about paths
+ or UID assignments, that are true on a specific system, but not necessarily
+ on others, and which are managed automatically by some user record manager
+ (such as `systemd-homed`). Data in this section is considered part of the
+ user record only in the local context, and is generally not ported to other
+ systems. Due to that it is not included in the reduced user record the
+ cryptographic signature defined in the `signature` section is calculated
+ on. In `systemd-homed` this section is also removed when the user's record
+ is stored in the `~.identity` file in the home directory, so that every
+ system with access to the home directory can manage these `binding` fields
+ individually. Typically, the binding section is persisted to the local disk.
+
+5. Various fields are located in the `status` section (a sub-sub-object of the
+ user record, also with an intermediary object between that is keyed by the
+ machine ID, similar to the way the `binding` section is organized). This
+ section is augmented during runtime only, and never persisted to disk. The
+ idea is that this section contains information about current runtime
+ resource usage (for example: currently used disk space of the user), that
+ changes dynamically but is otherwise immediately associated with the user
+ record and for many purposes should be considered to be part of the user
+ record.
+
+6. The `signature` section contains one or more cryptographic signatures of a
+ reduced version of the user record. This is used to ensure that only user
+ records defined by a specific source are accepted on a system, by validating
+ the signature against the set of locally accepted signature public keys. The
+ signature is calculated from the JSON user record with all sections removed,
+ except for `regular`, `privileged`, `perMachine`. Specifically, `binding`,
+ `status`, `signature` itself and `secret` are removed first and thus not
+ covered by the signature. This section is optional, and is only used when
+ cryptographic validation of user records is required (as it is by
+ `systemd-homed.service` for example).
+
+7. The `secret` section contains secret user credentials, such as password or
+ PIN information. This data is never persisted, and never returned when user
+ records are inquired by a client, privileged or not. This data should only
+ be included in a user record very briefly, for example when certain very
+ specific operations are executed. For example, in tools such as
+ `systemd-homed` this section may be included in user records, when creating
+ a new home directory, as passwords and similar credentials need to be
+ provided to encrypt the home directory with.
+
+Here's a tabular overview of the sections and their properties:
+
+| Section | Included in Signature | Persisted | Security Sensitive | Contains Host-Specific Data |
+|------------|-----------------------|-----------|--------------------|-----------------------------|
+| regular | yes | yes | no | no |
+| privileged | yes | yes | yes | no |
+| perMachine | yes | yes | no | yes |
+| binding | no | yes | no | yes |
+| status | no | no | no | yes |
+| signature | no | yes | no | no |
+| secret | no | no | yes | no |
+
+## Fields in the `regular` section
+
+As mentioned, the `regular` section's fields are placed at the top level
+object. The following fields are currently defined:
+
+`userName` → The UNIX user name for this record. Takes a string with a valid
+UNIX user name. This field is the only mandatory field, all others are
+optional. Corresponds with the `pw_name` field of of `struct passwd` and the
+`sp_namp` field of `struct spwd` (i.e. the shadow user record stored in
+`/etc/shadow`).
+
+`realm` → The "realm" a user is defined in. This concept allows distinguishing
+users with the same name that originate in different organizations or
+installations. This should take a string in DNS domain syntax, but doesn't have
+to refer to an actual DNS domain (though it is recommended to use one for
+this). The idea is that the user `lpoetter` in the `redhat.com` realm might be
+distinct from the same user in the `poettering.hq` realm. User records for the
+same user name that have different realm fields are considered referring to
+different users. When updating a user record it is required that any new
+version has to match in both `userName` and `realm` field. This field is
+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.
+
+`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 record this
+field is initialized from GECOS (i.e. the `pw_gecos` field), and vice versa
+when converting back. That said, unlike GECOS this field is supposed to contain
+only the real name and no other information.
+
+`emailAddress` → The email address of the user, formatted as
+string. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+initializes the `$EMAIL` environment variable from this value for all login
+sessions.
+
+`iconName` → The name of an icon picked by the user, for example for the
+purpose of an avatar. This must be a string, and should follow the semantics
+defined in the [Icon Naming
+Specification](https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html).
+
+`location` → A free-form location string describing the location of the user,
+if that is applicable. It's probably wise to use a location string processable
+by geo-location subsystems, but this is not enforced nor required. Example:
+`Berlin, Germany` or `Basement, Room 3a`.
+
+`disposition` → A string, one of `intrinsic`, `system`, `dynamic`, `regular`,
+`container`, `reserved`. If specified clarifies the disposition of the user,
+i.e. the context it is defined in. For regular, "human" users this should be
+`regular`, for system users (i.e. users that system services run under, and
+similar) this should be `system`. The `intrinsic` disposition should be used
+only for the two users that have special meaning to the OS kernel itself,
+i.e. the `root` and `nobody` users. The `container` string should be used for
+users that are used by an OS container, and hence will show up in `ps` listings
+and such, but are only defined in container context. Finally `reserved` should
+be used for any users outside of these use-cases. Note that this property is
+entirely optional and applications are assumed to be able to derive the
+disposition of a user automatically from a record even in absence of this
+field, based on other fields, for example the numeric UID. By setting this
+field explicitly applications can override this default determination.
+
+`lastChangeUSec` → An unsigned 64bit integer value, referring to a timestamp in µs
+since the epoch 1970, indicating when the user record (specifically, any of the
+`regular`, `privileged`, `perMachine` sections) was last changed. This field is
+used when comparing two records of the same user to identify the newer one, and
+is used for example for automatic updating of user records, where appropriate.
+
+`lastPasswordChangeUSec` → Similar, also an unsigned 64bit integer value,
+indicating the point in time the password (or any authentication token) of the
+user was last changed. This corresponds to the `sp_lstchg` field of `struct
+spwd`, i.e. the matching field in the user shadow database `/etc/shadow`,
+though provides finer resolution.
+
+`shell` → A string, referring to the shell binary to use for terminal logins of
+this user. This corresponds with the `pw_shell` field of `struct passwd`, and
+should contain an absolute file system path. For system users not suitable for
+terminal log-in this field should not be set.
+
+`umask` → The `umask` to set for the user's login sessions. Takes an
+integer. Note that usually the umask is noted in octal, but JSON's integers are
+generally written in decimal, hence that's what this is too. The specified
+value should be in the valid range for umasks, i.e. 0000…0777 (in octal), or
+0…511 (in decimal). This `umask` is automatically set by
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+for all login sessions of the user.
+
+`environment` → An array of strings, each containing an environment variable
+and its value to set for the user's login session, in a format compatible with
+[`putenv()`](http://man7.org/linux/man-pages/man3/putenv.3.html). Any
+environment variable listed here is automatically set by
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+for all login sessions of the user.
+
+`timeZone` → A string indicating a preferred timezone to use for the user. When
+logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialized the `$TZ` environment variable from this
+string. The string hence should be in a format compatible with this environment
+variable, for example: `Europe/Berlin`.
+
+`preferredLanguage` → A string indicating the preferred language/locale for the
+user. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialized the `$LANG` environment variable from this
+string. The string hence should be in a format compatible with this environment
+variable, for example: `de_DE.UTF8`.
+
+`niceLevel` → An integer value in the range -20…19. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the login process' nice level to this value with
+this, which is then inherited by all the user's processes, see
+[`setpriority()`](http://man7.org/linux/man-pages/man2/setpriority.2.html) for
+more information.
+
+`resourceLimits` → An object, where each key refers to a Linux resource limit
+(such as `RLIMIT_NOFILE` and similar). Their values should be an object with
+two keys `cur` and `max` for the soft and hard resource limit. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the login process' resource limits to these
+values, which is then inherited by all the user's processes, see
+[`setrlimit()`](http://man7.org/linux/man-pages/man2/setrlimit.2.html) for more
+information.
+
+`locked` → A boolean value. If true the user account is locked, the user may
+not log in. If this field is missing it should be assumed to be false,
+i.e. logins are permitted. This field corresponds with the `sp_expire` field of
+`struct spwd` (i.e. the `/etc/shadow` data for a user) being set to zero or
+one.
+
+`notBeforeUSec` → An unsigned 64bit integer value, indicating a time in µs since
+the UNIX epoch (1970) before which the record should be considered invalid for
+the purpose of logging in.
+
+`notAfterUSec` → Similar, but indicates the point in time *after* which logins
+shall not be permitted anymore. This corresponds to the `sp_expire` field of
+`struct spwd`, when it is set to a value larger than one, but provides finer
+granularity.
+
+`storage` → A string, one of `classic`, `luks`, `directory`, `subvolume`,
+`fscrypt`, `cifs`. Indicates the storage mechanism for the user's home
+directory. If `classic` the home directory is a plain directory as in classic
+UNIX. When `directory`, the home directory is a regular directory, but the
+`~.identity` file in it contains the user's user record, so that the directory
+is self-contained. Similar, `subvolume` is a `btrfs` subvolume that also
+contains a `~/.identity` use record; `fscrypt` is an `fscrypt`-encrypted
+directory, also containing the `~/.identity` user record; `luks` is a per-user
+LUKS volume that is mounted as home directory, and `cifs` a home directory
+mounted from a Windows File Share. The five latter types are primarily used by
+`systemd-homed` when managing home directories, but may be used if other
+managers are used too. If this is not set `classic` is the implied default.
+
+`diskSize` → An unsigned 64bit integer, indicating the intended home directory
+disk space in bytes to assign to the user. Depending to the selected storage
+type this might be implement differently: for `luks` this is the intended size
+of the file system and LUKS volume, while for the others this likely translates
+to classic file system quota settings.
+
+`diskSizeRelative` → Similar to `diskSize` but takes a relative value, which is
+taken relative to the available disk space on the selected storage medium. This
+unsigned integer value is normalized to 2^32 = 100%.
+
+`skeletonDirectory` → Takes a string with the absolute path to the skeleton
+directory to populate a new home directory from. This is only used when a home
+directory is first created, and defaults to `/etc/skel` if not defined.
+
+`accessMode` → Takes an unsigned integer in the range 0…511 indicating the UNIX
+access mask for the home directory when it is first created.
+
+`tasksMax` → Takes an unsigned 64bit integer indicating the maximum number of
+tasks the user may start in parallel during system runtime. This value is
+enforced on all tasks (i.e. processes and threads) the user starts or that are
+forked off these processes regardless if the change user identity (for example
+by setuid binaries/`su`/`sudo` and
+similar). [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html)
+enforces this by setting the `TasksMax` slice property for the user's slice
+`user-$UID.slice`.
+
+`memoryHigh`/`memoryMax` → These take unsigned 64bit integers indicating upper
+memory limits for all processes of the user (plus all processes forked off them
+that might have changed user identity), in bytes. Enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html),
+similar to `tasksMax`.
+
+`cpuWeight`/`ioWeight` → These take unsigned integers in the range 100…10000
+and configure the CPU and IO scheduling weights for the user's processes as a
+whole. Also enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html),
+similar to `tasksMax`, `memoryHigh` and `memoryMax`.
+
+`mountNoDevices`/`mountNoSUID`/`mountNoExecute` → Three booleans that control
+the `nodev`, `nosuid`, `noexec` mount flags of the user's home
+directories. Note that these booleans are only honored if the home directory
+is managed by a subsystem such as `systemd-homed.service` that automatically
+mounts home directories on login.
+
+`fscryptSalt` → A string containing base64 encoded data. Selects the the salt
+bytes to use for the PBKDF2 key derivation function when using `fscrypt` as
+storage mechanism for the home directory.
+
+`cifsDomain` → A string indicating the Windows File Sharing domain (CIFS) to
+use. This is generally useful, but particularly when `cifs` is used as storage
+mechanism for the user's home directory, see above.
+
+`cifsUserName` → A string indicating the Windows File Sharing user name (CIFS)
+to associate this user record with. This is generally useful, but particularly
+useful when `cifs` is used as storage mechanism for the user's home directory,
+see above.
+
+`cifsService` → A string indicating the Windows File Share service (CIFS) to
+mount as home directory of the user on login.
+
+`imagePath` → A string with an absolute file system path to the file, directory
+or block device to use for storage backing the home directory. If the `luks`
+storage is used this refers to the loopback file or block device node to store
+the LUKS volume on. For `fscrypt`, `directory`, `subvolume` this refers to the
+directory to bind mount as home directory on login. Not defined for `classic`
+or `cifs`.
+
+`homeDirectory` → A string with an absolute file system path to the home
+directory. This is where the image indicated in `imagePath` is mounted to on
+login and thus indicates the application facing home directory while the home
+directory is active, and is what the user's `$HOME` environment variable is set
+to during log-in. It corresponds to the `pw_dir` field of `struct passwd`.
+
+`uid` → An unsigned integer in the range 0…4294967295: the numeric UNIX user ID (UID) to
+use for the user. This corresponds to the `pw_uid` field of `struct passwd`.
+
+`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group
+ID (GID) to use for the user. This corresponds to the `pw_gid` field of
+`struct passwd`.
+
+`memberOf` → An array of strings, each indicating a UNIX group this user shall
+be a member of. The listed strings must be valid group names, but it is not
+required that all groups listed exist in all contexts: any entry for which no
+group exists should be silently ignored.
+
+`fileSystemType` → A string, one of `ext4`, `xfs`, `btrfs` (possibly others) to
+use as file system for the user's home directory. This is primarily relevant
+when the storage mechanism used is `luks` as a file system to use inside the
+LUKS container must be selected.
+
+`partitionUUID` → A string containing a lower-case, text-formatted UUID, referencing
+the GPT partition UUID the home directory is located in. This is primarily
+relevant when the storage mechanism used is `luks`.
+
+`luksUUID` → A string containing a lower-case, text-formatted UUID, referencing
+the LUKS volume UUID the home directory is located in. This is primarily
+relevant when the storage mechanism used is `luks`.
+
+`fileSystemUUID` → A string containing a lower-case, text-formatted UUID,
+referencing the file system UUID the home directory is located in. This is
+primarily relevant when the storage mechanism used is `luks`.
+
+`luksDiscard` → A boolean. If true and `luks` storage is used controls whether
+the loopback block devices, LUKS and the file system on top shall be used in
+`discard` mode, i.e. erased sectors should always be returned to the underlying
+storage. If false and `luks` storage is used turns this behavior off. In
+addition, depending on this setting an `FITRIM` or `fallocate()` operation is
+executed to make sure the image matches the selected option.
+
+`luksCipher` → A string, indicating the cipher to use for the LUKS storage mechanism.
+
+`luksCipherMode` → A string, selecting the cipher mode to use for the LUKS storage mechanism.
+
+`luksVolumeKeySize` → An unsigned integer, indicating the volume key length in
+bytes to use for the LUKS storage mechanism.
+
+`luksPbkdfHashAlgorithm` → A string, selecting the hash algorithm to use for
+the PBKDF operation for the LUKS storage mechanism.
+
+`luksPbkdfType` → A string, indicating the PBKDF type to use for the LUKS storage mechanism.
+
+`luksPbkdfTimeCostUSec` → An unsigned 64bit integer, indicating the intended
+time cost for the PBKDF operation, when the LUKS storage mechanism is used, in
+µs.
+
+`luksPbkdfMemoryCost` → An unsigned 64bit integer, indicating the intended
+memory cost for the PBKDF operation, when LUKS storage is used, in bytes.
+
+`luksPbkdfParallelThreads` → An unsigned 64bit integer, indicating the intended
+required parallel threads for the PBKDF operation, when LUKS storage is used.
+
+`service` → A string declaring the service that defines or manages this user
+record. It is recommended to use reverse domain name notation for this. For
+example, if `systemd-homed` manages a user a string of `io.systemd.Home` is
+used for this.
+
+`rateLimitIntervalUSec` → An unsigned 64bit integer that configures the
+authentication rate limiting enforced on the user account. This specifies a
+timer interval (in µs) within which to count authentication attempts. When the
+counter goes above the value configured n `rateLimitIntervalBurst` log-ins are
+temporarily refused until the interval passes.
+
+`rateLimitIntervalBurst` → An unsigned 64bit integer, closely related to
+`rateLimitIntervalUSec`, that puts a limit on authentication attempts within
+the configured time interval.
+
+`enforcePasswordPolicy` → A boolean. Configures whether to enforce the system's
+password policy when creating the home directory for the user or changing the
+user's password. By default the policy is enforced, but if this field is false
+it is bypassed.
+
+`autoLogin` → A boolean. If true the user record is marked as suitable for
+auto-login. Systems are supposed to automatically log in a user marked this way
+during boot, if there's exactly one user on it defined this way.
+
+`stopDelayUSec` → An unsigned 64bit integer, indicating the time in µs the
+per-user service manager is kept around after the user fully logged out. This
+value is honored by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If
+set to zero the per-user service manager is immediately terminated when the
+user logs out, and longer values optimize high-frequency log-ins as the
+necessary work to set up and tear down a log-in is reduced if the service
+manager stays running.
+
+`killProcesses` → A boolean. If true all processes of the user are
+automatically killed when the user logs out. This is enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If
+false any processes left around when the user logs out are left running.
+
+`passwordChangeMinUSec`/`passwordChangeMaxUSec` → An unsigned 64bit integer,
+encoding how much time has to pass at least/at most between password changes of
+the user. This corresponds with the `sp_min` and `sp_max` fields of `struct
+spwd` (i.e. the `/etc/shadow` entries of the user), but offers finer
+granularity.
+
+`passwordChangeWarnUSec` → An unsigned 64bit integer, encoding how much time to
+warn the user before their password expires, in µs. This corresponds with the
+`sp_warn` field of `struct spwd`.
+
+`passwordChangeInactiveUSec` → An unsigned 64bit integer, encoding how much
+time has to pass after the password expired that the account is
+deactivated. This corresponds with the `sp_inact` field of `struct spwd`.
+
+`passwordChangeNow` → A boolean. If true the user has to change their password
+on next login. This corresponds with the `sp_lstchg` field of `struct spwd`
+being set to zero.
+
+`privileged` → An object, which contains the fields of he `privileged` section
+of the user record, see below.
+
+`perMachine` → An array of objects, which contain the `perMachine` section of
+the user record, and thus fields to apply on specific systems only, see below.
+
+`binding` → An object, keyed by machine IDs formatted as strings, pointing
+to objects that contain the `binding` section of the user record,
+i.e. additional fields that bind the user record to a specific machine, see
+below.
+
+`status` → An object, keyed by machine IDs formatted as strings, pointing to
+objects that contain the `status` section of the user record, i.e. additional
+runtime fields that expose the current status of the user record on a specific
+system, see below.
+
+`signature` → An array of objects, which contain cryptographic signatures of
+the user record, i.e. the fields of the `signature` section of the user record,
+see below.
+
+`secret` → An object, which contains the fields of the `secret` section of the
+user record, see below.
+
+## Fields in the `privileged` section
+
+As mentioned, the `privileged` section is encoded in a sub-object of the user
+record top-level object, in the `privileged` field. Any data included in this
+object shall only be visible to the administrator and the user themselves, and
+be suppressed implicitly when other users get access to a user record. It thus
+takes the role of the `/etc/shadow` records for each user, which has similarly
+restrictive access semantics. The following fields are currently defined:
+
+`passwordHint` → A user-selected password hint in free-form text. This should
+be a string like "What's the name of your first pet?", but is entirely for the
+user to choose.
+
+`hashPassword` → An array of strings, each containing a hashed UNIX password
+string, in the format
+[`crypt(3)`](http://man7.org/linux/man-pages/man3/crypt.3.html) generates. This
+corresponds with `sp_pwdp` field of `struct spwd` (and in a way the `pw_passwd`
+field of `struct passwd`).
+
+`sshAuthorizedKeys` → An array of strings, each listing an SSH public key that
+is authorized to access the account. The strings should follow the same format
+as the lines in the traditional `~./.ssh/authorized_key` file.
+
+## Fields in the `perMachine` section
+
+As mentioned, the `perMachine` section contains settings that shall apply to
+specific systems only. This is primarily interesting for resource management
+properties as they tend to require a per-system focus, however they may be used
+for other purposes too.
+
+The `perMachine` field in the top-level object is an array of objects. When
+processing the user record first the various fields on the top-level object
+should be used. Then this array should be iterated in order, and the various
+settings be applied that match either the indicated machine ID or host
+name. There may be multiple array entries that match a specific system, in
+which case all the object's setting should be applied. If the same option is
+set in the top-level object as in a per-machine object the latter wins and
+entirely undoes the setting in the top-level object (i.e. no merging of
+properties that are arrays themselves is done). If the same option is set in
+multiple per-machine objects the one specified later in the array wins (and
+here too no merging of individual fields is done, the later field always wins
+in full).
+
+The following fields are defined in this section:
+
+`matchMachineId` → An array of strings with each a formatted 128bit ID in
+hex. If any of the specified IDs match the system's local machine ID
+(i.e. matches `/etc/machine-id`) the fields in this object are honored.
+
+`matchHostname` → An array of string with a each a valid hostname. If any of
+the specified hostnames match the system's local hostname, the fields in this
+object are honored. If both `matchHostname` and `matchMachineId` are used
+within the same array entry, the object is honored when either match succeeds,
+i.e. the two match types are combined in OR, not in AND.
+
+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`,
+`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
+`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`,
+`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`,
+`mountNoDevices`, `mountNoSUID`, `mountNoExecute`, `fscryptSalt`, `cifsDomain`,
+`cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`,
+`fileSystemType`, `partitionUUID`, `luksUUID`, `fileSystemUUID`, `luksDiscard`,
+`luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, `luksPbkdfHashAlgorithm`,
+`luksPbkdfType`, `luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`,
+`luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, `rateLimitBurst`,
+`enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, `killProcesses`,
+`passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`,
+`passwordChangeInactiveUSec`, `passwordChangeNow`.
+
+## Fields in the `binding` section
+
+As mentioned, the `binding` section contains additional fields about the user
+record, that bind it to the local system. These fields are generally used by a
+local user manager (such as `systemd-homed.service`) to add in fields that make
+sense in a local context but not necessarily in a global one. For example, a
+user record that contains no `uid` field in the regular section is likely
+extended with one in the `binding` section to assign a local UID if no global
+UID is defined.
+
+All fields in the `binding` section only make sense in a local context and are
+suppressed when the use record is ported between systems. The `binding` section
+is generally persisted on the system but not in the home directories themselves
+and the home directory is supposed to be fully portable and thus not contain
+the information that `binding` is supposed to contain that binds the portable
+record to a specific system.
+
+The `binding` sub-object on the top-level user record object is keyed by the
+machine ID the binding is intended for, which point to an object with the
+fields of the bindings. These fields generally match fields that may also be
+defined in the `regular` and `perMachine` sections, however override
+both. Usually, the `binding` value should not contain settings different from
+those set via `regular` or `perMachine`, however this might happen if some
+settings are not supported locally (think: `fscrypt` is recorded as intended
+storage mechanism in the `regular` section, but the local kernel does not
+support `fscrypt`, hence `directory` was chosen as implicit fallback), or have
+been changed in the `regular` section through updates (e.g. a home directory
+was created with `luks` as storage mechanism but later the user record was
+updated to prefer `subvolume`, which however doesn't change the actual storage
+used already which is pinned in the `binding` section).
+
+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`, `fscryptSalt`, `luksCipher`,
+`luksCipherMode`, `luksVolumeKeySize`.
+
+## Fields in the `status` section
+
+As mentioned, the `status` section contains additional fields about the user
+record that are exclusively acquired during runtime, and that expose runtime
+metrics of the user and similar metadata that shall not be persisted but are
+only acquired "on-the-fly" when requested.
+
+This section is arranged similarly to the `binding` section: the `status`
+sub-object of the top-level user record object is keyed by the machine ID,
+which points to the object with the fields defined here. The following fields
+are defined:
+
+`diskUsage` → An unsigned 64bit integer. The currently used disk space of the
+home directory in bytes. This value might be determined in different ways,
+depending on the selected storage mechanism. For LUKS storage this is the file
+size of the loopback file or block device size. For the
+directory/subvolume/fscrypt storage this is the current disk space used as
+reported by the file system quota subsystem.
+
+`diskFree` → An unsigned 64bit integer, denoting the number of "free" bytes in
+the disk space allotment, i.e. usually the difference between the disk size as
+reported by `diskSize` and the used already as reported in `diskFree`, but
+possibly skewed by metadata sizes, disk compression and similar.
+
+`diskSize` → An unsigned 64bit integer, denoting the disk space currently
+allotted to the user, in bytes. Depending on the storage mechanism this can mean
+different things (see above). In contrast to the top-level field of the same
+(or the one in the `perMachine` section), this field reports the current size
+allotted to the user, not the intended one. The values may differ when user
+records are updated without the home directory being re-sized.
+
+`diskCeiling`/`diskFloor` → Unsigned 64bit integers indicating upper and lower
+bounds when changing the `diskSize` value, in bytes. These values are typically
+derived from the underlying data storage, and indicate in which range the home
+directory may be re-sized in, i.e. in which sensible range the `diskSize` value
+should be kept.
+
+`state` → A string indicating the current state of the home directory. The
+precise set of values exposed here are up to the service managing the home
+directory to define (i.e. are up to the service identified with the `service`
+field below). However, it is recommended to stick to a basic vocabulary here:
+`inactive` for a home directory currently not mounted, `absent` for a home
+directory that cannot be mounted currently because it does not exist on the
+local system, `active` for a home directory that is currently mounted and
+accessible.
+
+`service` → A string identifying the service that manages this user record. For
+example `systemd-homed.service` sets this to `io.systemd.Home` to all user
+records it manages. This is particularly relevant to define clearly the context
+in which `state` lives, see above. Note that this field also exists on the
+top-level object (i.e. in the `regular` section), which it overrides. The
+`regular` field should be used if conceptually the user record can only be
+managed by the specified service, and this `status` field if it can
+conceptually be managed by different managers, but currently is managed by the
+specified one.
+
+`signedLocally` → A boolean. If true indicates that the user record is signed
+by a public key for which the private key is available locally. This indicates
+that indicates that the user record may be modified locally as it can be
+re-signed with the private key. If false indicates that the user record is
+signed by a public key recognized by the local manager but whose private key is
+not available locally. This means the user record cannot be modified locally as
+it couldn't be signed afterwards.
+
+`goodAuthenticationCounter` → An unsigned 64bit integer. This counter is
+increased by one on every successful authentication attempt, i.e. an
+authentication attempt where a security token of some form was presented and it
+was correct.
+
+`badAuthenticationCounter` → An unsigned 64bit integer. This counter is
+increased by one on every unsuccessfully authentication attempt, i.e. an
+authentication attempt where a security token of some form was presented and it
+was incorrect.
+
+`lastGoodAuthenticationUSec` → An unsigned 64bit integer, indicating the time
+of the last successful authentication attempt in µs since the UNIX epoch (1970).
+
+`lastBadAuthenticationUSec` → Similar, but the timestamp of the last
+unsuccessfully authentication attempt.
+
+`rateLimitBeginUSec` → An unsigned 64bit integer: the µs timestamp since the
+UNIX epoch (1970) where the most recent rate limiting interval has been
+started, as configured with `rateLimitIntervalUSec`.
+
+`rateLimitCount` → An unsigned 64bit integer, counting the authentication
+attempts in the current rate limiting interval, see above. If this counter
+grows beyond the value configured in `rateLimitBurst` authentication attempts
+are temporarily refused.
+
+`removable` → A boolean value. If true the manager of this user record
+determined the home directory being on removable media. If false it was
+determined the home directory is in internal built-in media. (This is used by
+`systemd-logind.service` to automatically pick the right default value for
+`stopDelayUSec` if the field is not explicitly specified: for home directories
+on removable media the delay is selected very low to minimize the chance the
+home directory remains in unclean state if the storage device is removed from
+the system by the user).
+
+## Fields in the `signature` section
+
+As mentioned, the `signature` section of the user record may contain one or
+more cryptographic signatures of the user record. Like all others, this section
+is optional, and only used when cryptographic validation of user records shall
+be used. Specifically, all user records managed by `systemd-homed.service` will
+carry such signatures and the service refuses managing user records that come
+without signature or with signatures not recognized by any locally defined
+public key.
+
+The `signature` field in the top-level user record object is an array of
+objects. Each object encapsulates one signature and has two fields: `data` and
+`key` (both are strings). The `data` field contains the actual signature,
+encoded in base64, the `key` field contains a copy of the public key whose
+private key was used to make the signature, in PEM format. Currently only
+signatures with Ed25519 keys are defined.
+
+Before signing the user record should be brought into "normalized" form,
+i.e. the keys in all objects should be sorted alphabetically. All redundant
+white-space and newlines should be removed and the JSON text then signed.
+
+The signatures only cover the `regular`, `perMachine` and `privileged` sections
+of the user records, all other sections (include `signature` itself), are
+removed before the signature is calculated.
+
+## Fields in the `secret` section
+
+As mentioned, the `secret` section of the user record should never be persisted
+nor transferred across machines. It is only defined in short-lived operations,
+for example when a user record is first created or registered, as the secret
+key data needs to be available to derive encryption keys from and similar.
+
+The `secret` field of the top-level user record contains the following fields:
+
+`password` → an array of strings, each containing a plain text password.
+
+## Mapping to `struct passwd` and `struct spwd`
+
+When mapping classic UNIX user records (i.e. `struct passwd` and `struct spwd`)
+to JSON user records the following mappings should be applied:
+
+| Structure | Field | Section | Field | Condition |
+|-----------------|-------------|--------------|------------------------------|----------------------------|
+| `struct passwd` | `pw_name` | `regular` | `userName` | |
+| `struct passwd` | `pw_passwd` | `privileged` | `password` | (See notes below) |
+| `struct passwd` | `pw_uid` | `regular` | `uid` | |
+| `struct passwd` | `pw_gid` | `regular` | `gid` | |
+| `struct passwd` | `pw_gecos` | `regular` | `realName` | |
+| `struct passwd` | `pw_dir` | `regular` | `homeDirectory` | |
+| `struct passwd` | `pw_shell` | `regular` | `shell` | |
+| `struct spwd` | `sp_namp` | `regular` | `userName` | |
+| `struct spwd` | `sp_pwdp` | `privileged` | `password` | (See notes below) |
+| `struct spwd` | `sp_lstchg` | `regular` | `lastPasswordChangeUSec` | (if `sp_lstchg` > 0) |
+| `struct spwd` | `sp_lstchg` | `regular` | `passwordChangeNow` | (if `sp_lstchg` == 0) |
+| `struct spwd` | `sp_min` | `regular` | `passwordChangeMinUSec` | |
+| `struct spwd` | `sp_max` | `regular` | `passwordChangeMaxUSec` | |
+| `struct spwd` | `sp_warn` | `regular` | `passwordChangeWarnUSec` | |
+| `struct spwd` | `sp_inact` | `regular` | `passwordChangeInactiveUSec` | |
+| `struct spwd` | `sp_expire` | `regular` | `locked` | (if `sp_expire` in [0, 1]) |
+| `struct spwd` | `sp_expire` | `regular` | `notAfterUSec` | (if `sp_expire` > 1) |
+
+At this time almost all Linux machines employ shadow passwords, thus the
+`pw_passwd` field in `struct passwd` is set to `"x"`, and the actual password
+is stored in the shadow entry `struct spwd`'s field `sp_pwdp`.
+
+## Extending These Records
+
+User records following this specifications are supposed to be extendable for
+various applications. In general, subsystems are free to introduce their own
+keys, as long as:
+
+* Care should be taken to place the keys in the right section, i.e. the most
+ appropriate for the data field.
+
+* Care should be taken to avoid namespace clashes. Please prefix your fields
+ 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 fields, please consider submitting them upstream for inclusion in
+ this specification. If they are reasonably universally useful, it would be
+ best to list them here.
+
+## Examples
+
+The shortest valid user record looks like this:
+
+```json
+{
+ "userName" : "u"
+}
+```
+
+A reasonable user record for a system user might look like this:
+
+```json
+{
+ "userName" : "httpd",
+ "uid" : 473,
+ "gid" : 473,
+ "disposition" : "system",
+ "locked" : true
+}
+```
+
+A fully featured user record associated with a home directory managed by
+`systemd-homed.service` might look like this:
+
+```json
+{
+ "autoLogin" : true,
+ "binding" : {
+ "15e19cf24e004b949ddaac60c74aa165" : {
+ "fileSystemType" : "ext4",
+ "fileSystemUUID" : "758e88c8-5851-4a2a-b88f-e7474279c111",
+ "gid" : 60232,
+ "homeDirectory" : "/home/grobie",
+ "imagePath" : "/home/grobie.home",
+ "luksCipher" : "aes",
+ "luksCipherMode" : "xts-plain64",
+ "luksUUID" : "e63581ba-79fb-4226-b9de-1888393f7573",
+ "luksVolumeKeySize" : 32,
+ "partitionUUID" : "41f9ce04-c827-4b74-a981-c669f93eb4dc",
+ "storage" : "luks",
+ "uid" : 60232
+ }
+ },
+ "disposition" : "regular",
+ "enforcePasswordPolicy" : false,
+ "lastChangeUSec" : 1565950024279735,
+ "memberOf" : [
+ "wheel"
+ ],
+ "privileged" : {
+ "hashedPassword" : [
+ "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/"
+ ]
+ },
+ "signature" : [
+ {
+ "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==",
+ "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n"
+ }
+ ],
+ "userName" : "grobie",
+ "status" : {
+ "15e19cf24e004b949ddaac60c74aa165" : {
+ "goodAuthenticationCounter" : 16,
+ "lastGoodAuthenticationUSec" : 1566309343044322,
+ "rateLimitBeginUSec" : 1566309342340723,
+ "rateLimitCount" : 1,
+ "state" : "inactive",
+ "service" : "io.systemd.Home",
+ "diskSize" : 161118667776,
+ "diskCeiling" : 190371729408,
+ "diskFloor" : 5242880,
+ "signedLocally" : true
+ }
+ }
+}
+```
diff --git a/factory/etc/nsswitch.conf b/factory/etc/nsswitch.conf
index 5470993e34b2a..e7365cd142650 100644
--- a/factory/etc/nsswitch.conf
+++ b/factory/etc/nsswitch.conf
@@ -1,7 +1,7 @@
# This file is part of systemd.
passwd: compat mymachines systemd
-group: compat mymachines systemd
+group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] systemd
shadow: compat
hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname
diff --git a/factory/etc/pam.d/system-auth b/factory/etc/pam.d/system-auth
index 747efc1fbdb69..02544808e6246 100644
--- a/factory/etc/pam.d/system-auth
+++ b/factory/etc/pam.d/system-auth
@@ -4,16 +4,19 @@
# unmodified you are not building systems safely and securely.
auth sufficient pam_unix.so nullok try_first_pass
+auth sufficient pam_systemd_home.so
auth required pam_deny.so
account required pam_nologin.so
account sufficient pam_unix.so
-account required pam_permit.so
+account sufficient pam_systemd_home.so
password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok
+password sufficient pam_systemd_home.so
password required pam_deny.so
-session optional pam_keyinit.so revoke
-session optional pam_loginuid.so
+-session optional pam_systemd_home.so
-session optional pam_systemd.so
-session sufficient pam_unix.so
+session required pam_unix.so
diff --git a/man/homectl.xml b/man/homectl.xml
new file mode 100644
index 0000000000000..79a8e3fdd68d8
--- /dev/null
+++ b/man/homectl.xml
@@ -0,0 +1,742 @@
+
+
+
+
+
+
+
+ homectl
+ systemd
+
+
+
+ homectl
+ 1
+
+
+
+ homectl
+ Create, remove, change or inspect home directories
+
+
+
+
+ homectl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+
+ Description
+
+ homectl may be used to create, remove, change or inspect a user's home
+ directory. It's primarily a command interfacing with
+ systemd-homed.service8
+ which manages home directories of users.
+
+ Home directories managed by systemd-homed.service are self-contained, and thus
+ include the user's full metadata record in the home's data storage itself, making them easily migratable
+ between machines. In particular a home directory in itself describes a matching user record, and every
+ user record managed by systemd-homed.service also implies existance and
+ encapsulation of a home directory. The user account and home directory hence become the same concept. The
+ following backing storage mechanisms are supported:
+
+
+ Individual LUKS2 encrypted loopback files for each user, located in
+ /home/*.home. At login the file systems contained in these files are mounted,
+ after the LUKS2 encrypted volume is attached. The user's password is identical to the encryption
+ passphrase of the LUKS2 volume. Access to data without preceeding user authentication is thus not
+ possible, not even for the systems administrator. This storage mechanism provides the strongest data
+ security and is thus recommended.
+
+ Similar, but the LUKS2 encrypted file system is located on regular block device, such
+ as an USB storage stick. In this mode home directories and all data they include are nicely migratable
+ between machines, simply by plugging the USB stick into different systems at different
+ times.
+
+ An encrypted directory using fscrypt on file systems that support it
+ (at the moment this is primarily ext4), located in
+ /home/*.homedir. This mechanism also provides encryption, but substantially
+ weaker than the two mechanisms described above, as most file system metadata is unprotected. Moreover
+ it currently does not support changing user passwords once the home directory has been
+ created.
+
+ A btrfs subvolume for each user, also located in
+ /home/*.homedir. This provides no encryption, but good quota
+ support.
+
+ A regular directory for each user, also located in
+ /home/*.homedir. This provides no encryption, but is a suitable fallback
+ available on all machines, even where LUKS2, fscrypt or btrfs
+ support is not available.
+
+ An individual Windows file share (CIFS) for each user.
+
+
+ Note that systemd-homed.service and homectl will not manage
+ "classic" UNIX user accounts as created with useradd8 or
+ similar tools. In particular, this functionality is not suitable for managing system users (i.e. users
+ with a UID below 1000) but is exclusive to regular ("human") users.
+
+ Note that users/home directories managed via systemd-homed.service do not show
+ up in /etc/passwd and similar files, they are synthesized via glibc NSS during
+ runtime. They are thus resolvable and may be enumerated via the getent1
+ tool.
+
+ This tool interfaces directly with systemd-homed.service, and may execute
+ specific commands on the home directories it manages. Since every home directory managed that way also
+ defines a JSON user and group record these home directories may also be inspected and enumerated via
+ userdbctl1.
+
+
+
+ Options
+
+ The following general options are understood (further options that control the various properties
+ of user records managed by systemd-homed.service are documented further
+ down):
+
+
+
+
+ FILE
+
+ Read the user's JSON record from the specified file. If passed as
+ - reads the user record from standard input. The supplied JSON object must follow
+ the structure documented on JSON User
+ Records. This option may be used in conjunction with the create and
+ update commands (see below), where it allows configuring the user record in JSON
+ as-is, instead of setting the individual user record properties (see below).
+
+
+
+ FORMAT
+
+
+ Controls whether to output the user record in JSON format, if the
+ inspect command (see below) is used. Takes one of pretty,
+ short or off. If pretty human-friendly
+ whitespace and newlines are inserted in the output to make the JSON data more readable. If
+ short all superfluous whitespace is suppressed. If off (the
+ default) the user information is not shown in JSON format but in a friendly human readable formatting
+ instead. The option picks pretty when run interactively and
+ short otherwise.
+
+
+
+ FORMAT
+
+
+
+ When used with the inspect verb in JSON mode (see above) may be
+ used to suppress certain aspects of the JSON user record on output. Specifically, if
+ stripped format is used the binding and runtime fields of the record are
+ removed. If minimal format is used the cryptographic signature is removed too. If
+ full format is used the full JSON record is shown (this is the default). This
+ option is useful for copying an existing user record to a different system in order to create a
+ similar user there with the same settings. Specifically: homectl inspect -EE | ssh
+ root@othersystem homectl create -i- may be used as simple command line for replicating a
+ user on another host. is equivalent to ,
+ to . Note that when replicating user
+ accounts user records acquired in stripped mode will retain the original
+ cryptographic signatures and thus may only modified when the private key to update them is available
+ on the destination machine. When replicating users in minimal mode the signature
+ is remove during the replication and thus it is implicitly signed with the key of the destination
+ machine and thus may be updated there without any private key replication.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User Record Properties
+
+ The following options control various properties of the user records/home directories that
+ systemd-homed.service manages. These switches may be used in conjunction with the
+ create and update commands for configuring various aspects of the
+ home directory and the user account:
+
+
+
+
+ NAME
+ NAME
+
+ The real name for the user. This corresponds with the GECOS field on classic UNIX NSS
+ records.
+
+
+
+ REALM
+
+ The realm for the user. The realm associates a user with a specific organization or
+ installation, and allows distuingishing users of the same name defined in different contexts. The realm
+ can be any string that also qualifies as valid DNS domain name, and it is recommended to use the
+ organization's or installation's domain name for this purpose, but this is not enforced nor
+ required. On each system only a single user of the same name may exist, and if a user with the same
+ name and realm is seen it is assumed to refer to the same user while a user with the same name but
+ different realm is considered a different user. Assigning a realm to a user is
+ optional.
+
+
+
+ EMAIL
+
+ Takes an electronic mail address to associate with the user. On log-in the
+ $EMAIL environment variable is initialized from this value.
+
+
+
+ TEXT
+
+ Takes location specification for this user. This is free-form text, which might or
+ might not be usable by geo-location applications. Example: or
+
+
+
+ TEXT
+
+ Takes a password hint to store alongside the user record. This string is stored
+ accessible only to privileged users and the user itself and may not be queried by other users.
+ Example:
+
+
+
+ ICON
+
+ Takes an icon name to associate with the user, following the scheme defined by the Icon Naming
+ Specification.
+
+
+
+ PATH
+ PATH
+
+ Takes a path to use as home directory for the user. Note that this is the directory
+ the user's home directory is mounted to while the user is logged in. This is not where the user's
+ data is actually stored, see for that. If not specified defaults to
+ /home/$USER.
+
+
+
+ UID
+
+ Takes a preferred numeric UNIX UID to use for this user. Note that
+ systemd-homed may assign a user a different UID if needed if the UID is already in
+ use. The specified UID must be outside of the system user range. It is recommended to use the
+ 60001…60513 UID range for this purpose. If not specified the UID is automatically picked. When
+ logging in and the home directory is found to be owned by a UID not matching the user's assigned one
+ the home directory and all files and directories inside it will have their ownership
+ changed automatically before login completes.
+
+ Note that users managed by systemd-homed always have a matching group
+ associated with the same name as well as a GID matching the UID of the user. Thus, configuring the
+ GID separately is not permitted.
+
+
+
+ GROUP
+ GROUP
+
+ Takes a comma-separated list of auxiliary UNIX groups this user shall belong
+ to. Example: to provide the user with administrator
+ privileges. Note that systemd-homed does not manage any groups besides a group
+ matching the user in name and numeric UID/GID. Thus any groups listed here must be registered
+ independently, for example with
+ groupadd8. If
+ non-existant groups are listed they are ignored. This option may be used more than once in case all
+ specified group lists are combined.
+
+
+
+ PATH
+
+ Takes a file system path to a directory. Specifies the skeleton directory to
+ initialize the home directory with. All files and directories in the specified are copied into any
+ newly create home directory. If not specified defaults to
+ /etc/skel/.
+
+
+
+ SHELL
+
+ Takes a file system path. Specifies the shell binary to execute on terminal
+ logins. If not specified defaults to /bin/bash.
+
+
+
+ VARIABLE=VALUE
+
+ Takes an environment variable assignment to set for all user processes. Note that a
+ number of other settings also result in environment variables to be set for the user, including
+ , and . May be used
+ multiple times to set multiple environment variables.
+
+
+
+ TIMEZONE
+
+ Takes a timezone specification as string that sets the timezone for the specified
+ user. When the user logs in the $TZ environment variable is initialized from this
+ setting. Example: .
+
+
+
+ LANG
+
+ Takes a specifier indicating the preferred language of the user. The
+ $LANG environment variable is initialized from this value on login, and thus a
+ value suitable for this environment variable is accepted here, for example
+
+
+
+
+ KEYS
+ Either takes a SSH authorized key line to associate with the user record or a
+ @ character followed by a path to a file to read one or more such lines from. SSH
+ keys configured this way are made available to SSH to permit access to this home directory and user
+ record. This option may be used more than once to configure multiple SSH keys.
+
+
+
+ BOOLEAN
+
+ Takes a boolean argument. Specifies whether this user account shall be locked. If
+ true logins into this account are prohibited, if false (the default) they are permitted (of course,
+ only if authorization otherwise succeeds).
+
+
+
+ TIMESTAMP
+ TIMESTAMP
+
+ These options take a timestamp string, in the format documented in
+ systemd.time7 and
+ configures points in time before and after logins into this account are not
+ permitted.
+
+
+
+ SECS
+ NUMBER
+
+ Configures a rate limit on authentication attempts for this user. If the user
+ attempts to authenticate more often than the specified number, on a specific system, within the
+ specified time interval authentication is refused until the time interval passes. Defaults to 10
+ times per 1min.
+
+
+
+ BOOL
+
+
+ Takes a boolean argument. Configures whether to enforce the system's password policy
+ for this user, regarding quality and strength of selected passwords. Defaults to
+ on. is short for
+ .
+
+
+
+ BYTES
+ Either takes a size in bytes as argument (possibly using the usual K, M, G, …
+ suffixes for 1024 base values), or a percentage value and configures the disk space to assign to the
+ user. If a percentage value is specified (i.e. the argument suffixed with %) it is
+ taken relative to the available disk space of the backing file system. If the LUKS2 backend is used
+ this configures the size of the loopback file and file system contained therein. For the other
+ storage backends configures disk quota using the filesystem's native quota logic, if available. If
+ not specified, defaults to 85% of the available disk space for the LUKS2 backend and to no quota for
+ the others.
+
+
+
+ MODE
+
+ Takes a UNIX file access mode written in octal. Configures the access mode of the
+ home directory itself. Note that this is only used when the directory is first created, and the user
+ may change this any time afterwards. Example:
+
+
+
+
+ MASK
+
+ Takes the access mode mask (in octal syntax) to apply to newly created files and
+ directories of the user ("umask"). If set this controls the initial umask set for all login sessions of
+ the user, possibly overriding the system's defaults.
+
+
+
+ NICE
+
+ Takes the numeric scheduling priority ("nice level") to apply to the processes of the user at login
+ time. Takes a numeric value in the range -20 (highest priority) to 19 (lowest priority).
+
+
+
+ LIMIT=VALUE:VALUE
+
+ Allows configuration of resource limits for processes of this user, see getrlimit2
+ for details. Takes a resource limit name (e.g. LIMIT_NOFILE) followed by an equal
+ sign, followed by a numeric limit. Optionally, separated by colon a second numeric limit may be
+ specified. If two are specified this refers to the soft and hard limits, respectively. If only one
+ limit is specified the setting sets both limits in one.
+
+
+
+ TASKS
+
+ Takes a non-zero unsigned integer as argument. Configures the maximum numer of tasks
+ (i.e. processes and threads) the user may have at any given time. This limit applies to all tasks
+ forked off the user's sessions, even if they change user identity via su1 or a
+ similar tool. Use to place a limit on the tasks actually
+ running under the UID of the user, thus excluding any child processes that might have changed user
+ identity. This controls the TasksMax= settting of the per-user systemd slice unit
+ user-$UID.slice. See
+ systemd.resource-control5
+ for further details.
+
+
+
+ BYTES
+ BYTES
+
+ Set a limit on the memory a user may take up on a system at any given time in bytes
+ (the usual K, M, G, … suffixes are supported, to the base of 1024). This includes all memory used by
+ the user itself and all processes they forked off that changed user credentials. This controls the
+ MemoryHigh= and MemoryMax= settings of the per-user systemd
+ slice unit user-$UID.slice. See
+ systemd.resource-control5
+ for further details.
+
+
+
+ WEIGHT
+ WEIGHT
+
+ Set a CPU and IO scheduling weights of the processes of the user, including those of
+ processes forked off by the user that changed user credentials. Takes a numeric value in the range
+ 1…10000. This controls the CPUWeight= and IOWeight= settings of
+ the per-user systemd slice unit user-$UID.slice. See
+ systemd.resource-control5
+ for further details.
+
+
+
+ STORAGE
+
+ Selects the storage mechanism to use for this home directory. Takes one of
+ luks, fscrypt, directory,
+ subvolume, cifs. For details about these mechanisms, see
+ above. If a new home directory is created and the storage type is not specifically specified defaults
+ to luks if supported, subvolume as first fallback if supported,
+ and directory if not.
+
+
+
+ PATH
+
+ Takes a file system path. Configures where to place the user's home directory. When
+ LUKS2 storage is used refers to the path to the loopback file, otherwise to the path to the home
+ directory. When unspecified defaults to /home/$USER.home when LUKS storage is
+ used and /home/$USER.homedir for the other storage mechanisms. Not defined for
+ the cifs storage mechanism. To use LUKS2 storage on a regular block device (for
+ example a USB stick) pass the path to the block device here.
+
+
+
+ TYPE
+
+ When LUKS2 storage is used configures the file system type to use inside the home
+ directory LUKS2 container. One of ext4, xfs,
+ btrfs. If not specified defaults to ext4. Note that
+ xfs is not recommended as its support for file system resizing is too
+ limited.
+
+
+
+ BOOL
+
+ When LUKS2 storage is used configures whether to enable the
+ discard feature of the file system. If enabled the file system on top of the LUKS2
+ volume will report empty block information to LUKS2 and the loopback file below, ensuring that empty
+ space in the home directory is returned to the backing file system below the LUKS2 volume, resulting
+ in a "sparse" loopback file. This option mostly defaults to off, since this permits over-committing
+ home directories which results in I/O errors if the underlying file system runs full while the upper
+ file system wants to allocate a block. Such I/O errors are generally not handled well by file systems
+ nor applications. When LUKS2 storage is used on top of regular block devices (instead of on top a
+ loopback file) the discard logic defaults to on.
+
+
+
+ CIPHER
+ MODE
+ BITS
+ TYPE
+ ALGORITHM
+ SECONDS
+ BYTES
+ THREADS
+
+ Configures various cryptographic parameters for the LUKS2 storage mechanism. See
+ cryptsetup8
+ for details on the specific attributes.
+
+
+
+ BOOL
+ BOOL
+ BOOL
+
+ Configures the nosuid, nodev and
+ noexec mount options for the home directories. By default nodev
+ and nosuid are on, while noexec is off. For details about these
+ mount options see mount8.
+
+
+
+ DOMAIN
+ USER
+ SERVICE
+
+ Configures the Windows File Sharing (CIFS) domain and user to associate with the home
+ directory/user account, as well as the file share ("service") to mount as directory. The latter is used when
+ cifs storage is selected.
+
+
+
+ SECS
+
+ Configures the time the per-user service manager shall continue to run after the all
+ sessions of the user ended. The default is configured in
+ logind.conf5 (for
+ home directories of LUKS2 storage located on removable media this defaults to 0 though). A longer
+ time makes sure quick, repetitive logins are more efficient as the user's service manager doesn't
+ have to be started every time.
+
+
+
+ BOOL
+
+ Configures whether to kill all processes of the user on logout. The default is
+ configured in
+ logind.conf5.
+
+
+
+ BOOL
+
+ Takes a boolean argument. Configures whether the graphical UI of the system should
+ automatically log this user in if possible. Defaults to off. If less or more than one user is marked
+ this way automatic login is disabled.
+
+
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+
+ list
+
+ List all home directories (along with brief details) currently managed by
+ systemd-homed.service. This command is also executed if none is specified on the
+ command line.
+
+
+
+ activate USER [USER…]
+
+ Activate one or more home directories. The home directories of each listed user will
+ be activated and made available under their mount points (typically in
+ /home/$USER). Note that any home activated this way stays active indefinitely,
+ until it is explicitly deactivated again (with deactivate, see below), or the user
+ logs in and out again and it thus is deactivated due to the automatic deactivation-on-logout
+ logic.
+
+ Activation of a home directory involves various operations that depend on the selected storage
+ mechanism. If the LUKS2 mechanism is used, this generally involves: setting up a loopback device,
+ validating and activating the LUKS2 volume, checking the file system, mounting the file system, and
+ potentiatlly changing the ownership of all included files to the correct UID/GID.
+
+
+
+ deactivate USER [USER…]
+
+ Deactivate one or more home directories. This undoes the effect of
+ activate.
+
+
+
+ inspect USER [USER…]
+
+ Show various details about the specified home directories. This shows various
+ information about the home directory and its user account, including runtime data such as current
+ state, disk use and similar. Combine with to show the detailed JSON user
+ record instead, possibly combined with to suppress certain aspects
+ of the output.
+
+
+
+ authenticate USER [USER…]
+
+ Validate authentication credentials of a home directory. This queries the caller for
+ a password (or similar) and checks that it correctly unlocks the home directory.
+
+
+
+ create USER
+ create PATH USER
+
+ Create a new home directory/user account of the specified name. Use the various
+ user record property options (as documented above) to control various aspects of the home directory
+ and its user accounts.
+
+
+
+ remove USER
+
+ Remove a home directory/user account.
+
+
+
+ update USER
+ update PATH USER
+
+ Update a home directory/user account. Use the various user record property options
+ (as documented above) to make changes to the account, or alternatively provide a full, updated JSON
+ user record via the option.
+
+ Note that changes to user records not signed by a cryptographic private key available locally
+ are not permitted, unless is used with a user record that is already
+ correctly signed by a recognized private key.
+
+
+
+ passwd USER
+
+ Change the password of the specified home direcory/user account.
+
+
+
+ resize USER BYTES
+
+ Change the disk space assigned to the specified home directory. If the LUKS2 storage
+ mechanism is used this will automatically resize the loopback file and the file system contained
+ within. Note that if ext4 is used inside of the LUKS2 volume the home directory
+ while activated may only be grown, and for being shrunk needs to be deactivated first (i.e. the user
+ has to log out). If xfs is used inside of the LUKS2 volume the home directory may
+ not be shrunk whatsoever. On all three of ext4, xfs and
+ btrfs the home directory may be grown while the user is logged in, and on the
+ latter also shrunk while the user is logged in. If the subvolume,
+ directory, fscrypt storage mechanisms are used, resizing will
+ change file system quota.
+
+
+
+ lock USER
+
+ Temporarily suspend access to the user's home directory and remove any associated
+ cryptographic keys from memory. Any attempts to access the user's home directory will stall until the
+ home directory is unlocked again (while re-authenticating). This functionality is primarily intended
+ to be used during system suspend to make sure the user's data cannot be accessed until the user
+ re-authenticates on resume. This operation is only defined for home directories that use the LUKS2
+ storage mechanism.
+
+
+
+ unlock USER
+
+ Resume access to the user's home directory again, undoing the effect of
+ lock above. This requires authentication of the user, as the cryptographic keys
+ required for access to the home directory need to be reacquired.
+
+
+
+ lock-all
+
+ Execute the lock command on all suitable home directories at
+ once. This operation is generally execute on system suspend, to ensure all active user's
+ cryptographic keys for accessing their home directories are removed from memory.
+
+
+
+ with USER COMMAND…
+
+ Activate the specified user's home directory, run the specified command (under the
+ caller's identity, not the specified user's) and deactivate the home directory afterwards again
+ (unless the user is logged in otherwise). This command is useful for running privileged backup
+ scripts and such, but requires authentication with the user's credentials in order to be able to
+ unlock the user's home directory.
+
+
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ Examples
+
+
+ Create a user waldo in the administrator group wheel, and
+ assign 500 MiB disk space to them.
+
+ homectl create waldo --real-name="Waldo McWaldo" -G wheel --disk-size=500M
+
+
+
+ Create a user wally on a USB stick, and assign a maximum of 500 concurrent
+ tasks to them.
+
+ homectl create wally --real-name="Wally McWally" --image-path=/dev/disk/by-id/usb-SanDisk_Ultra_Fit_476fff954b2b5c44-0:0 --tasks-max=500
+
+
+
+ Change nice level of user odlaw to +5 and make sure the environment variable
+ $SOME is set to the string THING for them on login.
+
+ homectl update odlaw --nice=5 --setenv=SOME=THING
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd-homed.service8,
+ userdbctl1,
+ useradd8,
+ cryptsetup8
+
+
+
+
diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml
new file mode 100644
index 0000000000000..f0a58bda15cfb
--- /dev/null
+++ b/man/pam_systemd_home.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+ pam_systemd_home
+ systemd
+
+
+
+ pam_systemd_home
+ 8
+
+
+
+ pam_systemd_home
+ Automatically mount home directories managed by systemd-homed.service on logout, and unmount them on logout
+
+
+
+ pam_systemd_home.so
+
+
+
+ Description
+
+ pam_systemd_home ensures that home directories managed by
+ systemd-homed.service8
+ are automatically activated (mounted) on user login, and are deactivated (unmounted) when the last
+ session of the user ends.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+ suspend=
+
+ Takes a boolean argument. If true, the home directory of the user will be suspended
+ automatically during system suspend; if false it will remain active. Automatic suspending of the home
+ directory improves security substantially as secret key material is automatically removed from memory
+ before the system is put into sleep and must be re-acquired (by user re-authentication) when coming
+ back from suspend. It is recommended to set this parameter for all PAM applications that have support
+ for automatically re-authenticating via PAM on system resume. If this behaviour is enabled without
+ the PAM application supporting this, access to the home directory will sooner or later stall and will
+ resume only after the user re-authenticated via any PAM session. If multiple sessions of the same
+ user are open in parallel the user's home directory will be left unsuspended on system suspend as
+ soon as at least one of the sessions does not set this parameter. Defaults to off.
+
+
+
+ debug=
+
+ Takes an optional boolean argument. If yes or without the argument, the module will log
+ debugging information as it operates.
+
+
+
+
+
+ Module Types Provided
+
+ The module provides all four management operations: , ,
+ , .
+
+
+
+ Environment
+
+ The following environment variables are initialized by the module and available to the processes of the
+ user's session:
+
+
+
+ $SYSTEMD_HOME=1
+
+ Indicates that the user's home directory is managed by systemd-homed.service.
+
+
+
+
+
+
+ Example
+
+ #%PAM-1.0
+auth sufficient pam_unix.so nullok try_first_pass
+auth sufficient pam_systemd_home.so
+auth required pam_deny.so
+
+account required pam_nologin.so
+account sufficient pam_unix.so
+account sufficient pam_systemd_home.so
+
+password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok
+password sufficient pam_systemd_home.so
+password required pam_deny.so
+
+-session optional pam_keyinit.so revoke
+-session optional pam_loginuid.so
+-session optional pam_systemd_home.so
+-session optional pam_systemd.so
+session required pam_unix.so
+
+
+
+ See Also
+
+ systemd1,
+ systemd-homed.service8,
+ homed.conf5,
+ homectl1,
+ pam.conf5,
+ pam.d5,
+ pam8
+
+
+
+
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 3b63311d7b52b..6efe2b6045c5f 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -17,6 +17,7 @@ manpages = [
['environment.d', '5', [], 'ENABLE_ENVIRONMENT_D'],
['file-hierarchy', '7', [], ''],
['halt', '8', ['poweroff', 'reboot'], ''],
+ ['homectl', '1', [], 'ENABLE_HOMED'],
['hostname', '5', [], ''],
['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'],
['hwdb', '7', [], 'ENABLE_HWDB'],
@@ -45,6 +46,7 @@ manpages = [
['nss-systemd', '8', ['libnss_systemd.so.2'], 'ENABLE_NSS_SYSTEMD'],
['os-release', '5', [], ''],
['pam_systemd', '8', [], 'HAVE_PAM'],
+ ['pam_systemd_home', '8', [], 'HAVE_PAM'],
['portablectl', '1', [], 'ENABLE_PORTABLED'],
['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'],
['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'],
@@ -271,6 +273,7 @@ manpages = [
['sd_bus_message_read_array', '3', [], ''],
['sd_bus_message_read_basic', '3', [], ''],
['sd_bus_message_rewind', '3', [], ''],
+ ['sd_bus_message_sensitive', '3', [], ''],
['sd_bus_message_set_destination',
'3',
['sd_bus_message_get_destination',
@@ -697,6 +700,7 @@ manpages = [
'8',
['systemd-hibernate-resume'],
'ENABLE_HIBERNATE'],
+ ['systemd-homed.service', '8', ['systemd-homed'], 'ENABLE_HOMED'],
['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'],
['systemd-hwdb', '8', [], 'ENABLE_HWDB'],
['systemd-id128', '1', [], ''],
@@ -811,6 +815,7 @@ manpages = [
['systemd-update-utmp', 'systemd-update-utmp-runlevel.service'],
'ENABLE_UTMP'],
['systemd-user-sessions.service', '8', ['systemd-user-sessions'], 'HAVE_PAM'],
+ ['systemd-userdbd.service', '8', ['systemd-userdbd'], 'ENABLE_USERDB'],
['systemd-vconsole-setup.service',
'8',
['systemd-vconsole-setup'],
@@ -942,6 +947,7 @@ manpages = [
['udev_new', '3', ['udev_ref', 'udev_unref'], ''],
['udevadm', '8', [], ''],
['user@.service', '5', ['user-runtime-dir@.service'], ''],
+ ['userdbctl', '1', [], 'ENABLE_USERDB'],
['vconsole.conf', '5', [], 'ENABLE_VCONSOLE']
]
# Really, do not edit.
diff --git a/man/sd_bus_message_sensitive.xml b/man/sd_bus_message_sensitive.xml
new file mode 100644
index 0000000000000..a4a732cfd152c
--- /dev/null
+++ b/man/sd_bus_message_sensitive.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+ sd_bus_message_sensitive
+ systemd
+
+
+
+ sd_bus_message_sensitive
+ 3
+
+
+
+ sd_bus_message_sensitive
+
+ Mark a message object as containing sensitive data
+
+
+
+
+ #include <systemd/sd-bus.h>
+
+
+ int sd_bus_message_sensitive
+ sd_bus_message *message
+
+
+
+
+
+ Description
+
+ sd_bus_message_sensitive() marks an allocated bus message as containing
+ sensitive data. This ensures that the message data is carefully removed from memory (specifically,
+ overwritten with zero bytes) when released. It is recommended to mark all incoming and outgoing messages
+ like this that contain security credentials and similar data that should be dealt with carefully. Note
+ that it is not possible to unmark messages like this, it's a one way operation. If a message is already
+ marked sensitive and then marked sensitive a second time the message remains marked so and no further
+ operation is executed.
+
+ As a safety precaution all messages that are created as reply to messages that are marked sensitive
+ are also implicitly marked so.
+
+
+
+ Return Value
+
+ On success, theis functions return 0 or a positive integer. On failure, it returns a
+ negative errno-style error code.
+
+
+ Errors
+
+ Returned errors may indicate the following problems:
+
+
+
+ -EINVAL
+
+ The message parameter is
+ NULL.
+
+
+
+
+
+
+
+
+
+ See Also
+
+
+ systemd1,
+ sd-bus3,
+ sd_bus_message_new_method_call3
+
+
+
+
diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml
new file mode 100644
index 0000000000000..9313ea03141a5
--- /dev/null
+++ b/man/systemd-homed.service.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ systemd-homed.service
+ systemd
+
+
+
+ systemd-homed.service
+ 8
+
+
+
+ systemd-homed.service
+ systemd-homed
+ Home Directory/User Account Manager
+
+
+
+ systemd-homed.service
+ /usr/lib/systemd/systemd-homed
+
+
+
+ Description
+
+ systemd-homed is a system service that may be used to create, remove, change or
+ inspect home directories.
+
+ Most of systemd-homed's functionality is accessible through the
+ homectl1 command.
+
+ See the Home Directories documentation for
+ details about the format and design of home directories managed by
+ systemd-homed.service.
+
+ Each home directory managed by systemd-homed.service synthesizes a local user
+ and group. These are made available to the system using the Varlink User/Group Record Lookup API, and thus may be browsed
+ with
+ userdbctl1.
+
+
+
+ See Also
+
+ systemd1,
+ homectl1,
+ userdbctl1
+
+
+
diff --git a/man/systemd-userdbd.service.xml b/man/systemd-userdbd.service.xml
new file mode 100644
index 0000000000000..b73004f7d29f4
--- /dev/null
+++ b/man/systemd-userdbd.service.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+ systemd-userdbd.service
+ systemd
+
+
+
+ systemd-userdbd.service
+ 8
+
+
+
+ systemd-userdbd.service
+ systemd-userdbd
+ JSON User/Group Record Query Multiplexer/NSS Compatibility
+
+
+
+ systemd-userdbd.service
+ /usr/lib/systemd/systemd-userdbd
+
+
+
+ Description
+
+ systemd-userdbd is a system service that multiplexes user/group lookups to all
+ local services that provide JSON user/group record definitions to the system. In addition it synthesizes
+ JSON user/group records from classic UNIX/glibc NSS user/group records in order to provide full backwards
+ compatibility.
+
+ Most of systemd-userdbd's functionality is accessible through the
+ userdbctl1
+ command.
+
+ The user and group records this service provides access to follow the JSON User Record and JSON Group Record definitions. This service implements the
+ Varlink User/Group Record Lookup API, and
+ multiplexes access other services implementing this API, too. It is thus both server and client of this
+ API.
+
+ This service provides two distinct Varlink services:
+ io.systemd.Multiplexer provides a single, unified API for querying JSON user and
+ group records. Internally it talks to all other user/group record services running on the system in
+ parallel and forwards any information discovered. This simplifies clients substantially since they need
+ to talk to a single service only instead of all of them in
+ parallel. io.systemd.NameSeviceSwitch provides compatibility with classic UNIX/glibc
+ NSS user records, i.e. converts struct passwd and struct group records as
+ acquired with APIs such as getpwnam1 to JSON
+ user/group records, thus hiding the differences between the services as much as possible.
+
+
+
+ See Also
+
+ systemd1,
+ nss-systemd8,
+ userdbctl1
+
+
+
diff --git a/man/userdbctl.xml b/man/userdbctl.xml
new file mode 100644
index 0000000000000..72043a423aedb
--- /dev/null
+++ b/man/userdbctl.xml
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+ userdbctl
+ systemd
+
+
+
+ userdbctl
+ 1
+
+
+
+ userdbctl
+ Inspect users, groups and group memberships
+
+
+
+
+ userdbctl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+
+ Description
+
+ userdbctl may be used to inspect user and groups (as well as group memberships)
+ of the system. This client utility inquires user/group information provided by various system services,
+ both operating on advanced JSON user/group records (as defined by the JSON User Record and JSON Group Record definitions), and classic UNIX NSS/glibc
+ user and group records. This tool is primarily a client to the Varlink User/Group Lookup API.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+ MODE
+
+ Choose the display mode, takes one of classic,
+ friendly, table, json. If
+ classic an output very close to the format of /etc/passwd or
+ /etc/group is generated. If friendly a more comprehensive and
+ user friendly, human readable output is generated; if table a minimal, tabular
+ output is generated; if json a JSON formatted output is generated. Defaults to
+ friendly if a user/group is specified on the command line,
+ table otherwise.
+
+
+
+ SERVICE:SERVICE…
+ SERVICE:SERVICE…
+
+ Controls which services to enquire for looking up or enumerating users/groups. Takes
+ a list of one or more service names, separated by :. See below for a list of
+ well-known service names. If not specified all available services are enquired at
+ once.
+
+
+
+ BOOL
+
+ Controls whether to include classic glibc/NSS user/group lookups in the output. If
+ is used any attempts to resolve or enumerate users/groups provided
+ only via glibc NSS is suppressed. If is specified such users/groups
+ are included in the output (which is the default).
+
+
+
+ BOOL
+
+ Controls whether to synthesize records for the root and nobody users/groups if they
+ aren't defined otherwise. By default (or yes) such records are implicitly
+ synthesized if otherwise missing since they have special significance to the OS. When
+ no this synthesizing is turned off.
+
+
+
+
+
+ This option is short for
+ . Use this option to show only records that are natively defined as
+ JSON user or group records, with all NSS/glibc compatibility and all implicit synthesis turned
+ off.
+
+
+
+
+
+
+
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+
+ user USER…
+
+ List all known users records or show details of one or more specified user
+ records. Use to tweak display mode.
+
+
+
+ group GROUP…
+
+ List all known group records or show details of one or more specified group
+ records. Use to tweak display mode.
+
+
+
+ users-in-group GROUP…
+
+ List users that are members of the specified groups. If no groups are specified list
+ all user/group memberships defined. Use to tweak display
+ mode.
+
+
+
+ groups-of-user USER…
+
+ List groups that the specified users are members of. If no users are specified list
+ all user/group memberships defined (in this case groups-of-user and
+ users-in-group are equivalent). Use to tweak display
+ mode.
+
+
+
+ services
+
+ List all services currently providing user/group definitions to the system. See below
+ for a list of well-known services providing user information.
+
+
+
+
+
+ Well-Known Services
+
+ The userdbctl services command will list all currently running services that
+ provide user or group definitions to the system. The following are well-known services are shown among
+ this list.
+
+
+
+
+ io.systemd.DynamicUser
+
+ This service is provided by the system service manager itself (i.e. PID 1) and
+ makes all users (and their groups) synthesized through the DynamicUser= setting in
+ service unit files available to the system (see
+ systemd.exec5 for
+ details about this setting).
+
+
+
+ io.systemd.Home
+
+ This service is provided by
+ systemd-homed.service8
+ and makes all users (and their groups) belonging to home directories managed by that service
+ available to the system.
+
+
+
+ io.systemd.Multiplexer
+
+ This service is provided by
+ systemd-userdbd.service8
+ and multiplexes user/group look-ups to all other running lookup services. This is the primary entry point
+ for user/group record clients, as it simplifies client side implementation substantially since they
+ can ask a single service for lookups instead of asking all running services in parallel.
+ userdbctl uses this service preferably, too, unless
+ or are used, in which case finer control over the services to talk to is
+ required.
+
+
+
+ io.systemd.NameSeviceSwitch
+
+ This service is (also) provided by
+ systemd-userdbd.service8
+ and converts classic NSS/glibc user and group records to JSON user/group records, providing full
+ backwards compatibility. Use to disable this compatibility, see
+ above. Note that compatibility is actually provided in both directions:
+ nss-systemd8 will
+ automatically synthesize classic NSS/glibc user/group records from all JSON user/group records
+ provided to the system, thus using both APIs is mostly equivalent and provides access to the same
+ data, however the NSS/glibc APIs necessarily expose a more reduced set of fields
+ only.
+
+
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd-userdbd.service8,
+ systemd-homed.service8,
+ nss-systemd8,
+ getent1
+
+
+
+
diff --git a/meson.build b/meson.build
index e5ceb1e169db3..1bdb6d7d891ee 100644
--- a/meson.build
+++ b/meson.build
@@ -242,6 +242,8 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib
conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg'))
conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg'))
conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd'))
+conf.set_quoted('SYSTEMD_HOMEWORK_PATH', join_paths(rootlibexecdir, 'systemd-homework'))
+conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork'))
conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default)
conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no')
conf.set('STATUS_UNIT_FORMAT_DEFAULT', 'STATUS_UNIT_FORMAT_' + status_unit_format_default.to_upper())
@@ -836,6 +838,12 @@ endif
libmount = dependency('mount',
version : fuzzer_build ? '>= 0' : '>= 2.30')
+want_libfdisk = get_option('homed')
+libfdisk = dependency('fdisk', required : want_libfdisk)
+
+libpwquality = dependency('pwquality')
+conf.set10('HAVE_LIBPWQUALITY', libpwquality.found())
+
want_seccomp = get_option('seccomp')
if want_seccomp != 'false' and not skip_deps
libseccomp = dependency('libseccomp',
@@ -960,7 +968,7 @@ conf.set10('HAVE_MICROHTTPD', have)
want_libcryptsetup = get_option('libcryptsetup')
if want_libcryptsetup != 'false' and not skip_deps
libcryptsetup = dependency('libcryptsetup',
- version : '>= 1.6.0',
+ version : '>= 2.2.0',
required : want_libcryptsetup == 'true')
have = libcryptsetup.found()
have_sector = cc.has_member(
@@ -1073,6 +1081,17 @@ else
endif
conf.set10('HAVE_OPENSSL', have)
+want_p11kit = get_option('p11kit')
+if want_p11kit != 'false' and not skip_deps
+ libp11kit = dependency('p11-kit-1',
+ required : want_p11kit == 'true')
+ have = libp11kit.found()
+else
+ have = false
+ libp11kit = []
+endif
+conf.set10('HAVE_P11KIT', have)
+
want_elfutils = get_option('elfutils')
if want_elfutils != 'false' and not skip_deps
libdw = dependency('libdw',
@@ -1279,6 +1298,8 @@ foreach term : ['utmp',
'localed',
'machined',
'portabled',
+ 'userdb',
+ 'homed',
'networkd',
'timedated',
'timesyncd',
@@ -1492,6 +1513,8 @@ subdir('src/kernel-install')
subdir('src/locale')
subdir('src/machine')
subdir('src/portable')
+subdir('src/userdb')
+subdir('src/home')
subdir('src/nspawn')
subdir('src/resolve')
subdir('src/timedate')
@@ -1518,7 +1541,7 @@ test_dlopen = executable(
build_by_default : want_tests != 'false')
foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'],
- ['systemd', 'ENABLE_NSS_SYSTEMD'],
+ ['systemd', 'ENABLE_NSS_SYSTEMD', 'src/nss-systemd/userdb-glue.c src/nss-systemd/userdb-glue.h'],
['mymachines', 'ENABLE_NSS_MYMACHINES'],
['resolve', 'ENABLE_NSS_RESOLVE']]
@@ -1529,9 +1552,14 @@ foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'],
sym = 'src/nss-@0@/nss-@0@.sym'.format(module)
version_script_arg = join_paths(project_source_root, sym)
+ sources = ['src/nss-@0@/nss-@0@.c'.format(module)]
+ if tuple.length() > 2
+ sources += tuple[2].split()
+ endif
+
nss = shared_library(
'nss_' + module,
- 'src/nss-@0@/nss-@0@.c'.format(module),
+ sources,
disable_mempool_c,
version : '2',
include_directories : includes,
@@ -1928,6 +1956,94 @@ if conf.get('ENABLE_PORTABLED') == 1
public_programs += exe
endif
+if conf.get('ENABLE_USERDB') == 1
+ executable('systemd-userwork',
+ systemd_userwork_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+
+ executable('systemd-userdbd',
+ systemd_userdbd_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+
+ executable('userdbctl',
+ userdbctl_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootbindir)
+endif
+
+if conf.get('ENABLE_HOMED') == 1
+ executable('systemd-homework',
+ systemd_homework_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads,
+ libcryptsetup,
+ libblkid,
+ libcrypt,
+ libopenssl,
+ libfdisk],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+
+ executable('systemd-homed',
+ systemd_homed_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads,
+ libcrypt,
+ libopenssl,
+ libpwquality],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+
+ executable('homectl',
+ homectl_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads,
+ libcrypt,
+ libpwquality],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootbindir)
+
+ if conf.get('HAVE_PAM') == 1
+ version_script_arg = join_paths(project_source_root, pam_systemd_home_sym)
+ pam_systemd = shared_library(
+ 'pam_systemd_home',
+ pam_systemd_home_c,
+ name_prefix : '',
+ include_directories : includes,
+ link_args : ['-shared',
+ '-Wl,--version-script=' + version_script_arg],
+ link_with : [libsystemd_static,
+ libshared_static],
+ dependencies : [threads,
+ libpam,
+ libpam_misc,
+ libcrypt],
+ link_depends : pam_systemd_home_sym,
+ install : true,
+ install_dir : pamlibdir)
+ endif
+endif
+
foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit']
meson.add_install_script(meson_make_symlink,
join_paths(rootbindir, 'systemctl'),
@@ -1964,10 +2080,13 @@ executable('systemd-system-update-generator',
if conf.get('HAVE_LIBCRYPTSETUP') == 1
executable('systemd-cryptsetup',
- 'src/cryptsetup/cryptsetup.c',
+ ['src/cryptsetup/cryptsetup.c',
+ 'src/cryptsetup/cryptsetup-pkcs11.c',
+ 'src/cryptsetup/cryptsetup-pkcs11.h'],
include_directories : includes,
link_with : [libshared],
- dependencies : [libcryptsetup],
+ dependencies : [libcryptsetup,
+ libp11kit],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootlibexecdir)
diff --git a/meson_options.txt b/meson_options.txt
index 5dc898eb80480..3c314adf0140e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -90,6 +90,10 @@ option('machined', type : 'boolean',
description : 'install the systemd-machined stack')
option('portabled', type : 'boolean',
description : 'install the systemd-portabled stack')
+option('userdb', type : 'boolean',
+ description : 'install the systemd-userdbd stack')
+option('homed', type : 'boolean',
+ description : 'install the systemd-homed stack')
option('networkd', type : 'boolean',
description : 'install the systemd-networkd stack')
option('timedated', type : 'boolean',
@@ -278,6 +282,8 @@ option('gnutls', type : 'combo', choices : ['auto', 'true', 'false'],
description : 'gnutls support')
option('openssl', type : 'combo', choices : ['auto', 'true', 'false'],
description : 'openssl support')
+option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'],
+ description : 'p11kit support')
option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'],
description : 'elfutils support')
option('zlib', type : 'combo', choices : ['auto', 'true', 'false'],
diff --git a/src/basic/errno-util.h b/src/basic/errno-util.h
index 6053cde62dd42..670772623b299 100644
--- a/src/basic/errno-util.h
+++ b/src/basic/errno-util.h
@@ -86,3 +86,26 @@ static inline bool ERRNO_IS_RESOURCE(int r) {
ENFILE,
ENOMEM);
}
+
+/* Three different errors for "operation/system call/ioctl not supported" */
+static inline bool ERRNO_IS_NOT_SUPPORTED(int r) {
+ return IN_SET(abs(r),
+ EOPNOTSUPP,
+ ENOTTY,
+ ENOSYS);
+}
+
+/* Three difference errors for "not enough disk space" */
+static inline bool ERRNO_IS_DISK_SPACE(int r) {
+ return IN_SET(abs(r),
+ ENOSPC,
+ EDQUOT,
+ EFBIG);
+}
+
+/* Two different errors for access problems */
+static inline bool ERRNO_IS_PRIVILEGE(int r) {
+ return IN_SET(abs(r),
+ EACCES,
+ EPERM);
+}
diff --git a/src/basic/fileio.c b/src/basic/fileio.c
index 623e43e4caeae..eb7e92f88d811 100644
--- a/src/basic/fileio.c
+++ b/src/basic/fileio.c
@@ -138,16 +138,21 @@ static int write_string_file_atomic(
assert(fn);
assert(line);
+ /* Note that we'd really like to use O_TMPFILE here, but can't really, since we want replacement
+ * semantics here, and O_TMPFILE can't offer that. i.e. rename() replaces but linkat() doesn't. */
+
r = fopen_temporary(fn, &f, &p);
if (r < 0)
return r;
- (void) fchmod_umask(fileno(f), 0644);
-
r = write_string_stream_ts(f, line, flags, ts);
if (r < 0)
goto fail;
+ r = fchmod_umask(fileno(f), FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0644);
+ if (r < 0)
+ goto fail;
+
if (rename(p, fn) < 0) {
r = -errno;
goto fail;
@@ -167,7 +172,7 @@ int write_string_file_ts(
struct timespec *ts) {
_cleanup_fclose_ FILE *f = NULL;
- int q, r;
+ int q, r, fd;
assert(fn);
assert(line);
@@ -192,26 +197,20 @@ int write_string_file_ts(
} else
assert(!ts);
- if (flags & WRITE_STRING_FILE_CREATE) {
- r = fopen_unlocked(fn, "we", &f);
- if (r < 0)
- goto fail;
- } else {
- int fd;
-
- /* We manually build our own version of fopen(..., "we") that
- * works without O_CREAT */
- fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | ((flags & WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0));
- if (fd < 0) {
- r = -errno;
- goto fail;
- }
+ /* We manually build our own version of fopen(..., "we") that works without O_CREAT and with O_NOFOLLOW if needed. */
+ fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY |
+ (FLAGS_SET(flags, WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0) |
+ (FLAGS_SET(flags, WRITE_STRING_FILE_CREATE) ? O_CREAT : 0),
+ (FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0666));
+ if (fd < 0) {
+ r = -errno;
+ goto fail;
+ }
- r = fdopen_unlocked(fd, "w", &f);
- if (r < 0) {
- safe_close(fd);
- goto fail;
- }
+ r = fdopen_unlocked(fd, "w", &f);
+ if (r < 0) {
+ safe_close(fd);
+ goto fail;
}
if (flags & WRITE_STRING_FILE_DISABLE_BUFFER)
@@ -438,17 +437,19 @@ int read_full_stream_full(
return r;
}
-int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) {
+int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) {
_cleanup_fclose_ FILE *f = NULL;
int r;
assert(filename);
assert(contents);
- r = fopen_unlocked(filename, "re", &f);
+ r = xfopenat(dir_fd, filename, "re", 0, &f);
if (r < 0)
return r;
+ (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
return read_full_stream_full(f, filename, flags, contents, size);
}
@@ -575,6 +576,81 @@ DIR *xopendirat(int fd, const char *name, int flags) {
return d;
}
+static int mode_to_flags(const char *mode) {
+ const char *p;
+ int flags;
+
+ if ((p = startswith(mode, "r+")))
+ flags = O_RDWR;
+ else if ((p = startswith(mode, "r")))
+ flags = O_RDONLY;
+ else if ((p = startswith(mode, "w+")))
+ flags = O_RDWR|O_CREAT|O_TRUNC;
+ else if ((p = startswith(mode, "w")))
+ flags = O_WRONLY|O_CREAT|O_TRUNC;
+ else if ((p = startswith(mode, "a+")))
+ flags = O_RDWR|O_CREAT|O_APPEND;
+ else if ((p = startswith(mode, "a")))
+ flags = O_WRONLY|O_CREAT|O_APPEND;
+ else
+ return -EINVAL;
+
+ for (; *p != 0; p++) {
+
+ switch (*p) {
+
+ case 'e':
+ flags |= O_CLOEXEC;
+ break;
+
+ case 'x':
+ flags |= O_EXCL;
+ break;
+
+ case 'm':
+ /* ignore this here, fdopen() might care later though */
+ break;
+
+ case 'c': /* not sure what to do about this one */
+ default:
+ return -EINVAL;
+ }
+ }
+
+ return flags;
+}
+
+int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret) {
+ FILE *f;
+
+ /* A combination of fopen() with openat() */
+
+ if (dir_fd == AT_FDCWD && flags == 0) {
+ f = fopen(path, mode);
+ if (!f)
+ return -errno;
+ } else {
+ int fd, mode_flags;
+
+ mode_flags = mode_to_flags(mode);
+ if (mode_flags < 0)
+ return mode_flags;
+
+ fd = openat(dir_fd, path, mode_flags | flags);
+ if (fd < 0)
+ return -errno;
+
+ f = fdopen(fd, mode);
+ if (!f) {
+ safe_close(fd);
+ return -errno;
+ }
+ }
+
+ *ret = f;
+ return 0;
+}
+
static int search_and_fopen_internal(const char *path, const char *mode, const char *root, char **search, FILE **_f) {
char **i;
diff --git a/src/basic/fileio.h b/src/basic/fileio.h
index 05f6c89da09dc..3cf1ab77bf6a3 100644
--- a/src/basic/fileio.h
+++ b/src/basic/fileio.h
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
#include "macro.h"
@@ -22,6 +23,7 @@ typedef enum {
WRITE_STRING_FILE_DISABLE_BUFFER = 1 << 5,
WRITE_STRING_FILE_NOFOLLOW = 1 << 6,
WRITE_STRING_FILE_MKDIR_0755 = 1 << 7,
+ WRITE_STRING_FILE_MODE_0600 = 1 << 8,
/* And before you wonder, why write_string_file_atomic_label_ts() is a separate function instead of just one
more flag here: it's about linking: we don't want to pull -lselinux into all users of write_string_file()
@@ -52,9 +54,9 @@ static inline int write_string_file(const char *fn, const char *line, WriteStrin
int write_string_filef(const char *fn, WriteStringFileFlags flags, const char *format, ...) _printf_(3, 4);
int read_one_line_file(const char *filename, char **line);
-int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size);
+int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size);
static inline int read_full_file(const char *filename, char **contents, size_t *size) {
- return read_full_file_full(filename, 0, contents, size);
+ return read_full_file_full(AT_FDCWD, filename, 0, contents, size);
}
int read_full_stream_full(FILE *f, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size);
static inline int read_full_stream(FILE *f, char **contents, size_t *size) {
@@ -68,6 +70,7 @@ int executable_is_script(const char *path, char **interpreter);
int get_proc_field(const char *filename, const char *pattern, const char *terminator, char **field);
DIR *xopendirat(int dirfd, const char *name, int flags);
+int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret);
int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **_f);
int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **_f);
diff --git a/src/basic/memory-util.h b/src/basic/memory-util.h
index 9cb8ac3c10fca..66c64bdf0e779 100644
--- a/src/basic/memory-util.h
+++ b/src/basic/memory-util.h
@@ -79,14 +79,21 @@ static inline void* explicit_bzero_safe(void *p, size_t l) {
void *explicit_bzero_safe(void *p, size_t l);
#endif
-static inline void erase_and_freep(void *p) {
- void *ptr = *(void**) p;
+static inline void* erase_and_free(void *p) {
+ size_t l;
+
+ if (!p)
+ return NULL;
+
+ l = malloc_usable_size(p);
+ explicit_bzero_safe(p, l);
+ free(p);
- if (ptr) {
- size_t l = malloc_usable_size(ptr);
- explicit_bzero_safe(ptr, l);
- free(ptr);
- }
+ return NULL;
+}
+
+static inline void erase_and_freep(void *p) {
+ erase_and_free(*(void**) p);
}
/* Use with _cleanup_ to erase a single 'char' when leaving scope */
diff --git a/src/basic/missing_magic.h b/src/basic/missing_magic.h
index 4910cd368f90a..80bcfcca68edb 100644
--- a/src/basic/missing_magic.h
+++ b/src/basic/missing_magic.h
@@ -32,3 +32,8 @@
#ifndef MQUEUE_MAGIC
#define MQUEUE_MAGIC 0x19800202
#endif
+
+/* Not exposed yet (4.20). Defined in fs/xfs/libxfs/xfs_format.h */
+#ifndef XFS_SB_MAGIC
+#define XFS_SB_MAGIC 0x58465342
+#endif
diff --git a/src/basic/missing_xfs.h b/src/basic/missing_xfs.h
new file mode 100644
index 0000000000000..9eac76dd67c4e
--- /dev/null
+++ b/src/basic/missing_xfs.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+/* This is currently not exported in the public kernel headers, but the libxfs library code part of xfsprogs
+ * defines it as public header */
+
+#ifndef XFS_IOC_FSGEOMETRY
+#define XFS_IOC_FSGEOMETRY _IOR ('X', 124, struct xfs_fsop_geom)
+
+typedef struct xfs_fsop_geom {
+ uint32_t blocksize;
+ uint32_t rtextsize;
+ uint32_t agblocks;
+ uint32_t agcount;
+ uint32_t logblocks;
+ uint32_t sectsize;
+ uint32_t inodesize;
+ uint32_t imaxpct;
+ uint64_t datablocks;
+ uint64_t rtblocks;
+ uint64_t rtextents;
+ uint64_t logstart;
+ unsigned char uuid[16];
+ uint32_t sunit;
+ uint32_t swidth;
+ int32_t version;
+ uint32_t flags;
+ uint32_t logsectsize;
+ uint32_t rtsectsize;
+ uint32_t dirblocksize;
+ uint32_t logsunit;
+} xfs_fsop_geom_t;
+#endif
+
+#ifndef XFS_IOC_FSGROWFSDATA
+#define XFS_IOC_FSGROWFSDATA _IOW ('X', 110, struct xfs_growfs_data)
+
+typedef struct xfs_growfs_data {
+ uint64_t newblocks;
+ uint32_t imaxpct;
+} xfs_growfs_data_t;
+#endif
diff --git a/src/basic/nss-util.h b/src/basic/nss-util.h
index 2045175d1cb3c..29cf22676ae6f 100644
--- a/src/basic/nss-util.h
+++ b/src/basic/nss-util.h
@@ -139,6 +139,38 @@ enum nss_status _nss_##module##_getgrgid_r( \
char *buffer, size_t buflen, \
int *errnop) _public_
+#define NSS_PWENT_PROTOTYPES(module) \
+enum nss_status _nss_##module##_endpwent( \
+ void) _public_; \
+enum nss_status _nss_##module##_setpwent( \
+ int stayopen) _public_; \
+enum nss_status _nss_##module##_getpwent_r( \
+ struct passwd *result, \
+ char *buffer, \
+ size_t buflen, \
+ int *errnop) _public_;
+
+#define NSS_GRENT_PROTOTYPES(module) \
+enum nss_status _nss_##module##_endgrent( \
+ void) _public_; \
+enum nss_status _nss_##module##_setgrent( \
+ int stayopen) _public_; \
+enum nss_status _nss_##module##_getgrent_r( \
+ struct group *result, \
+ char *buffer, \
+ size_t buflen, \
+ int *errnop) _public_;
+
+#define NSS_INITGROUPS_PROTOTYPE(module) \
+enum nss_status _nss_##module##_initgroups_dyn( \
+ const char *user, \
+ gid_t group, \
+ long int *start, \
+ long int *size, \
+ gid_t **groupsp, \
+ long int limit, \
+ int *errnop) _public_;
+
typedef enum nss_status (*_nss_gethostbyname4_r_t)(
const char *name,
struct gaih_addrtuple **pat,
diff --git a/src/basic/ordered-set.h b/src/basic/ordered-set.h
index ba43451e27d4a..383a729cab869 100644
--- a/src/basic/ordered-set.h
+++ b/src/basic/ordered-set.h
@@ -50,6 +50,10 @@ static inline void* ordered_set_remove(OrderedSet *s, void *p) {
return ordered_hashmap_remove((OrderedHashmap*) s, p);
}
+static inline void* ordered_set_first(OrderedSet *s) {
+ return ordered_hashmap_first((OrderedHashmap*) s);
+}
+
static inline void* ordered_set_steal_first(OrderedSet *s) {
return ordered_hashmap_steal_first((OrderedHashmap*) s);
}
diff --git a/src/basic/process-util.c b/src/basic/process-util.c
index 5452edd7a4bc0..f8822a8309952 100644
--- a/src/basic/process-util.c
+++ b/src/basic/process-util.c
@@ -1319,6 +1319,13 @@ int safe_fork_full(
log_full_errno(prio, r, "Failed to connect stdin/stdout to /dev/null: %m");
_exit(EXIT_FAILURE);
}
+
+ } else if (flags & FORK_STDOUT_TO_STDERR) {
+
+ if (dup2(STDERR_FILENO, STDOUT_FILENO) < 0) {
+ log_full_errno(prio, r, "Failed to connect stdout to stderr: %m");
+ _exit(EXIT_FAILURE);
+ }
}
if (flags & FORK_RLIMIT_NOFILE_SAFE) {
diff --git a/src/basic/process-util.h b/src/basic/process-util.h
index 41d4759c97136..dbe216cd9f100 100644
--- a/src/basic/process-util.h
+++ b/src/basic/process-util.h
@@ -148,16 +148,17 @@ void reset_cached_pid(void);
int must_be_root(void);
typedef enum ForkFlags {
- FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */
- FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */
- FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */
- FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */
- FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */
- FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */
- FORK_WAIT = 1 << 6, /* Wait until child exited */
- FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */
- FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */
- FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */
+ FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */
+ FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */
+ FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */
+ FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */
+ FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */
+ FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */
+ FORK_WAIT = 1 << 6, /* Wait until child exited */
+ FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */
+ FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */
+ FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */
+ FORK_STDOUT_TO_STDERR = 1 << 10, /* Make stdout a copy of stderr */
} ForkFlags;
int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid);
diff --git a/src/basic/string-util.c b/src/basic/string-util.c
index 9586b3940eb93..418a50e2bc7e3 100644
--- a/src/basic/string-util.c
+++ b/src/basic/string-util.c
@@ -1050,3 +1050,13 @@ bool string_is_safe(const char *p) {
return true;
}
+
+char* string_erase(char *x) {
+ if (!x)
+ return NULL;
+
+ /* A delicious drop of snake-oil! To be called on memory where we stored passphrases or so, after we
+ * used them. */
+ explicit_bzero_safe(x, strlen(x));
+ return x;
+}
diff --git a/src/basic/string-util.h b/src/basic/string-util.h
index 76767afcac49a..ddbb87e1fcf3e 100644
--- a/src/basic/string-util.h
+++ b/src/basic/string-util.h
@@ -263,3 +263,5 @@ static inline char* str_realloc(char **p) {
return (*p = t);
}
+
+char* string_erase(char *x);
diff --git a/src/basic/tmpfile-util.c b/src/basic/tmpfile-util.c
index afcf58aeac8bb..2b9ce5ef7a8af 100644
--- a/src/basic/tmpfile-util.c
+++ b/src/basic/tmpfile-util.c
@@ -20,50 +20,60 @@
#include "tmpfile-util.h"
#include "umask-util.h"
-int fopen_temporary(const char *path, FILE **_f, char **_temp_path) {
- FILE *f;
- char *t;
- int r, fd;
+int fopen_temporary(const char *path, FILE **ret_f, char **ret_temp_path) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *t = NULL;
+ _cleanup_close_ int fd = -1;
+ int r;
- assert(path);
- assert(_f);
- assert(_temp_path);
+ if (path) {
+ r = tempfn_xxxxxx(path, NULL, &t);
+ if (r < 0)
+ return r;
+ } else {
+ const char *d;
- r = tempfn_xxxxxx(path, NULL, &t);
- if (r < 0)
- return r;
+ r = tmp_dir(&d);
+ if (r < 0)
+ return r;
+
+ t = path_join(d, "XXXXXX");
+ if (!t)
+ return -ENOMEM;
+ }
fd = mkostemp_safe(t);
- if (fd < 0) {
- free(t);
+ if (fd < 0)
return -errno;
- }
/* This assumes that returned FILE object is short-lived and used within the same single-threaded
* context and never shared externally, hence locking is not necessary. */
r = fdopen_unlocked(fd, "w", &f);
if (r < 0) {
- unlink(t);
- free(t);
- safe_close(fd);
+ (void) unlink(t);
return r;
}
- *_f = f;
- *_temp_path = t;
+ TAKE_FD(fd);
+
+ if (ret_f)
+ *ret_f = TAKE_PTR(f);
+
+ if (ret_temp_path)
+ *ret_temp_path = TAKE_PTR(t);
return 0;
}
/* This is much like mkostemp() but is subject to umask(). */
int mkostemp_safe(char *pattern) {
- _unused_ _cleanup_umask_ mode_t u = umask(0077);
int fd;
assert(pattern);
- fd = mkostemp(pattern, O_CLOEXEC);
+ RUN_WITH_UMASK(0077)
+ fd = mkostemp(pattern, O_CLOEXEC);
if (fd < 0)
return -errno;
diff --git a/src/basic/user-util.c b/src/basic/user-util.c
index 3b253bc264dd3..70355554afb62 100644
--- a/src/basic/user-util.c
+++ b/src/basic/user-util.c
@@ -89,7 +89,7 @@ char *getusername_malloc(void) {
return uid_to_name(getuid());
}
-static bool is_nologin_shell(const char *shell) {
+bool is_nologin_shell(const char *shell) {
return PATH_IN_SET(shell,
/* 'nologin' is the friendliest way to disable logins for a user account. It prints a nice
@@ -925,3 +925,15 @@ int make_salt(char **ret) {
*ret = salt;
return 0;
}
+
+bool hashed_password_valid(const char *s) {
+
+ /* Returns true if the specified string is a 'valid' hashed UNIX password, i.e. if starts with '$' or
+ * with '!$' (the latter being a valid, yet locked password). */
+
+ if (!s)
+ return false;
+
+ return s[0] == '$' ||
+ (s[0] == '!' && s[1] == '$');
+}
diff --git a/src/basic/user-util.h b/src/basic/user-util.h
index cfa515f5e8a26..87972bdca4a1e 100644
--- a/src/basic/user-util.h
+++ b/src/basic/user-util.h
@@ -57,6 +57,14 @@ int take_etc_passwd_lock(const char *root);
#define ETC_PASSWD_LOCK_PATH "/etc/.pwd.lock"
+static inline bool uid_is_system(uid_t uid) {
+ return uid <= SYSTEM_UID_MAX;
+}
+
+static inline bool gid_is_system(gid_t gid) {
+ return gid <= SYSTEM_GID_MAX;
+}
+
static inline bool uid_is_dynamic(uid_t uid) {
return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX;
}
@@ -65,12 +73,12 @@ static inline bool gid_is_dynamic(gid_t gid) {
return uid_is_dynamic((uid_t) gid);
}
-static inline bool uid_is_system(uid_t uid) {
- return uid <= SYSTEM_UID_MAX;
+static inline bool uid_is_container(uid_t uid) {
+ return CONTAINER_UID_BASE_MIN <= uid && uid <= CONTAINER_UID_BASE_MAX;
}
-static inline bool gid_is_system(gid_t gid) {
- return gid <= SYSTEM_GID_MAX;
+static inline bool gid_is_container(gid_t gid) {
+ return uid_is_container((uid_t) gid);
}
/* The following macros add 1 when converting things, since UID 0 is a valid UID, while the pointer
@@ -127,3 +135,7 @@ int putsgent_sane(const struct sgrp *sg, FILE *stream);
#endif
int make_salt(char **ret);
+
+bool is_nologin_shell(const char *shell);
+
+bool hashed_password_valid(const char *s);
diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c
new file mode 100644
index 0000000000000..628b9ccaaa0dc
--- /dev/null
+++ b/src/core/core-varlink.c
@@ -0,0 +1,310 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "core-varlink.h"
+#include "mkdir.h"
+#include "user-util.h"
+#include "varlink.h"
+
+typedef struct LookupParameters {
+ const char *user_name;
+ const char *group_name;
+ union {
+ uid_t uid;
+ gid_t gid;
+ };
+ const char *service;
+} LookupParameters;
+
+static int build_user_json(const char *user_name, uid_t uid, JsonVariant **ret) {
+ assert(user_name);
+ assert(uid_is_valid(uid));
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(uid)),
+ JSON_BUILD_PAIR("realName", JSON_BUILD_STRING("Dynamic User")),
+ JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/")),
+ JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)),
+ JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)),
+ JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic"))))));
+}
+
+static bool user_match_lookup_parameters(LookupParameters *p, const char *name, uid_t uid) {
+ assert(p);
+
+ if (p->user_name && !streq(name, p->user_name))
+ return false;
+
+ if (uid_is_valid(p->uid) && uid != p->uid)
+ return false;
+
+ return true;
+}
+
+static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 },
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ LookupParameters p = {
+ .uid = UID_INVALID,
+ };
+ _cleanup_free_ char *found_name = NULL;
+ uid_t found_uid = UID_INVALID, uid;
+ Manager *m = userdata;
+ const char *un;
+ int r;
+
+ assert(parameters);
+ assert(m);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ if (uid_is_valid(p.uid))
+ r = dynamic_user_lookup_uid(m, p.uid, &found_name);
+ else if (p.user_name)
+ r = dynamic_user_lookup_name(m, p.user_name, &found_uid);
+ else {
+ Iterator i;
+ DynamicUser *d;
+
+ HASHMAP_FOREACH(d, m->dynamic_users, i) {
+ r = dynamic_user_current(d, &uid);
+ if (r == -EAGAIN) /* not realized yet? */
+ continue;
+ if (r < 0)
+ return r;
+
+ if (!user_match_lookup_parameters(&p, d->name, uid))
+ continue;
+
+ if (v) {
+ r = varlink_notify(link, v);
+ if (r < 0)
+ return r;
+
+ v = json_variant_unref(v);
+ }
+
+ r = build_user_json(d->name, uid, &v);
+ if (r < 0)
+ return r;
+ }
+
+ if (!v)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, v);
+ }
+ if (r == -ESRCH)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ if (r < 0)
+ return r;
+
+ uid = uid_is_valid(found_uid) ? found_uid : p.uid;
+ un = found_name ?: p.user_name;
+
+ if (!user_match_lookup_parameters(&p, un, uid))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_user_json(un, uid, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+static int build_group_json(const char *group_name, gid_t gid, JsonVariant **ret) {
+ assert(group_name);
+ assert(gid_is_valid(gid));
+ assert(ret);
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(group_name)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)),
+ JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic"))))));
+}
+
+static bool group_match_lookup_parameters(LookupParameters *p, const char *name, gid_t gid) {
+ assert(p);
+
+ if (p->group_name && !streq(name, p->group_name))
+ return false;
+
+ if (gid_is_valid(p->gid) && gid != p->gid)
+ return false;
+
+ return true;
+}
+
+static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ LookupParameters p = {
+ .gid = GID_INVALID,
+ };
+ _cleanup_free_ char *found_name = NULL;
+ uid_t found_gid = GID_INVALID, gid;
+ Manager *m = userdata;
+ const char *gn;
+ int r;
+
+ assert(parameters);
+ assert(m);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ if (gid_is_valid(p.gid))
+ r = dynamic_user_lookup_uid(m, (uid_t) p.gid, &found_name);
+ else if (p.group_name)
+ r = dynamic_user_lookup_name(m, p.group_name, (uid_t*) &found_gid);
+ else {
+ DynamicUser *d;
+ Iterator i;
+
+ HASHMAP_FOREACH(d, m->dynamic_users, i) {
+ uid_t uid;
+
+ r = dynamic_user_current(d, &uid);
+ if (r == -EAGAIN)
+ continue;
+ if (r < 0)
+ return r;
+
+ if (!group_match_lookup_parameters(&p, d->name, (gid_t) uid))
+ continue;
+
+ if (v) {
+ r = varlink_notify(link, v);
+ if (r < 0)
+ return r;
+
+ v = json_variant_unref(v);
+ }
+
+ r = build_group_json(d->name, (gid_t) uid, &v);
+ if (r < 0)
+ return r;
+ }
+
+ if (!v)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, v);
+ }
+ if (r == -ESRCH)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ if (r < 0)
+ return r;
+
+ gid = gid_is_valid(found_gid) ? found_gid : p.gid;
+ gn = found_name ?: p.group_name;
+
+ if (!group_match_lookup_parameters(&p, gn, gid))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_group_json(gn, gid, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ LookupParameters p = {};
+ int r;
+
+ assert(parameters);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ /* We don't support auxiliary groups with dynamic users. */
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+}
+
+int manager_varlink_init(Manager *m) {
+ _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL;
+ int r;
+
+ assert(m);
+
+ if (m->varlink_server)
+ return 0;
+
+ if (!MANAGER_IS_SYSTEM(m))
+ return 0;
+
+ r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate varlink server object: %m");
+
+ varlink_server_set_userdata(s, m);
+
+ r = varlink_server_bind_method_many(
+ s,
+ "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record,
+ "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
+ "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
+ if (r < 0)
+ return log_error_errno(r, "Failed to register varlink methods: %m");
+
+ (void) mkdir_p("/run/systemd/userdb", 0755);
+
+ r = varlink_server_listen_address(s, "/run/systemd/userdb/io.systemd.DynamicUser", 0666);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind to varlink socket: %m");
+
+ r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+ m->varlink_server = TAKE_PTR(s);
+ return 0;
+}
+
+void manager_varlink_done(Manager *m) {
+ assert(m);
+
+ m->varlink_server = varlink_server_unref(m->varlink_server);
+}
diff --git a/src/core/core-varlink.h b/src/core/core-varlink.h
new file mode 100644
index 0000000000000..89818e2766caa
--- /dev/null
+++ b/src/core/core-varlink.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "manager.h"
+
+int manager_varlink_init(Manager *m);
+void manager_varlink_done(Manager *m);
diff --git a/src/core/dynamic-user.c b/src/core/dynamic-user.c
index e7a2f64525165..5dd7de2c7c4cd 100644
--- a/src/core/dynamic-user.c
+++ b/src/core/dynamic-user.c
@@ -531,7 +531,6 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) {
int r;
assert(d);
- assert(ret);
/* Get the currently assigned UID for the user, if there's any. This simply pops the data from the storage socket, and pushes it back in right-away. */
@@ -547,7 +546,9 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) {
if (r < 0)
return r;
- *ret = uid;
+ if (ret)
+ *ret = uid;
+
return 0;
}
@@ -734,7 +735,6 @@ int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret) {
assert(m);
assert(name);
- assert(ret);
/* A friendly call for translating a dynamic user's name into its UID */
diff --git a/src/core/manager.c b/src/core/manager.c
index d9114bb0c597a..ded90f3d32787 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -31,6 +31,7 @@
#include "bus-util.h"
#include "clean-ipc.h"
#include "clock-util.h"
+#include "core-varlink.h"
#include "dbus-job.h"
#include "dbus-manager.h"
#include "dbus-unit.h"
@@ -45,8 +46,8 @@
#include "fileio.h"
#include "fs-util.h"
#include "hashmap.h"
-#include "io-util.h"
#include "install.h"
+#include "io-util.h"
#include "label.h"
#include "locale-setup.h"
#include "log.h"
@@ -1337,6 +1338,7 @@ Manager* manager_free(Manager *m) {
lookup_paths_flush_generator(&m->lookup_paths);
bus_done(m);
+ manager_varlink_done(m);
exec_runtime_vacuum(m);
hashmap_free(m->exec_runtime_by_id);
@@ -1698,6 +1700,10 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds) {
log_warning_errno(r, "Failed to deserialized tracked clients, ignoring: %m");
m->deserialized_subscribed = strv_free(m->deserialized_subscribed);
+ r = manager_varlink_init(m);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set up watchdog server, ignoring: %m");
+
/* Third, fire things up! */
manager_coldplug(m);
diff --git a/src/core/manager.h b/src/core/manager.h
index 308ee013bda9d..73c68488a3ccf 100644
--- a/src/core/manager.h
+++ b/src/core/manager.h
@@ -15,6 +15,7 @@
#include "list.h"
#include "prioq.h"
#include "ratelimit.h"
+#include "varlink.h"
struct libmnt_monitor;
typedef struct Unit Unit;
@@ -423,6 +424,8 @@ struct Manager {
unsigned notifygen;
bool honor_device_enumeration;
+
+ VarlinkServer *varlink_server;
};
static inline usec_t manager_default_timeout_abort_usec(Manager *m) {
diff --git a/src/core/meson.build b/src/core/meson.build
index fb6820e109a9e..3586838f59b3b 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -22,8 +22,8 @@ libcore_sources = '''
bpf-firewall.h
cgroup.c
cgroup.h
- chown-recursive.c
- chown-recursive.h
+ core-varlink.c
+ core-varlink.h
dbus-automount.c
dbus-automount.h
dbus-cgroup.c
diff --git a/src/core/namespace.c b/src/core/namespace.c
index 973b64007cf87..89b776f175dd7 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include
+#include
#include
#include
#include
@@ -1215,6 +1216,7 @@ int setup_namespace(
r = loop_device_make_by_path(root_image,
dissect_image_flags & DISSECT_IMAGE_READ_ONLY ? O_RDONLY : O_RDWR,
+ LO_FLAGS_PARTSCAN,
&loop_device);
if (r < 0)
return log_debug_errno(r, "Failed to create loop device for root image: %m");
diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c
new file mode 100644
index 0000000000000..a13e5427f2dee
--- /dev/null
+++ b/src/cryptsetup/cryptsetup-pkcs11.c
@@ -0,0 +1,707 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include "alloc-util.h"
+#include "ask-password-api.h"
+#include "cryptsetup-pkcs11.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "stat-util.h"
+#include "strv.h"
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(P11KitUri*, p11_kit_uri_free);
+DEFINE_TRIVIAL_CLEANUP_FUNC(CK_FUNCTION_LIST**, p11_kit_modules_finalize_and_release);
+
+static int token_login(
+ const char *friendly_name,
+ CK_FUNCTION_LIST *m,
+ CK_SESSION_HANDLE session,
+ CK_SLOT_ID slotid,
+ const CK_TOKEN_INFO *token_info,
+ const char *token_uri_string,
+ const char *token_label,
+ usec_t until) {
+
+ _cleanup_free_ char *token_uri_escaped = NULL, *id = NULL;
+ CK_TOKEN_INFO updated_token_info;
+ CK_RV rv;
+ int r;
+
+ assert(friendly_name);
+ assert(m);
+ assert(token_info);
+ assert(token_uri_string);
+ assert(token_label);
+
+ if (FLAGS_SET(token_info->flags, CKF_PROTECTED_AUTHENTICATION_PATH)) {
+ rv = m->C_Login(session, CKU_USER, NULL, 0);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+ log_info("Successully logged into security token '%s' via protected authentication path.", token_label);
+ return 0;
+ }
+
+ if (!FLAGS_SET(token_info->flags, CKF_LOGIN_REQUIRED)) {
+ log_info("No login into security token '%s' required.", token_label);
+ return 0;
+ }
+
+ token_uri_escaped = cescape(token_uri_string);
+ if (!token_uri_escaped)
+ return log_oom();
+
+ id = strjoin("pkcs11:", token_uri_escaped);
+ if (!id)
+ return log_oom();
+
+ for (unsigned tries = 0; tries < 3; tries++) {
+ _cleanup_strv_free_erase_ char **passwords = NULL;
+ _cleanup_free_ char *text = NULL;
+ char **i;
+
+ if (FLAGS_SET(token_info->flags, CKF_USER_PIN_FINAL_TRY))
+ r = asprintf(&text, "Please enter correct PIN for security token '%s' in order to unlock disk %s (final try):", token_label, friendly_name);
+ if (FLAGS_SET(token_info->flags, CKF_USER_PIN_COUNT_LOW))
+ r = asprintf(&text, "PIN has been entered incorrectly previously, please enter correct PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name);
+ else if (tries == 0)
+ r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name);
+ else
+ r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s (try #%u):", token_label, friendly_name, tries+1);
+ if (r < 0)
+ return log_oom();
+
+ /* We never cache PINs, simply because it's fatal if we use wrong PINs, since usually there are only 3 tries */
+ r = ask_password_auto(text, "drive-harddisk", id, "pkcs11-pin", until, 0, &passwords);
+ if (r < 0)
+ return log_error_errno(r, "Failed to query PIN for security token '%s': %m", token_label);
+
+ STRV_FOREACH(i, passwords) {
+ rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i));
+ if (rv == CKR_OK) {
+ log_info("Successfully logged into security token '%s'.", token_label);
+ return 0;
+ }
+ if (rv == CKR_PIN_LOCKED)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "PIN has been locked, please reset PIN of security token '%s'.", token_label);
+ if (!IN_SET(rv, CKR_PIN_INCORRECT, CKR_PIN_LEN_RANGE))
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+ /* Referesh the token info, so that we can prompt knowing the new flags if they changed. */
+ rv = m->C_GetTokenInfo(slotid, &updated_token_info);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire updated security token information for slot %lu: %s", slotid, p11_kit_strerror(rv));
+
+ token_info = &updated_token_info;
+ log_notice("PIN for token '%s' is incorrect, please try again.", token_label);
+ }
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Too many attempts to log into token '%s'.", token_label);
+}
+
+static int token_find_private_key(
+ CK_FUNCTION_LIST *m,
+ CK_SESSION_HANDLE session,
+ P11KitUri *search_uri,
+ CK_OBJECT_HANDLE *ret_object) {
+
+ bool found_decrypt = false, found_class = false, found_key_type = false;
+ _cleanup_free_ CK_ATTRIBUTE *attributes_buffer = NULL;
+ CK_ULONG n_attributes, a, n_objects;
+ CK_ATTRIBUTE *attributes = NULL;
+ CK_OBJECT_HANDLE objects[2];
+ CK_RV rv, rv2;
+
+ assert(m);
+ assert(search_uri);
+ assert(ret_object);
+
+ attributes = p11_kit_uri_get_attributes(search_uri, &n_attributes);
+ for (a = 0; a < n_attributes; a++) {
+
+ /* We use the URI's included match attributes, but make them more strict. This allows users
+ * to specify a token URL instead of an object URL and the right thing should happen if
+ * there's only one suitable key on the token. */
+
+ switch (attributes[a].type) {
+
+ case CKA_CLASS: {
+ CK_OBJECT_CLASS c;
+
+ if (attributes[a].ulValueLen != sizeof(c))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_CLASS attribute size.");
+
+ memcpy(&c, attributes[a].pValue, sizeof(c));
+ if (c != CKO_PRIVATE_KEY)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not a private key, refusing.");
+
+ found_class = true;
+ break;
+ }
+
+ case CKA_DECRYPT: {
+ CK_BBOOL b;
+
+ if (attributes[a].ulValueLen != sizeof(b))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_DECRYPT attribute size.");
+
+ memcpy(&b, attributes[a].pValue, sizeof(b));
+ if (!b)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not suitable for decryption, refusing.");
+
+ found_decrypt = true;
+ break;
+ }
+
+ case CKA_KEY_TYPE: {
+ CK_KEY_TYPE t;
+
+ if (attributes[a].ulValueLen != sizeof(t))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_KEY_TYPE attribute size.");
+
+ memcpy(&t, attributes[a].pValue, sizeof(t));
+ if (t != CKK_RSA)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not an RSA key, refusing.");
+
+ found_key_type = true;
+ break;
+ }}
+ }
+
+ if (!found_decrypt || !found_class || !found_key_type) {
+ /* Hmm, let's slightly extend the attribute list we search for */
+
+ attributes_buffer = new(CK_ATTRIBUTE, n_attributes + !found_decrypt + !found_class + !found_key_type);
+ if (!attributes_buffer)
+ return log_oom();
+
+ memcpy(attributes_buffer, attributes, sizeof(CK_ATTRIBUTE) * n_attributes);
+
+ if (!found_decrypt) {
+ static const CK_BBOOL yes = true;
+
+ attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) {
+ .type = CKA_DECRYPT,
+ .pValue = (CK_BBOOL*) &yes,
+ .ulValueLen = sizeof(yes),
+ };
+ }
+
+ if (!found_class) {
+ static const CK_OBJECT_CLASS class = CKO_PRIVATE_KEY;
+
+ attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) {
+ .type = CKA_CLASS,
+ .pValue = (CK_OBJECT_CLASS*) &class,
+ .ulValueLen = sizeof(class),
+ };
+ }
+
+ if (!found_key_type) {
+ static const CK_KEY_TYPE type = CKK_RSA;
+
+ attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) {
+ .type = CKA_KEY_TYPE,
+ .pValue = (CK_KEY_TYPE*) &type,
+ .ulValueLen = sizeof(type),
+ };
+ }
+
+ attributes = attributes_buffer;
+ }
+
+ rv = m->C_FindObjectsInit(session, attributes, n_attributes);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize object find call: %s", p11_kit_strerror(rv));
+
+ rv = m->C_FindObjects(session, objects, ELEMENTSOF(objects), &n_objects);
+ rv2 = m->C_FindObjectsFinal(session);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to search objects: %s", p11_kit_strerror(rv));
+ if (rv2 != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to finalize object find call: %s", p11_kit_strerror(rv));
+ if (n_objects == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to find selected private key suitable for decryption on token.");
+ if (n_objects > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Configured private key URI matches multiple keys, refusing.");
+
+ *ret_object = objects[0];
+ return 0;
+}
+
+static int token_decrypt_our_key(
+ CK_FUNCTION_LIST *m,
+ CK_SESSION_HANDLE session,
+ CK_OBJECT_HANDLE object,
+ const char *token_label,
+ const void *encrypted_key,
+ size_t encrypted_key_size,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ static const CK_MECHANISM mechanism = {
+ .mechanism = CKM_RSA_PKCS
+ };
+ _cleanup_(erase_and_freep) CK_BYTE *dbuffer = NULL;
+ CK_ULONG dbuffer_size = 0;
+ CK_RV rv;
+
+ assert(m);
+ assert(encrypted_key);
+ assert(encrypted_key_size > 0);
+ assert(ret_decrypted_key);
+ assert(ret_decrypted_key_size);
+
+ rv = m->C_DecryptInit(session, (CK_MECHANISM*) &mechanism, object);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize decryption on security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+ dbuffer_size = encrypted_key_size; /* Start with something reasonable */
+ dbuffer = malloc(dbuffer_size);
+ if (!dbuffer)
+ return log_oom();
+
+ rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size);
+ if (rv == CKR_BUFFER_TOO_SMALL) {
+ erase_and_free(dbuffer);
+
+ dbuffer = malloc(dbuffer_size);
+ if (!dbuffer)
+ return log_oom();
+
+ rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size);
+ }
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to decrypt key on security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+ log_info("Successfully decrypted key with security token '%s'.", token_label);
+
+ *ret_decrypted_key = TAKE_PTR(dbuffer);
+ *ret_decrypted_key_size = dbuffer_size;
+ return 0;
+}
+
+static int token_process(
+ const char *friendly_name,
+ CK_FUNCTION_LIST *m,
+ CK_SLOT_ID slotid,
+ const CK_TOKEN_INFO *token_info,
+ const char *token_uri_string,
+ P11KitUri *search_uri,
+ const void *encrypted_key,
+ size_t encrypted_key_size,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ _cleanup_free_ char *token_label = NULL;
+ CK_SESSION_HANDLE session;
+ CK_OBJECT_HANDLE object;
+ CK_RV rv;
+ int r;
+
+ assert(friendly_name);
+ assert(m);
+ assert(token_info);
+ assert(token_uri_string);
+ assert(search_uri);
+ assert(encrypted_key);
+ assert(encrypted_key_size > 0);
+ assert(ret_decrypted_key);
+ assert(ret_decrypted_key_size);
+
+ /* The label is not NUL terminated and likely padded with spaces, let's make a copy here, so that we can strip that. */
+ token_label = strndup((char*) token_info->label, sizeof(token_info->label));
+ if (!token_label)
+ return log_oom();
+
+ strstrip(token_label);
+
+ rv = m->C_OpenSession(slotid, CKF_SERIAL_SESSION, NULL, NULL, &session);
+ if (rv != CKR_OK)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create session for security token '%s': %s", token_label, p11_kit_strerror(rv));
+
+ r = token_login(friendly_name, m, session, slotid, token_info, token_uri_string, token_label, until);
+ if (r < 0)
+ goto finish;
+
+ r = token_find_private_key(m, session, search_uri, &object);
+ if (r < 0)
+ goto finish;
+
+ r = token_decrypt_our_key(m, session, object, token_label, encrypted_key, encrypted_key_size, ret_decrypted_key, ret_decrypted_key_size);
+ if (r < 0)
+ goto finish;
+
+ r = 1;
+
+finish:
+ rv = m->C_CloseSession(session);
+ if (rv != CKR_OK)
+ log_warning_errno(SYNTHETIC_ERRNO(rv), "Failed to close session on PKCS#11 token, ignoring: %s", p11_kit_strerror(rv));
+
+ return r;
+}
+
+static int uri_from_string(const char *p, P11KitUri **ret) {
+ _cleanup_(p11_kit_uri_freep) P11KitUri *uri = NULL;
+
+ assert(p);
+ assert(ret);
+
+ uri = p11_kit_uri_new();
+ if (!uri)
+ return -ENOMEM;
+
+ if (p11_kit_uri_parse(p, P11_KIT_URI_FOR_ANY, uri) != P11_KIT_URI_OK)
+ return -EINVAL;
+
+ *ret = TAKE_PTR(uri);
+ return 0;
+}
+
+static P11KitUri *uri_from_module_info(const CK_INFO *info) {
+ P11KitUri *uri;
+
+ assert(info);
+
+ uri = p11_kit_uri_new();
+ if (!uri)
+ return NULL;
+
+ *p11_kit_uri_get_module_info(uri) = *info;
+ return uri;
+}
+
+static P11KitUri *uri_from_slot_info(const CK_SLOT_INFO *slot_info) {
+ P11KitUri *uri;
+
+ assert(slot_info);
+
+ uri = p11_kit_uri_new();
+ if (!uri)
+ return NULL;
+
+ *p11_kit_uri_get_slot_info(uri) = *slot_info;
+ return uri;
+}
+
+static P11KitUri *uri_from_token_info(const CK_TOKEN_INFO *token_info) {
+ P11KitUri *uri;
+
+ assert(token_info);
+
+ uri = p11_kit_uri_new();
+ if (!uri)
+ return NULL;
+
+ *p11_kit_uri_get_token_info(uri) = *token_info;
+ return uri;
+}
+
+static int slot_process(
+ const char *friendly_name,
+ CK_FUNCTION_LIST *m,
+ CK_SLOT_ID slotid,
+ P11KitUri *search_uri,
+ const void *encrypted_key,
+ size_t encrypted_key_size,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ _cleanup_(p11_kit_uri_freep) P11KitUri* slot_uri = NULL, *token_uri = NULL;
+ _cleanup_free_ char *token_uri_string = NULL;
+ CK_TOKEN_INFO token_info;
+ CK_SLOT_INFO slot_info;
+ int uri_result;
+ CK_RV rv;
+
+ assert(friendly_name);
+ assert(m);
+ assert(search_uri);
+ assert(encrypted_key);
+ assert(encrypted_key_size > 0);
+ assert(ret_decrypted_key);
+ assert(ret_decrypted_key_size);
+
+ /* We return -EAGAIN for all failures we can attribute to a specific slot in some way, so that the
+ * caller might try other slots before giving up. */
+
+ rv = m->C_GetSlotInfo(slotid, &slot_info);
+ if (rv != CKR_OK) {
+ log_warning("Failed to acquire slot info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv));
+ return -EAGAIN;
+ }
+
+ slot_uri = uri_from_slot_info(&slot_info);
+ if (!slot_uri)
+ return log_oom();
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *slot_uri_string = NULL;
+
+ uri_result = p11_kit_uri_format(slot_uri, P11_KIT_URI_FOR_ANY, &slot_uri_string);
+ if (uri_result != P11_KIT_URI_OK) {
+ log_warning("Failed to format slot URI, ignoring slot: %s", p11_kit_uri_message(uri_result));
+ return -EAGAIN;
+ }
+
+ log_debug("Found slot with URI %s", slot_uri_string);
+ }
+
+ rv = m->C_GetTokenInfo(slotid, &token_info);
+ if (rv == CKR_TOKEN_NOT_PRESENT) {
+ log_debug("Token not present in slot, ignoring.");
+ return -EAGAIN;
+ } else if (rv != CKR_OK) {
+ log_warning("Failed to acquire token info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv));
+ return -EAGAIN;
+ }
+
+ token_uri = uri_from_token_info(&token_info);
+ if (!token_uri)
+ return log_oom();
+
+ uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string);
+ if (uri_result != P11_KIT_URI_OK) {
+ log_warning("Failed to format slot URI: %s", p11_kit_uri_message(uri_result));
+ return -EAGAIN;
+ }
+
+ if (!p11_kit_uri_match_token_info(search_uri, &token_info)) {
+ log_debug("Found non-matching token with URI %s.", token_uri_string);
+ return -EAGAIN;
+ }
+
+ log_debug("Found matching token with URI %s.", token_uri_string);
+
+ return token_process(
+ friendly_name,
+ m,
+ slotid,
+ &token_info,
+ token_uri_string,
+ search_uri,
+ encrypted_key, encrypted_key_size,
+ until,
+ ret_decrypted_key, ret_decrypted_key_size);
+}
+
+static int module_process(
+ const char *friendly_name,
+ CK_FUNCTION_LIST *m,
+ P11KitUri *search_uri,
+ const void *encrypted_key,
+ size_t encrypted_key_size,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ _cleanup_free_ char *name = NULL, *module_uri_string = NULL;
+ _cleanup_(p11_kit_uri_freep) P11KitUri* module_uri = NULL;
+ _cleanup_free_ CK_SLOT_ID *slotids = NULL;
+ CK_ULONG n_slotids = 0;
+ int uri_result;
+ CK_INFO info;
+ size_t k;
+ CK_RV rv;
+ int r;
+
+ /* We ignore most errors from modules here, in order to skip over faulty modules: one faulty module
+ * should not have the effect that we don't try the others anymore. We indicate such per-module
+ * failures with -EAGAIN, which let's the caller try the next module. */
+
+ name = p11_kit_module_get_name(m);
+ if (!name)
+ return log_oom();
+
+ log_debug("Trying PKCS#11 module %s.", name);
+
+ rv = m->C_GetInfo(&info);
+ if (rv != CKR_OK) {
+ log_warning("Failed to get info on PKCS#11 module, ignoring module: %s", p11_kit_strerror(rv));
+ return -EAGAIN;
+ }
+
+ module_uri = uri_from_module_info(&info);
+ if (!module_uri)
+ return log_oom();
+
+ uri_result = p11_kit_uri_format(module_uri, P11_KIT_URI_FOR_ANY, &module_uri_string);
+ if (uri_result != P11_KIT_URI_OK) {
+ log_warning("Failed to format module URI, ignoring module: %s", p11_kit_uri_message(uri_result));
+ return -EAGAIN;
+ }
+
+ log_debug("Found module with URI %s", module_uri_string);
+
+ for (unsigned tries = 0; tries < 16; tries++) {
+ slotids = mfree(slotids);
+ n_slotids = 0;
+
+ rv = m->C_GetSlotList(0, NULL, &n_slotids);
+ if (rv != CKR_OK) {
+ log_warning("Failed to get slot list size, ignoring module: %s", p11_kit_strerror(rv));
+ n_slotids = 0;
+ break;
+ }
+ if (n_slotids == 0) {
+ log_debug("This module has no slots? Ignoring module.");
+ break;
+ }
+
+ slotids = new(CK_SLOT_ID, n_slotids);
+ if (!slotids)
+ return log_oom();
+
+ rv = m->C_GetSlotList(0, slotids, &n_slotids);
+ if (rv == CKR_OK)
+ break;
+ n_slotids = 0;
+ if (rv != CKR_BUFFER_TOO_SMALL) {
+ log_warning("Failed to acquire slot list, ignoring module: %s", p11_kit_strerror(rv));
+ break;
+ }
+
+ /* Hu? Maybe somebody plugged something in and things changed? Let's try again */
+ }
+
+ if (n_slotids == 0)
+ return -EAGAIN;
+
+ for (k = 0; k < n_slotids; k++) {
+ r = slot_process(
+ friendly_name,
+ m,
+ slotids[k],
+ search_uri,
+ encrypted_key, encrypted_key_size,
+ until,
+ ret_decrypted_key, ret_decrypted_key_size);
+ if (r != -EAGAIN)
+ return r;
+ }
+
+ return -EAGAIN;
+}
+
+static int load_key_file(
+ const char *key_file,
+ size_t key_file_size,
+ uint64_t key_file_offset,
+ void **ret_encrypted_key,
+ size_t *ret_encrypted_key_size) {
+
+ _cleanup_(erase_and_freep) char *buffer = NULL;
+ _cleanup_close_ int fd = -1;
+ ssize_t n;
+ int r;
+
+ assert(key_file);
+ assert(ret_encrypted_key);
+ assert(ret_encrypted_key_size);
+
+ fd = open(key_file, O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to load encrypted PKCS#11 key: %m");
+
+ if (key_file_size == 0) {
+ struct stat st;
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat key file: %m");
+
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_error_errno(r, "Key file is not a regular file: %m");
+
+ if (st.st_size == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file is empty, refusing.");
+ if ((uint64_t) st.st_size > SIZE_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Key file too large, refsing.");
+
+ if (key_file_offset >= (uint64_t) st.st_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file offset too large for file, refusing.");
+
+ key_file_size = st.st_size - key_file_offset;
+ }
+
+ buffer = malloc(key_file_size);
+ if (!buffer)
+ return log_oom();
+
+ if (key_file_offset > 0)
+ n = pread(fd, buffer, key_file_size, key_file_offset);
+ else
+ n = read(fd, buffer, key_file_size);
+ if (n < 0)
+ return log_error_errno(errno, "Failed to read PKCS#11 key file: %m");
+ if (n == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty encrypted key found, refusing.");
+
+ *ret_encrypted_key = TAKE_PTR(buffer);
+ *ret_encrypted_key_size = (size_t) n;
+
+ return 0;
+}
+
+int acquire_pkcs11_key(
+ const char *friendly_name,
+ const char *pkcs11_uri,
+ const char *key_file,
+ size_t key_file_size,
+ uint64_t key_file_offset,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ _cleanup_(p11_kit_modules_finalize_and_releasep) CK_FUNCTION_LIST **modules = NULL;
+ _cleanup_(p11_kit_uri_freep) P11KitUri *search_uri = NULL;
+ _cleanup_(erase_and_freep) void *encrypted_key = NULL;
+ size_t encrypted_key_size;
+ int r;
+
+ assert(friendly_name);
+ assert(pkcs11_uri);
+ assert(key_file);
+ assert(ret_decrypted_key);
+ assert(ret_decrypted_key_size);
+
+ r = load_key_file(key_file, key_file_size, key_file_offset, &encrypted_key, &encrypted_key_size);
+ if (r < 0)
+ return r;
+
+ r = uri_from_string(pkcs11_uri, &search_uri);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse PKCS#11 URI '%s': %m", pkcs11_uri);
+
+ modules = p11_kit_modules_load_and_initialize(0);
+ if (!modules)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize pkcs11 modules");
+
+ for (CK_FUNCTION_LIST **i = modules; *i; i++) {
+ r = module_process(
+ friendly_name,
+ *i,
+ search_uri,
+ encrypted_key,
+ encrypted_key_size,
+ until,
+ ret_decrypted_key,
+ ret_decrypted_key_size);
+ if (r != -EAGAIN)
+ return r;
+ }
+
+ return -EAGAIN;
+}
diff --git a/src/cryptsetup/cryptsetup-pkcs11.h b/src/cryptsetup/cryptsetup-pkcs11.h
new file mode 100644
index 0000000000000..f207b6c749645
--- /dev/null
+++ b/src/cryptsetup/cryptsetup-pkcs11.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "time-util.h"
+
+#if HAVE_P11KIT
+
+int acquire_pkcs11_key(
+ const char *friendly_name,
+ const char *pkcs11_uri,
+ const char *key_file,
+ size_t key_file_size,
+ uint64_t key_file_offset,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size);
+
+#else
+
+static inline int acquire_pkcs11_key(
+ const char *friendly_name,
+ const char *pkcs11_uri,
+ const char *key_file,
+ size_t key_file_size,
+ uint64_t key_file_offset,
+ usec_t until,
+ void **ret_decrypted_key,
+ size_t *ret_decrypted_key_size) {
+
+ return -EOPNOTSUPP;
+}
+
+#endif
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c
index 78732a0a577f9..f00fdb71e91b6 100644
--- a/src/cryptsetup/cryptsetup.c
+++ b/src/cryptsetup/cryptsetup.c
@@ -13,6 +13,7 @@
#include "alloc-util.h"
#include "ask-password-api.h"
#include "crypt-util.h"
+#include "cryptsetup-pkcs11.h"
#include "device-util.h"
#include "escape.h"
#include "fileio.h"
@@ -58,11 +59,13 @@ static char **arg_tcrypt_keyfiles = NULL;
static uint64_t arg_offset = 0;
static uint64_t arg_skip = 0;
static usec_t arg_timeout = USEC_INFINITY;
+static char *arg_pkcs11_uri = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep);
STATIC_DESTRUCTOR_REGISTER(arg_hash, freep);
STATIC_DESTRUCTOR_REGISTER(arg_header, freep);
STATIC_DESTRUCTOR_REGISTER(arg_tcrypt_keyfiles, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep);
/* Options Debian's crypttab knows we don't:
@@ -251,7 +254,16 @@ static int parse_one_option(const char *option) {
if (r < 0)
return log_error_errno(r, "Failed to parse %s: %m", option);
- } else if (!streq(option, "none"))
+ } else if ((val = startswith(option, "pkcs11-uri="))) {
+
+ if (!startswith(val, "pkcs11:"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing");
+
+ r = free_and_strdup(&arg_pkcs11_uri, val);
+ if (r < 0)
+ return log_oom();
+
+ } else
log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option);
return 0;
@@ -276,10 +288,10 @@ static int parse_options(const char *options) {
}
/* sanity-check options */
- if (arg_type != NULL && !streq(arg_type, CRYPT_PLAIN)) {
- if (arg_offset)
+ if (arg_type && !streq(arg_type, CRYPT_PLAIN)) {
+ if (arg_offset != 0)
log_warning("offset= ignored with type %s", arg_type);
- if (arg_skip)
+ if (arg_skip != 0)
log_warning("skip= ignored with type %s", arg_type);
}
@@ -337,28 +349,19 @@ static char *disk_mount_point(const char *label) {
return NULL;
}
-static int get_password(const char *vol, const char *src, usec_t until, bool accept_cached, char ***ret) {
- _cleanup_free_ char *description = NULL, *name_buffer = NULL, *mount_point = NULL, *text = NULL, *disk_path = NULL;
- _cleanup_strv_free_erase_ char **passwords = NULL;
- const char *name = NULL;
- char **p, *id;
- int r = 0;
+static char *friendly_disk_name(const char *src, const char *vol) {
+ _cleanup_free_ char *description = NULL, *mount_point = NULL;
+ char *name_buffer = NULL;
+ int r;
- assert(vol);
assert(src);
- assert(ret);
+ assert(vol);
description = disk_description(src);
mount_point = disk_mount_point(vol);
- disk_path = cescape(src);
- if (!disk_path)
- return log_oom();
-
+ /* If the description string is simply the volume name, then let's not show this twice */
if (description && streq(vol, description))
- /* If the description string is simply the
- * volume name, then let's not show this
- * twice */
description = mfree(description);
if (mount_point && description)
@@ -367,13 +370,39 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc
r = asprintf(&name_buffer, "%s on %s", vol, mount_point);
else if (description)
r = asprintf(&name_buffer, "%s (%s)", description, vol);
-
+ else
+ return strdup(vol);
if (r < 0)
+ return NULL;
+
+ return name_buffer;
+}
+
+static int get_password(
+ const char *vol,
+ const char *src,
+ usec_t until,
+ bool accept_cached,
+ char ***ret) {
+
+ _cleanup_free_ char *friendly = NULL, *text = NULL, *disk_path = NULL;
+ _cleanup_strv_free_erase_ char **passwords = NULL;
+ char **p, *id;
+ int r = 0;
+
+ assert(vol);
+ assert(src);
+ assert(ret);
+
+ friendly = friendly_disk_name(src, vol);
+ if (!friendly)
return log_oom();
- name = name_buffer ? name_buffer : vol;
+ if (asprintf(&text, "Please enter passphrase for disk %s:", friendly) < 0)
+ return log_oom();
- if (asprintf(&text, "Please enter passphrase for disk %s:", name) < 0)
+ disk_path = cescape(src);
+ if (!disk_path)
return log_oom();
id = strjoina("cryptsetup:", disk_path);
@@ -389,7 +418,7 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc
assert(strv_length(passwords) == 1);
- if (asprintf(&text, "Please enter passphrase for disk %s (verification):", name) < 0)
+ if (asprintf(&text, "Please enter passphrase for disk %s (verification):", friendly) < 0)
return log_oom();
id = strjoina("cryptsetup-verification:", disk_path);
@@ -447,6 +476,11 @@ static int attach_tcrypt(
assert(name);
assert(key_file || (passwords && passwords[0]));
+ if (arg_pkcs11_uri) {
+ log_error("Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11 support.");
+ return -EAGAIN; /* Ask for a regular password */
+ }
+
if (arg_tcrypt_hidden)
params.flags |= CRYPT_TCRYPT_HIDDEN_HEADER;
@@ -487,17 +521,19 @@ static int attach_tcrypt(
return 0;
}
-static int attach_luks_or_plain(struct crypt_device *cd,
- const char *name,
- const char *key_file,
- char **passwords,
- uint32_t flags) {
+static int attach_luks_or_plain(
+ struct crypt_device *cd,
+ const char *name,
+ const char *key_file,
+ char **passwords,
+ uint32_t flags,
+ usec_t until) {
+
int r = 0;
bool pass_volume_key = false;
assert(cd);
assert(name);
- assert(key_file || passwords);
if ((!arg_type && !crypt_get_type(cd)) || streq_ptr(arg_type, CRYPT_PLAIN)) {
struct crypt_params_plain params = {
@@ -553,7 +589,39 @@ static int attach_luks_or_plain(struct crypt_device *cd,
crypt_get_volume_key_size(cd)*8,
crypt_get_device_name(cd));
- if (key_file) {
+ if (arg_pkcs11_uri) {
+ _cleanup_free_ void *decrypted_key = NULL, *friendly = NULL;
+ size_t decrypted_key_size = 0;
+
+ if (!key_file)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing.");
+
+ friendly = friendly_disk_name(crypt_get_device_name(cd), name);
+ if (!friendly)
+ return log_oom();
+
+ r = acquire_pkcs11_key(
+ friendly,
+ arg_pkcs11_uri,
+ key_file,
+ arg_keyfile_size, arg_keyfile_offset,
+ until,
+ &decrypted_key, &decrypted_key_size);
+ if (r < 0)
+ return r;
+
+ if (pass_volume_key)
+ r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags);
+ else
+ r = crypt_activate_by_passphrase(cd, name, arg_key_slot, decrypted_key, decrypted_key_size, flags);
+ if (r == -EPERM) {
+ log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)");
+ return -EAGAIN; /* log actual error, but return EAGAIN */
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m");
+
+ } else if (key_file) {
r = crypt_activate_by_keyfile_offset(cd, name, arg_key_slot, key_file, arg_keyfile_size, arg_keyfile_offset, flags);
if (r == -EPERM) {
log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file);
@@ -565,6 +633,7 @@ static int attach_luks_or_plain(struct crypt_device *cd,
}
if (r < 0)
return log_error_errno(r, "Failed to activate with key file '%s': %m", key_file);
+
} else {
char **p;
@@ -659,25 +728,21 @@ static int run(int argc, char *argv[]) {
if (argc < 4)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "attach requires at least two arguments.");
- if (argc >= 5 &&
- argv[4][0] &&
- !streq(argv[4], "-") &&
- !streq(argv[4], "none")) {
-
- if (!path_is_absolute(argv[4]))
- log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]);
- else
+ if (argc >= 5 && !STR_IN_SET(argv[4], "", "-", "none")) {
+ if (path_is_absolute(argv[4]))
key_file = argv[4];
+ else
+ log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]);
}
- if (argc >= 6 && argv[5][0] && !streq(argv[5], "-")) {
+ if (argc >= 6 && !STR_IN_SET(argv[5], "", "-", "none")) {
r = parse_options(argv[5]);
if (r < 0)
return r;
}
/* A delicious drop of snake oil */
- mlockall(MCL_FUTURE);
+ (void) mlockall(MCL_FUTURE);
if (arg_header) {
log_debug("LUKS header: %s", arg_header);
@@ -740,7 +805,7 @@ static int run(int argc, char *argv[]) {
for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) {
_cleanup_strv_free_erase_ char **passwords = NULL;
- if (!key_file) {
+ if (!key_file && !arg_pkcs11_uri) {
r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords);
if (r == -EAGAIN)
continue;
@@ -751,11 +816,7 @@ static int run(int argc, char *argv[]) {
if (streq_ptr(arg_type, CRYPT_TCRYPT))
r = attach_tcrypt(cd, argv[2], key_file, passwords, flags);
else
- r = attach_luks_or_plain(cd,
- argv[2],
- key_file,
- passwords,
- flags);
+ r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags, until);
if (r >= 0)
break;
if (r != -EAGAIN)
@@ -763,6 +824,7 @@ static int run(int argc, char *argv[]) {
/* Passphrase not correct? Let's try again! */
key_file = NULL;
+ arg_pkcs11_uri = NULL;
}
if (arg_tries != 0 && tries >= arg_tries)
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index 50de0afce6f00..c1be6c034c561 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -1,8 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include
-#include
#include
+#include
+#include
#include "architecture.h"
#include "dissect-image.h"
@@ -171,7 +172,7 @@ static int run(int argc, char *argv[]) {
if (r <= 0)
return r;
- r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, &d);
+ r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &d);
if (r < 0)
return log_error_errno(r, "Failed to set up loopback device: %m");
diff --git a/src/fuzz/fuzz-json.c b/src/fuzz/fuzz-json.c
index ce7b69dbb99f6..c01e2a570c50a 100644
--- a/src/fuzz/fuzz-json.c
+++ b/src/fuzz/fuzz-json.c
@@ -18,7 +18,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
f = fmemopen_unlocked((char*) data, size, "re");
assert_se(f);
- if (json_parse_file(f, NULL, &v, NULL, NULL) < 0)
+ if (json_parse_file(f, NULL, 0, &v, NULL, NULL) < 0)
return 0;
g = open_memstream_unlocked(&out, &out_size);
diff --git a/src/home/home-util.c b/src/home/home-util.c
new file mode 100644
index 0000000000000..37f614425bc05
--- /dev/null
+++ b/src/home/home-util.c
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#if HAVE_CRYPT_H
+#include
+#endif
+
+#include "dns-domain.h"
+#include "errno-util.h"
+#include "home-util.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+bool suitable_user_name(const char *name) {
+
+ /* Checks whether the specified name is suitable for management via homed. Note that our client side
+ * usually validate susing a simple valid_user_group_name(), while server side we are a bit more
+ * restrictive, so that we can change the rules server side without having to update things client
+ * side, too. */
+
+ if (!valid_user_group_name(name))
+ return false;
+
+ /* We generally rely on NSS to tell us which users not to care for, but let's filter out some
+ * particularly well-known users. */
+ if (STR_IN_SET(name,
+ "root",
+ "nobody",
+ NOBODY_USER_NAME, NOBODY_GROUP_NAME))
+ return false;
+
+ /* Let's also defend our own namespace, as well as Debian's (unwritten?) logic of prefixing system
+ * users with underscores. */
+ if (STARTSWITH_SET(name, "systemd-", "_"))
+ return false;
+
+ return true;
+}
+
+int suitable_realm(const char *realm) {
+ _cleanup_free_ char *normalized = NULL;
+ int r;
+
+ /* Similar to the above: let's validate the realm a bit stricter server-side than client side */
+
+ r = dns_name_normalize(realm, 0, &normalized); /* this also checks general validity */
+ if (r == -EINVAL)
+ return 0;
+ if (r < 0)
+ return r;
+
+ if (!streq(realm, normalized)) /* is this normalized? */
+ return false;
+
+ if (dns_name_is_root(realm) || dns_name_is_single_label(realm)) /* Don't allow top level domain nor single label domains */
+ return false;
+
+ return true;
+}
+
+int suitable_image_path(const char *path) {
+
+ return !empty_or_root(path) &&
+ path_is_valid(path) &&
+ path_is_absolute(path);
+}
+
+int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm) {
+ _cleanup_free_ char *un = NULL, *rr = NULL;
+ const char *c;
+ int r;
+
+ assert(t);
+ assert(ret_user_name);
+ assert(ret_realm);
+
+ c = strchr(t, '@');
+ if (!c) {
+ if (!suitable_user_name(t))
+ return -EINVAL;
+
+ un = strdup(t);
+ if (!un)
+ return -ENOMEM;
+ } else {
+ un = strndup(t, c - t);
+ if (!un)
+ return -ENOMEM;
+
+ if (!suitable_user_name(un))
+ return -EINVAL;
+
+ r = suitable_realm(c + 1);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EINVAL;
+
+ rr = strdup(c + 1);
+ if (!rr)
+ return -ENOMEM;
+ }
+
+ *ret_user_name = TAKE_PTR(un);
+ *ret_realm = TAKE_PTR(rr);
+
+ return 0;
+}
+
+int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) {
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+ JsonVariant *v;
+ int r;
+
+ assert(m);
+ assert(secret);
+
+ if (!FLAGS_SET(secret->mask, USER_RECORD_SECRET))
+ return -EINVAL;
+
+ v = json_variant_by_key(secret->json, "secret");
+ if (!v)
+ return -EINVAL;
+
+ r = json_variant_format(v, 0, &formatted);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_append(m, "s", formatted);
+}
+
+int test_password(char **hashed_password, const char *password) {
+ char **hpw;
+
+ STRV_FOREACH(hpw, hashed_password) {
+ struct crypt_data cc = {};
+ const char *k;
+ bool b;
+
+ errno = 0;
+ k = crypt_r(password, *hpw, &cc);
+ if (!k) {
+ explicit_bzero_safe(&cc, sizeof(cc));
+ return errno_or_else(EINVAL);
+ }
+
+ b = streq(k, *hpw);
+ explicit_bzero_safe(&cc, sizeof(cc));
+
+ if (b)
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/home/home-util.h b/src/home/home-util.h
new file mode 100644
index 0000000000000..fea46d5e2e6a6
--- /dev/null
+++ b/src/home/home-util.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "sd-bus.h"
+
+#include "time-util.h"
+#include "user-record.h"
+
+bool suitable_user_name(const char *name);
+int suitable_realm(const char *realm);
+int suitable_image_path(const char *path);
+
+int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm);
+
+int bus_message_append_secret(sd_bus_message *m, UserRecord *secret);
+
+/* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these
+ * operations permit a *very* long time-out */
+#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
+
+int test_password(char **hashed_password, const char *password);
diff --git a/src/home/homectl.c b/src/home/homectl.c
new file mode 100644
index 0000000000000..263eae3087272
--- /dev/null
+++ b/src/home/homectl.c
@@ -0,0 +1,2998 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "ask-password-api.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "cgroup-util.h"
+#include "dns-domain.h"
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "home-util.h"
+#include "locale-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "pwquality-util.h"
+#include "rlimit-util.h"
+#include "spawn-polkit-agent.h"
+#include "terminal-util.h"
+#include "user-record-show.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "verbs.h"
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_ask_password = true;
+static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+static const char *arg_host = NULL;
+static const char *arg_identity = NULL;
+static JsonVariant *arg_identity_extra = NULL;
+static JsonVariant *arg_identity_extra_privileged = NULL;
+static JsonVariant *arg_identity_extra_this_machine = NULL;
+static JsonVariant *arg_identity_extra_rlimits = NULL;
+static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */
+static char **arg_identity_filter_rlimits = NULL;
+static uint64_t arg_disk_size = UINT64_MAX;
+static uint64_t arg_disk_size_relative = UINT64_MAX;
+static bool arg_json = false;
+static JsonFormatFlags arg_json_format_flags = 0;
+static enum {
+ EXPORT_FORMAT_FULL, /* export the full record */
+ EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */
+ EXPORT_FORMAT_MINIMAL, /* also strip signature */
+} arg_export_format = EXPORT_FORMAT_FULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
+
+static int acquire_bus(sd_bus **bus) {
+ int r;
+
+ assert(bus);
+
+ if (*bus)
+ return 0;
+
+ r = bus_connect_transport(arg_transport, arg_host, false, bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to bus: %m");
+
+ (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password);
+
+ return 0;
+}
+
+static int list_homes(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ListHomes",
+ &error,
+ &reply,
+ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r));
+
+ table = table_new("name", "uid", "gid", "state", "realname", "home", "shell");
+ if (!table)
+ return log_oom();
+
+ r = sd_bus_message_enter_container(reply, 'a', "(susussso)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ for (;;) {
+ const char *name, *state, *realname, *home, *shell, *color;
+ TableCell *cell;
+ uint32_t uid, gid;
+
+ r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
+
+ r = table_add_many(table,
+ TABLE_STRING, name,
+ TABLE_UID, uid,
+ TABLE_GID, gid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+
+ r = table_add_cell(table, &cell, TABLE_STRING, state);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add field to table: %m");
+
+ color = user_record_state_color(state);
+ if (color)
+ (void) table_set_color(table, cell, color);
+
+ r = table_add_many(table,
+ TABLE_STRING, strna(empty_to_null(realname)),
+ TABLE_STRING, home,
+ TABLE_STRING, strna(empty_to_null(shell)));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+ }
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (table_get_rows(table) > 1 || arg_json) {
+ r = table_set_sort(table, (size_t) 0, (size_t) -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to sort table: %m");
+
+ table_set_header(table, arg_legend);
+
+ if (arg_json)
+ r = table_print_json(table, stdout, arg_json_format_flags);
+ else
+ r = table_print(table, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show table: %m");
+ }
+
+ if (arg_legend && !arg_json) {
+ if (table_get_rows(table) > 1)
+ printf("\n%zu homes listed.\n", table_get_rows(table) - 1);
+ else
+ printf("No homes.\n");
+ }
+
+ return 0;
+}
+
+static int acquire_existing_password(const char *user_name, UserRecord *hr) {
+ _cleanup_(strv_free_erasep) char **password = NULL;
+ _cleanup_free_ char *question = NULL;
+ char *e;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ e = getenv("PASSWORD");
+ if (e) {
+ /* People really shouldn't use environment variables for passing passwords. We support this
+ * only for testing purposes, and do not document the behaviour, so that people won't
+ * actually use this outside of testing. */
+
+ r = user_record_set_password(hr, STRV_MAKE(e), true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ string_erase(e);
+
+ if (unsetenv("PASSWORD") < 0)
+ return log_error_errno(errno, "Failed to unset $PASSWORD: %m");
+
+ return 0;
+ }
+
+ if (asprintf(&question, "Please enter password for user %s:", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &password);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ r = user_record_set_password(hr, password, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ return 0;
+}
+
+static int activate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = acquire_existing_password(*i, secret);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ActivateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ 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_BAD_PASSWORD))
+ log_error("Password incorrect, please try again.");
+ else {
+ log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int deactivate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "DeactivateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ 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) {
+ log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static void dump_home_record(UserRecord *hr) {
+ int r;
+
+ assert(hr);
+
+ if (hr->incomplete) {
+ fflush(stdout);
+ log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name);
+ }
+
+ if (arg_json) {
+ _cleanup_(user_record_unrefp) UserRecord *stripped = NULL;
+
+ if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped);
+ else if (arg_export_format == EXPORT_FORMAT_MINIMAL)
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped);
+ else
+ r = 0;
+ if (r < 0)
+ log_warning_errno(r, "Failed to strip user record, ignoring: %m");
+ if (stripped)
+ hr = stripped;
+
+ json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL);
+ } else
+ user_record_show(hr, true);
+}
+
+static char **mangle_user_list(char **list, char ***ret_allocated) {
+ _cleanup_free_ char *myself = NULL;
+ char **l;
+
+ if (!strv_isempty(list)) {
+ *ret_allocated = NULL;
+ return list;
+ }
+
+ myself = getusername_malloc();
+ if (!myself)
+ return NULL;
+
+ l = new(char*, 2);
+ if (!l)
+ return NULL;
+
+ l[0] = TAKE_PTR(myself);
+ l[1] = NULL;
+
+ *ret_allocated = l;
+ return l;
+}
+
+static int inspect_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(strv_freep) char **mangled_list = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **items, **i;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
+ if (!items)
+ return log_oom();
+
+ STRV_FOREACH(i, items) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ const char *json;
+ int incomplete;
+ uid_t uid;
+
+ r = parse_uid(*i, &uid);
+ if (r < 0) {
+ if (!valid_user_group_name(*i)) {
+ log_error("Invalid user name '%s'.", *i);
+ if (ret == EXIT_SUCCESS)
+ ret = -EINVAL;
+
+ continue;
+ }
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetUserRecordByName",
+ &error,
+ &reply,
+ "s",
+ *i);
+ } else {
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetUserRecordByUID",
+ &error,
+ &reply,
+ "u",
+ (uint32_t) uid);
+ }
+
+ if (r < 0) {
+ log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ continue;
+ }
+
+ r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL);
+ if (r < 0) {
+ bus_log_parse_error(r);
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ continue;
+ }
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse JSON identity: %m");
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ continue;
+ }
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG);
+ if (r < 0) {
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ continue;
+ }
+
+ hr->incomplete = incomplete;
+ dump_home_record(hr);
+ }
+
+ return ret;
+}
+
+static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ const char *json;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ if (!valid_user_group_name(argv[1]))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name '%s'.", argv[1]);
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetUserRecordByName",
+ &error,
+ &reply,
+ "s",
+ argv[1]);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) ||
+ sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) {
+ log_debug_errno(r, "systemd-homed is not available: %s", bus_error_message(&error, r));
+ return EXIT_SUCCESS;
+ }
+
+ if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) {
+ log_debug_errno(r, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r));
+ return EXIT_SUCCESS;
+ }
+
+ return log_error_errno(r, "Failed to query user record for %s: %m", argv[1]);
+ }
+
+ r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON record: %m");
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ if (strv_isempty(hr->ssh_authorized_keys))
+ log_debug("User record for %s has no public SSH keys.", argv[1]);
+ else {
+ char **i;
+
+ STRV_FOREACH(i, hr->ssh_authorized_keys)
+ printf("%s\n", *i);
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int authenticate_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(strv_freep) char **mangled_list = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i, **items;
+
+ items = mangle_user_list(strv_skip(argv, 1), &mangled_list);
+ if (!items)
+ return log_oom();
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ STRV_FOREACH(i, items) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = acquire_existing_password(*i, secret);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "AuthenticateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ 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_BAD_PASSWORD))
+ log_error("Password incorrect, please try again.");
+ else {
+ log_error_errno(r, "Failed to authenticate user home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int update_last_change(JsonVariant **v, bool override) {
+ JsonVariant *c;
+ usec_t n;
+ int r;
+
+ assert(v);
+
+ n = now(CLOCK_REALTIME);
+
+ c = json_variant_by_key(*v, "lastChangeUSec");
+ if (c) {
+ uintmax_t u;
+
+ if (!override)
+ return 0;
+
+ if (!json_variant_is_unsigned(c))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing.");
+
+ u = json_variant_unsigned(c);
+ if (u >= n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update.");
+ }
+
+ r = json_variant_set_field_unsigned(v, "lastChangeUSec", n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge identities: %m");
+
+ return 1;
+}
+
+static int apply_identity_changes(JsonVariant **_v) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ assert(_v);
+
+ v = json_variant_ref(*_v);
+
+ r = json_variant_filter(&v, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter identity: %m");
+
+ r = json_variant_merge(&v, arg_identity_extra);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge identities: %m");
+
+ if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) {
+ _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL;
+ char mids[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire machine ID: %m");
+
+ r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids));
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate matchMachineId object: %m");
+
+ per_machine = json_variant_ref(json_variant_by_key(v, "perMachine"));
+ if (per_machine) {
+ _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ JsonVariant *z;
+ size_t i = 0;
+
+ if (!json_variant_is_array(per_machine))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing.");
+
+ array = new(JsonVariant*, json_variant_elements(per_machine) + 1);
+ if (!array)
+ return log_oom();
+
+ JSON_VARIANT_ARRAY_FOREACH(z, per_machine) {
+ JsonVariant *u;
+
+ if (!json_variant_is_object(z))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing.");
+
+ array[i++] = z;
+
+ u = json_variant_by_key(z, "matchMachineId");
+ if (!u)
+ continue;
+
+ if (!json_variant_equal(u, mmid))
+ continue;
+
+ r = json_variant_merge(&add, z);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge perMachine entry: %m");
+
+ i--;
+ }
+
+ r = json_variant_filter(&add, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter perMachine: %m");
+
+ r = json_variant_merge(&add, arg_identity_extra_this_machine);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge in perMachine fields: %m");
+
+ if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) {
+ _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;
+
+ rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits"));
+
+ r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter resource limits: %m");
+
+ r = json_variant_merge(&rlv, arg_identity_extra_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set resource limits: %m");
+
+ if (json_variant_is_blank_object(rlv)) {
+ r = json_variant_filter(&add, STRV_MAKE("resourceLimits"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
+ } else {
+ r = json_variant_set_field(&add, "resourceLimits", rlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+ }
+
+ if (!json_variant_is_blank_object(add)) {
+ r = json_variant_set_field(&add, "matchMachineId", mmid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set matchMachineId field: %m");
+
+ array[i++] = add;
+ }
+
+ r = json_variant_new_array(&npm, array, i);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new perMachine array: %m");
+
+ json_variant_unref(per_machine);
+ per_machine = TAKE_PTR(npm);
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine);
+
+ if (arg_identity_extra_rlimits) {
+ r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+
+ r = json_variant_set_field(&item, "matchMachineId", mmid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set matchMachineId field: %m");
+
+ r = json_variant_append_array(&per_machine, item);
+ if (r < 0)
+ return log_error_errno(r, "Failed to append to perMachine array: %m");
+ }
+
+ r = json_variant_set_field(&v, "perMachine", per_machine);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update per machine record: %m");
+ }
+
+ if (arg_identity_extra_privileged || arg_identity_filter) {
+ _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL;
+
+ privileged = json_variant_ref(json_variant_by_key(v, "privileged"));
+
+ r = json_variant_filter(&privileged, arg_identity_filter);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter identity (privileged part): %m");
+
+ r = json_variant_merge(&privileged, arg_identity_extra_privileged);
+ if (r < 0)
+ return log_error_errno(r, "Failed to merge identities (privileged part): %m");
+
+ if (json_variant_is_blank_object(privileged)) {
+ r = json_variant_filter(&v, STRV_MAKE("privileged"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop privileged part from identity: %m");
+ } else {
+ r = json_variant_set_field(&v, "privileged", privileged);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update privileged part of identity: %m");
+ }
+ }
+
+ if (arg_identity_filter_rlimits) {
+ _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL;
+
+ rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits"));
+
+ r = json_variant_filter(&rlv, arg_identity_filter_rlimits);
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter resource limits: %m");
+
+ /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */
+
+ if (json_variant_is_blank_object(rlv)) {
+ r = json_variant_filter(&v, STRV_MAKE("resourceLimits"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to drop resource limits field from identity: %m");
+ } else {
+ r = json_variant_set_field(&v, "resourceLimits", rlv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update resource limits of identity: %m");
+ }
+ }
+
+ if (DEBUG_LOGGING)
+ json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL);
+
+ json_variant_unref(*_v);
+ *_v = TAKE_PTR(v);
+
+ return 0;
+}
+
+static int add_disposition(JsonVariant **v) {
+ int r;
+
+ assert(v);
+
+ if (json_variant_by_key(*v, "disposition"))
+ return 0;
+
+ /* Set the disposition to regular, if not configured explicitly */
+ r = json_variant_set_field_string(v, "disposition", "regular");
+ if (r < 0)
+ return log_error_errno(r, "Failed to set disposition field: %m");
+
+ return 1;
+}
+
+static int acquire_home_record(UserRecord **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ assert(ret);
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ if (arg_identity) {
+ unsigned line, column;
+
+ r = json_parse_file(
+ streq(arg_identity, "-") ? stdin : NULL,
+ streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+ }
+
+ r = apply_identity_changes(&v);
+ if (r < 0)
+ return r;
+
+ r = update_last_change(&v, false);
+ if (r < 0)
+ return r;
+
+ r = add_disposition(&v);
+ if (r < 0)
+ return r;
+
+ r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
+
+static int acquire_new_password(
+ const char *user_name,
+ UserRecord *hr,
+ bool suggest) {
+
+ unsigned i = 5;
+ char *e;
+ int r;
+
+ assert(user_name);
+ assert(hr);
+
+ e = getenv("NEWPASSWORD");
+ if (e) {
+ /* As above, this is not for use, just for testing */
+
+ r = user_record_set_password(hr, STRV_MAKE(e), false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ string_erase(e);
+
+ if (unsetenv("NEWPASSWORD") < 0)
+ return log_error_errno(errno, "Failed to unse $NEWPASSWORD: %m");
+
+ return 0;
+ }
+
+ if (suggest)
+ (void) suggest_passwords();
+
+ for (;;) {
+ _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL;
+ _cleanup_free_ char *question = NULL;
+
+ if (--i == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:");
+
+ if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &first);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ question = mfree(question);
+ if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0)
+ return log_oom();
+
+ r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &second);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire password: %m");
+
+ if (strv_equal(first, second)) {
+ r = user_record_set_password(hr, first, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to store password: %m");
+
+ return 0;
+ }
+
+ log_error("Password didn't mach, try again.");
+ }
+}
+
+static int create_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (argc >= 2) {
+ /* If a username was specified, use it */
+
+ if (valid_user_group_name(argv[1]))
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+ else {
+ _cleanup_free_ char *un = NULL, *rr = NULL;
+
+ /* Before we consider the user name invalid, let's check if we can split it? */
+ r = split_user_name_realm(argv[1], &un, &rr);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]);
+
+ if (rr) {
+ r = json_variant_set_field_string(&arg_identity_extra, "realm", rr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+ } else {
+ /* If neither a username nor an identity have been specified we cannot operate. */
+ if (!arg_identity)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required.");
+ }
+
+ r = acquire_home_record(&hr);
+ if (r < 0)
+ return r;
+
+ /* If the JSON record carries no secrets, then let's query them manually */
+ if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) {
+
+ if (strv_isempty(hr->hashed_password)) {
+ /* No hashed passwords set in the record, let's fix that. */
+ r = acquire_new_password(hr->user_name, hr, /* suggest = */ true);
+ if (r < 0)
+ return r;
+
+ r = user_record_make_hashed_password(hr, hr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hash password: %m");
+ } else {
+ /* There's a hash password set in the record, acquire the unhashed version of it. */
+ r = acquire_existing_password(hr->user_name, hr);
+ if (r < 0)
+ return r;
+
+ r = user_record_test_secret(hr, hr);
+ if (r < 0)
+ return log_error_errno(r, "Password does not match record.");
+ }
+ }
+
+ if (hr->enforce_password_policy == 0) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ /* If password quality enforcement is disabled, let's at least warn client side */
+
+ r = quality_check_password(hr, hr, &error);
+ if (r < 0)
+ log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r));
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+
+ r = json_variant_format(hr->json, 0, &formatted);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "CreateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", formatted);
+ 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))
+ return log_error_errno(r, "Failed to create user home: %s", bus_error_message(&error, r));
+
+ log_error_errno(r, "%s", bus_error_message(&error, r));
+ log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)");
+ } else
+ break; /* done */
+
+ r = acquire_new_password(hr->user_name, hr, /* suggest = */ false);
+ if (r < 0)
+ return r;
+
+ r = user_record_make_hashed_password(hr, hr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to hash passwords: %m");
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int remove_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "RemoveHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ 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) {
+ log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static int update_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *json = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_free_ char *buffer = NULL;
+ const char *username;
+ int r;
+
+ if (argc >= 2)
+ username = argv[1];
+ else if (!arg_identity) {
+ buffer = getusername_malloc();
+ if (!buffer)
+ return log_oom();
+
+ username = buffer;
+ } else
+ username = NULL;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (arg_identity) {
+ unsigned line, column;
+ JsonVariant *un;
+
+ r = json_parse_file(
+ streq(arg_identity, "-") ? stdin : NULL,
+ streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+
+ un = json_variant_by_key(json, "userName");
+ if (un) {
+ if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match.");
+
+ if (!username) {
+ buffer = strdup(json_variant_string(un));
+ if (!buffer)
+ return log_oom();
+
+ username = buffer;
+ }
+ } else {
+ if (!username)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified.");
+
+ r = json_variant_set_field_string(&arg_identity_extra, "userName", username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set userName field: %m");
+ }
+
+ } else {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int incomplete;
+ const char *text;
+
+ if (!arg_identity_extra &&
+ !arg_identity_extra_this_machine &&
+ !arg_identity_extra_privileged &&
+ !arg_identity_extra_rlimits &&
+ strv_isempty(arg_identity_filter) &&
+ strv_isempty(arg_identity_filter_rlimits))
+ return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified.");
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetUserRecordByName",
+ &error,
+ &reply,
+ "s",
+ username);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (incomplete)
+ return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record.");
+
+ r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON identity: %m");
+
+ reply = sd_bus_message_unref(reply);
+
+ r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
+ }
+
+ r = apply_identity_changes(&json);
+ if (r < 0)
+ return r;
+
+ /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always override. */
+ r = update_last_change(&json, !arg_identity);
+ if (r < 0)
+ return r;
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) {
+ r = acquire_existing_password(username, hr);
+ if (r < 0)
+ return r;
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "UpdateHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = json_variant_format(hr->json, 0, &formatted);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(m, "s", formatted);
+ 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_BAD_PASSWORD))
+ return log_error_errno(r, "Failed to update home: %s", bus_error_message(&error, r));
+ } else
+ break;
+
+ log_error("Password incorrect, please try again.");
+
+ r = acquire_existing_password(username, hr);
+ if (r < 0)
+ return r;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int passwd_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *buffer = NULL;
+ const char *username;
+ int r;
+
+ if (argc >= 2)
+ username = argv[1];
+ else {
+ buffer = getusername_malloc();
+ if (!buffer)
+ return log_oom();
+
+ username = buffer;
+ }
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ old_secret = user_record_new();
+ if (!old_secret)
+ return log_oom();
+
+ r = acquire_existing_password(username, old_secret);
+ if (r < 0)
+ return r;
+
+ new_secret = user_record_new();
+ if (!new_secret)
+ return log_oom();
+
+ r = acquire_new_password(username, new_secret, /* suggest = */ true);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ChangePasswordHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", username);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, new_secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, old_secret);
+ 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)) {
+
+ log_error_errno(r, "%s", bus_error_message(&error, r));
+
+ r = acquire_new_password(username, new_secret, /* suggest = */ false);
+ if (r < 0)
+ return r;
+
+ continue;
+
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) {
+
+ log_notice("Old password incorrect, please try again.");
+
+ r = acquire_existing_password(username, old_secret);
+ if (r < 0)
+ return r;
+
+ continue;
+ } else
+ return log_error_errno(r, "Failed to change password for home: %s", bus_error_message(&error, r));
+ } else
+ break;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int resize_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ uint64_t ds = UINT64_MAX;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (arg_disk_size_relative != UINT64_MAX ||
+ (argc > 2 && parse_percent(argv[2]) >= 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Relative disk size specification currently not supported when resizing.");
+
+ if (argc > 2) {
+ r = parse_size(argv[2], 1024, &ds);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]);
+ }
+
+ if (arg_disk_size != UINT64_MAX) {
+ if (ds != UINT64_MAX && ds != arg_disk_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing.");
+
+ ds = arg_disk_size;
+ }
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = acquire_existing_password(argv[1], secret);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ResizeHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "st", argv[1], ds);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ 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_BAD_PASSWORD))
+ return log_error_errno(r, "Failed to resize home: %s", bus_error_message(&error, r));
+ } else
+ break;
+
+ log_error("Password incorrect, please try again.");
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int lock_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "LockHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ 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) {
+ log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+ }
+ }
+
+ return ret;
+}
+
+static int unlock_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, ret = EXIT_SUCCESS;
+ char **i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, strv_skip(argv, 1)) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = acquire_existing_password(*i, secret);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "UnlockHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", *i);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ 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_BAD_PASSWORD))
+ log_error("Password incorrect, please try again.");
+ else {
+ log_error_errno(r, "Failed to unlock user home: %s", bus_error_message(&error, r));
+ if (ret == EXIT_SUCCESS)
+ ret = r;
+
+ break;
+ }
+ } else
+ break;
+ }
+ }
+
+ return ret;
+}
+
+static int with_home(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ _cleanup_close_ int acquired_fd = -1;
+ _cleanup_strv_free_ char **cmdline = NULL;
+ const char *home;
+ int r, ret;
+ pid_t pid;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ if (argc < 3) {
+ _cleanup_free_ char *shell = NULL;
+
+ /* If no command is specified, spawn a shell */
+ r = get_shell(&shell);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire shell: %m");
+
+ cmdline = strv_new(shell);
+ } else
+ cmdline = strv_copy(argv + 2);
+ if (!cmdline)
+ return log_oom();
+
+ secret = user_record_new();
+ if (!secret)
+ return log_oom();
+
+ for (;;) {
+ r = acquire_existing_password(argv[1], secret);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "AcquireHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", argv[1]);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
+ m = sd_bus_message_unref(m);
+ if (r < 0) {
+ if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD))
+ return log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r));
+
+ log_error("Password incorrect, please try again.");
+ sd_bus_error_free(&error);
+ } else {
+ int fd;
+
+ r = sd_bus_message_read(reply, "h", &fd);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (acquired_fd < 0)
+ return log_error_errno(errno, "Failed to duplicate acquired fd: %m");
+
+ reply = sd_bus_message_unref(reply);
+ break;
+ }
+ }
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetHomeByName",
+ &error,
+ &reply,
+ "s",
+ argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ if (chdir(home) < 0) {
+ log_error_errno(errno, "Failed to change to directory %s: %m", home);
+ _exit(255);
+ }
+
+ execvp(cmdline[0], cmdline);
+ log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]);
+ _exit(255);
+ }
+
+ ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL);
+
+ /* Close the fd that pings the home now. */
+ acquired_fd = safe_close(acquired_fd);
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ReleaseHome");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", argv[1]);
+ 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_HOME_BUSY))
+ log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]);
+ else
+ return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r));
+ }
+
+ return ret;
+}
+
+static int lock_all_homes(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "LockAllHomes");
+ 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)
+ return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+
+ return EXIT_SUCCESS;
+}
+
+static int drop_from_identity(const char *field) {
+ int r;
+
+ assert(field);
+
+ /* If we are called to update an identity record and drop some field, let's keep track of what to
+ * remove from the old record */
+ r = strv_extend(&arg_identity_filter, field);
+ if (r < 0)
+ return log_oom();
+
+ /* Let's also drop the field if it was previously set to a new value on the same command line */
+ r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = terminal_urlify_man("homectl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] {COMMAND} ...\n\n"
+ "Create, manipulate or inspect home directories.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --no-ask-password Do not ask for system passwords\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " -M --machine=CONTAINER Operate on local container\n"
+ " --identity=PATH Read JSON identity from file\n"
+ " --json=FORMAT Output inspection data in JSON (takes one of\n"
+ " pretty, short, off)\n"
+ " -j Equivalent to --json=pretty (on TTY) or\n"
+ " --json=short (otherwise)\n"
+ " --export-format= Strip JSON inspection data (full, stripped,\n"
+ " minimal)\n"
+ " -E When specified once equals -j --export-format=\n"
+ " stripped, when specified twice equals\n"
+ " -j --export-format=minimal\n"
+ "\n%3$sGeneral User Record Properties:%4$s\n"
+ " -c --real-name=REALNAME Real name for user\n"
+ " --realm=REALM Realm to create user in\n"
+ " --email-address=EMAIL Email address for user\n"
+ " --location=LOCATION Set location of user on earth\n"
+ " --password-hint=HINT Set Password hint\n"
+ " --icon-name=NAME Icon name for user\n"
+ " -d --home-dir=PATH Home directory\n"
+ " --uid=UID Numeric UID for user\n"
+ " -G --member-of=GROUP Add user to group\n"
+ " --skel=PATH Skeleton directory to use\n"
+ " --shell=PATH Shell for account\n"
+ " --setenv=VARIABLE=VALUE Set an environment variable at log-in\n"
+ " --timezone=TIMEZONE Set a time-zone\n"
+ " --language=LOCALE Set preferred language\n"
+ " --ssh-authorized-keys=KEYS\n"
+ " Specify SSH public keys\n"
+ "\n%3$sAccount Management User Record Properties:%4$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"
+ " --rate-limit-interval=SECS\n"
+ " Login rate-limit interval in seconds\n"
+ " --rate-limit-burst=NUMBER\n"
+ " Login rate-limit attempts per interval\n"
+ " --enforce-password-policy=BOOL\n"
+ " Control whether to enforce system's password\n"
+ " policy for this user\n"
+ " -P Equivalent to --enforce-password-password=no\n"
+ "\n%3$sResource Management User Record Properties:%4$s\n"
+ " --disk-size=BYTES Size to assign the user on disk\n"
+ " --access-mode=MODE User home directory access mode\n"
+ " --umask=MODE Umask for user when logging in\n"
+ " --nice=NICE Nice level for user\n"
+ " --rlimit=LIMIT=VALUE[:VALUE]\n"
+ " Set resource limits\n"
+ " --tasks-max=MAX Set maximum number of per-user tasks\n"
+ " --memory-high=BYTES Set high memory threshold in bytes\n"
+ " --memory-max=BYTES Set maximum memory limit\n"
+ " --cpu-weight=WEIGHT Set CPU weight\n"
+ " --io-weight=WEIGHT Set IO weight\n"
+ "\n%3$sStorage User Record Properties:%4$s\n"
+ " --storage=STORAGE Storage type to use (luks, fscrypt, directory,\n"
+ " subvolume, cifs)\n"
+ " --image-path=PATH Path to image file/directory\n"
+ "\n%3$sLUKS Storage User Record Properties:%4$s\n"
+ " --fs-type=TYPE File system type to use in case of luks\n"
+ " storage (ext4, xfs, btrfs)\n"
+ " --luks-discard=BOOL Whether to use 'discard' feature of file system\n"
+ " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n"
+ " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n"
+ " --luks-volume-key-size=BITS\n"
+ " Volume key size to use for LUKS encryption\n"
+ " --luks-pbkdf-type=TYPE Password-based Key Derivation Function to use\n"
+ " --luks-pbkdf-hash-algorithm=ALGORITHM\n"
+ " PBKDF hash algorithm to use\n"
+ " --luks-pbkdf-time-cost=SECS\n"
+ " Time cost for PBKDF in seconds\n"
+ " --luks-pbkdf-memory-cost=BYTES\n"
+ " Memory cost for PBKDF in bytes\n"
+ " --luks-pbkdf-parallel-threads=NUMBER\n"
+ " Number of parallel threads for PKBDF\n"
+ "\n%3$sMounting User Record Properties:%4$s\n"
+ " --nosuid=BOOL Control the 'nosuid' flag of the home mount\n"
+ " --nodev=BOOL Control the 'nodev' flag of the home mount\n"
+ " --noexec=BOOL Control the 'noexec' flag of the home mount\n"
+ "\n%3$sCIFS User Record Properties:%4$s\n"
+ " --cifs-domain=DOMAIN CIFS (Windows) domain\n"
+ " --cifs-user-name=USER CIFS (Windows) user name\n"
+ " --cifs-service=SERVICE CIFS (Windows) service to mount as home\n"
+ "\n%3$sLogin Behaviour User Record Properties:%4$s\n"
+ " --stop-delay=SECS How long to leave user services running after\n"
+ " logout\n"
+ " --kill-processes=BOOL Whether to kill user processes when sessions\n"
+ " terminate\n"
+ " --auto-login=BOOL Try to log this user in automatically\n"
+ "\n%3$sCommands:%4$s\n"
+ " list List homes\n"
+ " activate USER… Activate home\n"
+ " deactivate USER… Deactivate home\n"
+ " inspect USER… Inspect home\n"
+ " authenticate USER… Authenticate home\n"
+ " create USER Create a home\n"
+ " remove USER… Remove a home\n"
+ " update USER Update a home\n"
+ " passwd USER Change password of a home\n"
+ " resize USER SIZE Resize a home\n"
+ " lock USER… Temporarily lock an active home\n"
+ " unlock USER… Unlock a temporarily locked home\n"
+ " lock-all Lock all suitable homes\n"
+ " with USER [COMMAND…] Run shell or command with access to home\n"
+ "\nSee the %2$s for details.\n"
+ , program_invocation_short_name
+ , link
+ , ansi_underline(), ansi_normal()
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_NO_ASK_PASSWORD,
+ ARG_REALM,
+ ARG_EMAIL_ADDRESS,
+ ARG_DISK_SIZE,
+ ARG_ACCESS_MODE,
+ ARG_STORAGE,
+ ARG_FS_TYPE,
+ ARG_IMAGE_PATH,
+ ARG_UMASK,
+ ARG_LUKS_DISCARD,
+ ARG_JSON,
+ ARG_SETENV,
+ ARG_TIMEZONE,
+ ARG_LANGUAGE,
+ ARG_LOCKED,
+ ARG_SSH_AUTHORIZED_KEYS,
+ ARG_LOCATION,
+ ARG_ICON_NAME,
+ ARG_PASSWORD_HINT,
+ ARG_NICE,
+ ARG_RLIMIT,
+ ARG_NOT_BEFORE,
+ ARG_NOT_AFTER,
+ ARG_LUKS_CIPHER,
+ ARG_LUKS_CIPHER_MODE,
+ ARG_LUKS_VOLUME_KEY_SIZE,
+ ARG_NOSUID,
+ ARG_NODEV,
+ ARG_NOEXEC,
+ ARG_CIFS_DOMAIN,
+ ARG_CIFS_USER_NAME,
+ ARG_CIFS_SERVICE,
+ ARG_TASKS_MAX,
+ ARG_MEMORY_HIGH,
+ ARG_MEMORY_MAX,
+ ARG_CPU_WEIGHT,
+ ARG_IO_WEIGHT,
+ ARG_LUKS_PBKDF_TYPE,
+ ARG_LUKS_PBKDF_HASH_ALGORITHM,
+ ARG_LUKS_PBKDF_TIME_COST,
+ ARG_LUKS_PBKDF_MEMORY_COST,
+ ARG_LUKS_PBKDF_PARALLEL_THREADS,
+ ARG_RATE_LIMIT_INTERVAL,
+ ARG_RATE_LIMIT_BURST,
+ ARG_STOP_DELAY,
+ ARG_KILL_PROCESSES,
+ ARG_ENFORCE_PASSWORD_POLICY,
+ ARG_EXPORT_FORMAT,
+ ARG_AUTO_LOGIN,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
+ { "host", required_argument, NULL, 'H' },
+ { "machine", required_argument, NULL, 'M' },
+ { "identity", required_argument, NULL, 'I' },
+ { "real-name", required_argument, NULL, 'c' },
+ { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "realm", required_argument, NULL, ARG_REALM },
+ { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS },
+ { "location", required_argument, NULL, ARG_LOCATION },
+ { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT },
+ { "icon-name", required_argument, NULL, ARG_ICON_NAME },
+ { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */
+ { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */
+ { "member-of", required_argument, NULL, 'G' },
+ { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */
+ { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */
+ { "setenv", required_argument, NULL, ARG_SETENV },
+ { "timezone", required_argument, NULL, ARG_TIMEZONE },
+ { "language", required_argument, NULL, ARG_LANGUAGE },
+ { "locked", required_argument, NULL, ARG_LOCKED },
+ { "not-before", required_argument, NULL, ARG_NOT_BEFORE },
+ { "not-after", required_argument, NULL, ARG_NOT_AFTER },
+ { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */
+ { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS },
+ { "disk-size", required_argument, NULL, ARG_DISK_SIZE },
+ { "access-mode", required_argument, NULL, ARG_ACCESS_MODE },
+ { "umask", required_argument, NULL, ARG_UMASK },
+ { "nice", required_argument, NULL, ARG_NICE },
+ { "rlimit", required_argument, NULL, ARG_RLIMIT },
+ { "tasks-max", required_argument, NULL, ARG_TASKS_MAX },
+ { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH },
+ { "memory-max", required_argument, NULL, ARG_MEMORY_MAX },
+ { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT },
+ { "io-weight", required_argument, NULL, ARG_IO_WEIGHT },
+ { "storage", required_argument, NULL, ARG_STORAGE },
+ { "image-path", required_argument, NULL, ARG_IMAGE_PATH },
+ { "fs-type", required_argument, NULL, ARG_FS_TYPE },
+ { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD },
+ { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER },
+ { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE },
+ { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE },
+ { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE },
+ { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM },
+ { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST },
+ { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST },
+ { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS },
+ { "nosuid", required_argument, NULL, ARG_NOSUID },
+ { "nodev", required_argument, NULL, ARG_NODEV },
+ { "noexec", required_argument, NULL, ARG_NOEXEC },
+ { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME },
+ { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN },
+ { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE },
+ { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL },
+ { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST },
+ { "stop-delay", required_argument, NULL, ARG_STOP_DELAY },
+ { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES },
+ { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY },
+ { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
+ {}
+ };
+
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ for (;;) {
+ int c;
+
+ c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
+ if (c < 0)
+ break;
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case 'M':
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case 'I':
+ arg_identity = optarg;
+ break;
+
+ case 'c':
+ if (isempty(optarg)) {
+ r = drop_from_identity("realName");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!valid_gecos(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realName field: %m");
+
+ break;
+
+ case 'd': {
+ _cleanup_free_ char *hd = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("homeDirectory");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_path_argument_and_warn(optarg, false, &hd);
+ if (r < 0)
+ return r;
+
+ if (!valid_home(hd))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set homeDirectory field: %m");
+
+ break;
+ }
+
+ case ARG_REALM:
+ if (isempty(optarg)) {
+ r = drop_from_identity("realm");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = dns_name_is_valid(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+ break;
+
+ case ARG_EMAIL_ADDRESS:
+ case ARG_LOCATION:
+ case ARG_ICON_NAME:
+ case ARG_CIFS_USER_NAME:
+ case ARG_CIFS_DOMAIN:
+ case ARG_CIFS_SERVICE: {
+
+ const char *field =
+ c == ARG_EMAIL_ADDRESS ? "emailAddress" :
+ c == ARG_LOCATION ? "location" :
+ c == ARG_ICON_NAME ? "iconName" :
+ c == ARG_CIFS_USER_NAME ? "cifsUserName" :
+ c == ARG_CIFS_DOMAIN ? "cifsDomain" :
+ c == ARG_CIFS_SERVICE ? "cifsService" :
+ NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra, field, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_PASSWORD_HINT:
+ if (isempty(optarg)) {
+ r = drop_from_identity("passwordHint");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set passwordHint field: %m");
+
+ string_erase(optarg);
+ break;
+
+ case ARG_NICE: {
+ int nc;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("niceLevel");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = parse_nice(optarg, &nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse nice level: %s", optarg);
+
+ r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set niceLevel field: %m");
+
+ break;
+ }
+
+ case ARG_RLIMIT: {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL;
+ _cleanup_free_ char *field = NULL, *t = NULL;
+ const char *eq;
+ struct rlimit rl;
+ int l;
+
+ if (isempty(optarg)) {
+ /* Remove all resource limits */
+
+ r = drop_from_identity("resourceLimits");
+ if (r < 0)
+ return r;
+
+ arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits);
+ arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits);
+ break;
+ }
+
+ eq = strchr(optarg, '=');
+ if (!eq)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg);
+
+ field = strndup(optarg, eq - optarg);
+ if (!field)
+ return log_oom();
+
+ l = rlimit_from_string_harder(field);
+ if (l < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field);
+
+ if (isempty(eq + 1)) {
+ /* Remove only the specific rlimit */
+
+ r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l));
+ if (r < 0)
+ return r;
+
+ r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field));
+ if (r < 0)
+ return log_error_errno(r, "Failed to filter JSON identity data: %m");
+
+ break;
+ }
+
+ r = rlimit_parse(l, eq + 1, &rl);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1);
+
+ r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m");
+
+ r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m");
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)),
+ JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build resource limit: %m");
+
+ t = strjoin("RLIMIT_", rlimit_to_string(l));
+ if (!t)
+ return log_oom();
+
+ r = json_variant_set_field(&arg_identity_extra_rlimits, t, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l));
+
+ break;
+ }
+
+ case 'u': {
+ uid_t uid;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("uid");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_uid(optarg, &uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse UID '%s'.", optarg);
+
+ if (uid_is_system(uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid);
+ if (uid_is_dynamic(uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid);
+ if (uid == UID_NOBODY)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set realm field: %m");
+
+ break;
+ }
+
+ case 'k':
+ case ARG_IMAGE_PATH: {
+ const char *field = c == 'k' ? "skeletonDirectory" : "imagePath";
+ _cleanup_free_ char *v = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_path_argument_and_warn(optarg, false, &v);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", v);
+
+ break;
+ }
+
+ case 's':
+ if (isempty(optarg)) {
+ r = drop_from_identity("shell");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!valid_shell(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set shell field: %m");
+
+ break;
+
+ case ARG_SETENV: {
+ _cleanup_free_ char **l = NULL, **k = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL;
+ JsonVariant *e;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("environment");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!env_assignment_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg);
+
+ e = json_variant_by_key(arg_identity_extra, "environment");
+ if (e) {
+ r = json_variant_strv(e, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse JSON environment field: %m");
+ }
+
+ k = strv_env_set(l, optarg);
+ if (!k)
+ return log_oom();
+
+ strv_sort(k);
+
+ r = json_variant_new_array_strv(&ne, k);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate environment list JSON: %m");
+
+ r = json_variant_set_field(&arg_identity_extra, "environment", ne);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set environent list: %m");
+
+ break;
+ }
+
+ case ARG_TIMEZONE:
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("timeZone");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!timezone_is_valid(optarg, LOG_DEBUG))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set timezone field: %m");
+
+ break;
+
+ case ARG_LANGUAGE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("language");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!locale_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);
+
+ r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set preferredLanguage field: %m");
+
+ break;
+
+ case ARG_NOSUID:
+ case ARG_NODEV:
+ case ARG_NOEXEC:
+ case ARG_LOCKED:
+ case ARG_KILL_PROCESSES:
+ case ARG_ENFORCE_PASSWORD_POLICY:
+ case ARG_AUTO_LOGIN: {
+ const char *field =
+ c == ARG_LOCKED ? "locked" :
+ c == ARG_NOSUID ? "mountNoSUID" :
+ c == ARG_NODEV ? "mountNoDevices" :
+ c == ARG_NOEXEC ? "mountNoExecute" :
+ c == ARG_KILL_PROCESSES ? "killProcesses" :
+ c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" :
+ c == ARG_AUTO_LOGIN ? "autoLogin" :
+ NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s boolean: %m", field);
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case 'P':
+ r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m");
+
+ break;
+
+ case ARG_DISK_SIZE:
+ if (isempty(optarg)) {
+ r = drop_from_identity("diskSize");
+ if (r < 0)
+ return r;
+
+ r = drop_from_identity("diskSizeRelative");
+ if (r < 0)
+ return r;
+
+ arg_disk_size = arg_disk_size_relative = UINT64_MAX;
+ break;
+ }
+
+ r = parse_permille(optarg);
+ if (r < 0) {
+ r = parse_size(optarg, 1024, &arg_disk_size);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg);
+
+ r = drop_from_identity("diskSizeRelative");
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set diskSize field: %m");
+
+ arg_disk_size_relative = UINT64_MAX;
+ } else {
+ /* Normalize to UINT32_MAX == 100% */
+ arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U;
+
+ r = drop_from_identity("diskSize");
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set diskSizeRelative field: %m");
+
+ arg_disk_size = UINT64_MAX;
+ }
+
+ break;
+
+ case ARG_ACCESS_MODE: {
+ mode_t mode;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("accessMode");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_mode(optarg, &mode);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set access mode field: %m");
+
+ break;
+ }
+
+ case ARG_LUKS_DISCARD:
+ if (isempty(optarg)) {
+ r = drop_from_identity("luksDiscard");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg);
+
+ r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set discard field: %m");
+
+ break;
+
+ case ARG_LUKS_VOLUME_KEY_SIZE:
+ case ARG_LUKS_PBKDF_PARALLEL_THREADS:
+ case ARG_RATE_LIMIT_BURST: {
+ const char *field =
+ c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" :
+ c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" :
+ c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL;
+ unsigned n;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ }
+
+ r = safe_atou(optarg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_UMASK: {
+ mode_t m;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("umask");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_mode(optarg, &m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse umask: %m");
+
+ r = json_variant_set_field_integer(&arg_identity_extra, "umask", m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set umask field: %m");
+
+ break;
+ }
+
+ case ARG_SSH_AUTHORIZED_KEYS: {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *p = NULL;
+ _cleanup_(strv_freep) char **l = NULL, **add = NULL;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("sshAuthorizedKeys");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (optarg[0] == '@') {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ /* If prefixed with '@' read from a file */
+
+ f = fopen(optarg+1, "re");
+ if (!f)
+ return log_error_errno(errno, "Failed to open '%s': %m", optarg+1);
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Faile dto read from '%s': %m", optarg+1);
+ if (r == 0)
+ break;
+
+ if (isempty(line))
+ continue;
+
+ if (line[0] == '#')
+ continue;
+
+ r = strv_consume(&add, TAKE_PTR(line));
+ if (r < 0)
+ return log_oom();
+ }
+ } else {
+ /* Otherwise, assume it's a literal key. Let's do some superficial checks
+ * before accept it though. */
+
+ if (string_has_cc(optarg, NULL))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing.");
+ if (optarg[0] == '#')
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?");
+
+ add = strv_new(optarg);
+ if (!add)
+ return log_oom();
+ }
+
+ v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys"));
+ if (v) {
+ r = json_variant_strv(v, &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse SSH authorized keys list: %m");
+ }
+
+ r = strv_extend_strv(&l, add, true);
+ if (r < 0)
+ return log_oom();
+
+ v = json_variant_unref(v);
+
+ r = json_variant_new_array_strv(&v, l);
+ if (r < 0)
+ return log_oom();
+
+ r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set authorized keys: %m");
+
+ break;
+ }
+
+ case ARG_NOT_BEFORE:
+ case ARG_NOT_AFTER:
+ case 'e': {
+ const char *field;
+ usec_t n;
+
+ field = c == ARG_NOT_BEFORE ? "notBeforeUSec" :
+ IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ /* Note the minor discrepancy regarding -e parsing here: we support that for compat
+ * reasons, and in the original useradd(8) implementation it accepts dates in the
+ * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but
+ * with greater precision. */
+ r = parse_timestamp(optarg, &n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %m", field);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, n);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+ break;
+ }
+
+ case ARG_STORAGE:
+ case ARG_FS_TYPE:
+ case ARG_LUKS_CIPHER:
+ case ARG_LUKS_CIPHER_MODE:
+ case ARG_LUKS_PBKDF_TYPE:
+ case ARG_LUKS_PBKDF_HASH_ALGORITHM: {
+
+ const char *field =
+ c == ARG_STORAGE ? "storage" :
+ c == ARG_FS_TYPE ? "fileSytemType" :
+ c == ARG_LUKS_CIPHER ? "luksCipher" :
+ c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" :
+ c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" :
+ c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ if (!string_is_safe(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg);
+
+ r = json_variant_set_field_string(
+ IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ?
+ &arg_identity_extra_this_machine :
+ &arg_identity_extra, field, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_LUKS_PBKDF_TIME_COST:
+ case ARG_RATE_LIMIT_INTERVAL:
+ case ARG_STOP_DELAY: {
+ const char *field =
+ c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" :
+ c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" :
+ c == ARG_STOP_DELAY ? "stopDelayUSec" :
+ NULL;
+ usec_t t;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ r = parse_sec(optarg, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s field: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case 'G': {
+ const char *p = optarg;
+
+ if (isempty(p)) {
+ r = drop_from_identity("memberOf");
+ if (r < 0)
+ return r;
+
+ break;
+ }
+
+ for (;;) {
+ _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL;
+ _cleanup_strv_free_ char **list = NULL;
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, ",", 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
+ if (r == 0)
+ break;
+
+ if (!valid_user_group_name(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word);
+
+ mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf"));
+
+ r = json_variant_strv(mo, &list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse group list: %m");
+
+ r = strv_extend(&list, word);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extend group list: %m");
+
+ strv_sort(list);
+ strv_uniq(list);
+
+ mo = json_variant_unref(mo);
+ r = json_variant_new_array_strv(&mo, list);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create group list JSON: %m");
+
+ r = json_variant_set_field(&arg_identity_extra, "memberOf", mo);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update group list: %m");
+ }
+
+ break;
+ }
+
+ case ARG_TASKS_MAX: {
+ uint64_t u;
+
+ if (isempty(optarg)) {
+ r = drop_from_identity("tasksMax");
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = safe_atou64(optarg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set tasksMax field: %m");
+
+ break;
+ }
+
+ case ARG_MEMORY_MAX:
+ case ARG_MEMORY_HIGH:
+ case ARG_LUKS_PBKDF_MEMORY_COST: {
+ const char *field =
+ c == ARG_MEMORY_MAX ? "memoryMax" :
+ c == ARG_MEMORY_HIGH ? "memoryHigh" :
+ c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL;
+
+ uint64_t u;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = parse_size(optarg, 1024, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case ARG_CPU_WEIGHT:
+ case ARG_IO_WEIGHT: {
+ const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" :
+ c == ARG_IO_WEIGHT ? "ioWeight" : NULL;
+ uint64_t u;
+
+ assert(field);
+
+ if (isempty(optarg)) {
+ r = drop_from_identity(field);
+ if (r < 0)
+ return r;
+ break;
+ }
+
+ r = safe_atou64(optarg, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg);
+
+ if (!CGROUP_WEIGHT_IS_OK(u))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u);
+
+ r = json_variant_set_field_unsigned(&arg_identity_extra, field, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set %s field: %m", field);
+
+ break;
+ }
+
+ case 'j':
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_JSON:
+ if (streq(optarg, "pretty")) {
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO;
+ } else if (streq(optarg, "short")) {
+ arg_json = true;
+ arg_json_format_flags = JSON_FORMAT_NEWLINE;
+ } else if (streq(optarg, "off")) {
+ arg_json = false;
+ arg_json_format_flags = 0;
+ } else if (streq(optarg, "help")) {
+ puts("pretty\n"
+ "short\n"
+ "off");
+ return 0;
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg);
+
+ break;
+
+ case 'E':
+ if (arg_export_format == EXPORT_FORMAT_FULL)
+ arg_export_format = EXPORT_FORMAT_STRIPPED;
+ else if (arg_export_format == EXPORT_FORMAT_STRIPPED)
+ arg_export_format = EXPORT_FORMAT_MINIMAL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported.");
+
+ arg_json = true;
+ if (arg_json_format_flags == 0)
+ arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+ break;
+
+ case ARG_EXPORT_FORMAT:
+ if (streq(optarg, "full"))
+ arg_export_format = EXPORT_FORMAT_FULL;
+ else if (streq(optarg, "stripped"))
+ arg_export_format = EXPORT_FORMAT_STRIPPED;
+ else if (streq(optarg, "minimal"))
+ arg_export_format = EXPORT_FORMAT_MINIMAL;
+ else if (streq(optarg, "help")) {
+ puts("full\n"
+ "stripped\n"
+ "minimal");
+ return 0;
+ }
+
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes },
+ { "activate", 2, VERB_ANY, 0, activate_home },
+ { "deactivate", 2, VERB_ANY, 0, deactivate_home },
+ { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home },
+ { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home },
+ { "create", VERB_ANY, 2, 0, create_home },
+ { "remove", 2, VERB_ANY, 0, remove_home },
+ { "update", VERB_ANY, 2, 0, update_home },
+ { "passwd", VERB_ANY, 2, 0, passwd_home },
+ { "resize", 2, 3, 0, resize_home },
+ { "lock", 2, VERB_ANY, 0, lock_home },
+ { "unlock", 2, VERB_ANY, 0, unlock_home },
+ { "with", 2, VERB_ANY, 0, with_home },
+ { "lock-all", VERB_ANY, 1, 0, lock_all_homes },
+
+ /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a
+ * user-facing verb and thus should not appear in man pages or --help texts. */
+ { "ssh-authorized-keys", 2, 2, 0, ssh_authorized_keys },
+ {}
+ };
+
+ int r;
+
+ log_show_color(true);
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c
new file mode 100644
index 0000000000000..0193089668d71
--- /dev/null
+++ b/src/home/homed-bus.c
@@ -0,0 +1,64 @@
+#include "homed-bus.h"
+#include "strv.h"
+
+int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *full = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ unsigned line = 0, column = 0;
+ const char *json;
+ int r;
+
+ assert(ret);
+
+ r = sd_bus_message_read(m, "s", &json);
+ if (r < 0)
+ return r;
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON secret record at %u:%u: %m", line, column);
+
+ r = json_build(&full, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("secret", JSON_BUILD_VARIANT(v))));
+ if (r < 0)
+ return r;
+
+ hr = user_record_new();
+ if (!hr)
+ return -ENOMEM;
+
+ r = user_record_load(hr, full, USER_RECORD_REQUIRE_SECRET);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
+
+int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ unsigned line = 0, column = 0;
+ const char *json;
+ int r;
+
+ assert(ret);
+
+ r = sd_bus_message_read(m, "s", &json);
+ if (r < 0)
+ return r;
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON identity record at %u:%u: %m", line, column);
+
+ hr = user_record_new();
+ if (!hr)
+ return -ENOMEM;
+
+ r = user_record_load(hr, v, flags);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "JSON data is not a valid identity record");
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h
new file mode 100644
index 0000000000000..20f13b43ade38
--- /dev/null
+++ b/src/home/homed-bus.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "user-record.h"
+#include "json.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);
diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c
new file mode 100644
index 0000000000000..0f9229aafbd05
--- /dev/null
+++ b/src/home/homed-home-bus.c
@@ -0,0 +1,877 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "bus-common-errors.h"
+#include "bus-util.h"
+#include "fd-util.h"
+#include "homed-bus.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+static int property_get_unix_record(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(h);
+
+ return sd_bus_message_append(
+ reply, "(suusss)",
+ h->user_name,
+ (uint32_t) h->uid,
+ h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+ h->record ? user_record_real_name(h->record) : NULL,
+ h->record ? user_record_home_directory(h->record) : NULL,
+ h->record ? user_record_shell(h->record) : NULL);
+}
+
+static int property_get_state(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(h);
+
+ return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h)));
+}
+
+int bus_home_client_is_trusted(Home *h, sd_bus_message *message) {
+ _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
+ uid_t euid;
+ int r;
+
+ assert(h);
+
+ if (!message)
+ return -EINVAL;
+
+ r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_creds_get_euid(creds, &euid);
+ if (r < 0)
+ return r;
+
+ return euid == 0 || h->uid == euid;
+}
+
+int bus_home_get_record_json(
+ Home *h,
+ sd_bus_message *message,
+ char **ret,
+ bool *ret_incomplete) {
+
+ _cleanup_(user_record_unrefp) UserRecord *augmented = NULL;
+ UserRecordLoadFlags flags;
+ int r, trusted;
+
+ assert(h);
+ assert(ret);
+
+ trusted = bus_home_client_is_trusted(h, message);
+ if (trusted < 0) {
+ log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted.");
+ trusted = false;
+ }
+
+ flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+ if (trusted)
+ flags |= USER_RECORD_ALLOW_PRIVILEGED;
+ else
+ flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+ r = home_augment_status(h, flags, &augmented);
+ if (r < 0)
+ return r;
+
+ r = json_variant_format(augmented->json, 0, ret);
+ if (r < 0)
+ return r;
+
+ if (ret_incomplete)
+ *ret_incomplete = augmented->incomplete;
+
+ return 0;
+}
+
+static int property_get_user_record(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *json = NULL;
+ Home *h = userdata;
+ bool incomplete;
+ int r;
+
+ assert(bus);
+ assert(reply);
+ assert(h);
+
+ r = bus_home_get_record_json(h, sd_bus_get_current_message(bus), &json, &incomplete);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_append(reply, "(sb)", json, incomplete);
+}
+
+int bus_home_method_activate(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = home_activate(h, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ /* The operation is now in process, keep track of this message so that we can later reply to it. */
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_deactivate(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = home_deactivate(h, false, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_unregister(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.remove-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_unregister(h, error);
+ if (r < 0)
+ return r;
+
+ assert(r > 0);
+
+ /* Note that home_unregister() destroyed 'h' here, so no more accesses */
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_home_method_realize(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.create-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_create(h, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ h->unregister_on_failure = false;
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_remove(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.remove-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_remove(h, error);
+ if (r < 0)
+ return r;
+ if (r > 0) /* Done already. Note that home_remove() destroyed 'h' here, so no more accesses */
+ return sd_bus_reply_method_return(message, NULL);
+
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_fixate(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = home_fixate(h, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_authenticate(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.authenticate-home",
+ NULL,
+ true,
+ h->uid,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_authenticate(h, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+ assert(message);
+ assert(hr);
+
+ r = user_record_is_supported(hr, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.update-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_update(h, hr, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_update(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+ if (r < 0)
+ return r;
+
+ return bus_home_method_update_record(h, message, hr, error);
+}
+
+int bus_home_method_resize(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ uint64_t sz;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = sd_bus_message_read(message, "t", &sz);
+ if (r < 0)
+ return r;
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.resize-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_resize(h, sz, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_change_password(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *new_secret = NULL, *old_secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &new_secret, error);
+ if (r < 0)
+ return r;
+
+ r = bus_message_read_secret(message, &old_secret, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.passwd-home",
+ NULL,
+ true,
+ h->uid,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = home_passwd(h, new_secret, old_secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_lock(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = home_lock(h, error);
+ if (r < 0)
+ return r;
+ if (r > 0) /* Done */
+ return sd_bus_reply_method_return(message, NULL);
+
+ /* The operation is now in process, keep track of this message so that we can later reply to it. */
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_unlock(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = home_unlock(h, secret, error);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ assert(!h->current_operation);
+
+ /* The operation is now in process, keep track of this message so that we can later reply to it. */
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_acquire(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+ _cleanup_close_ int fd = -1;
+ int r, please_suspend;
+ Home *h = userdata;
+
+ assert(message);
+ assert(h);
+
+ r = bus_message_read_secret(message, &secret, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "b", &please_suspend);
+ if (r < 0)
+ return r;
+
+ /* This operation might not be something we can executed immediately, hence queue it */
+ fd = home_create_fifo(h, please_suspend);
+ if (fd < 0)
+ return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name);
+
+ o = operation_new(OPERATION_ACQUIRE, message);
+ if (!o)
+ return -ENOMEM;
+
+ o->secret = TAKE_PTR(secret);
+ o->send_fd = TAKE_FD(fd);
+
+ r = home_schedule_operation(h, o, error);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int bus_home_method_ref(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_close_ int fd = -1;
+ Home *h = userdata;
+ HomeState state;
+ int please_suspend, r;
+
+ assert(message);
+ assert(h);
+
+ r = sd_bus_message_read(message, "b", &please_suspend);
+ if (r < 0)
+ return r;
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_UNFIXATED:
+ case HOME_INACTIVE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ default:
+ if (HOME_STATE_IS_ACTIVE(state))
+ break;
+
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ fd = home_create_fifo(h, please_suspend);
+ if (fd < 0)
+ return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name);
+
+ return sd_bus_reply_method_return(message, "h", fd);
+}
+
+int bus_home_method_release(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(message);
+ assert(h);
+
+ o = operation_new(OPERATION_RELEASE, message);
+ if (!o)
+ return -ENOMEM;
+
+ r = home_schedule_operation(h, o, error);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+/* We map a uid_t as uint32_t bus property, let's ensure this is safe. */
+assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+
+const sd_bus_vtable home_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_PROPERTY("UserName", "s", NULL, offsetof(Home, user_name), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("UID", "u", NULL, offsetof(Home, uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("UnixRecord", "(suusss)", property_get_unix_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("State", "s", property_get_state, 0, 0),
+ SD_BUS_PROPERTY("UserRecord", "(sb)", property_get_user_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Activate", "s", NULL, bus_home_method_activate, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0),
+ SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Realize", "s", NULL, bus_home_method_realize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Remove", NULL, NULL, bus_home_method_remove, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Fixate", "s", NULL, bus_home_method_fixate, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Authenticate", "s", NULL, bus_home_method_authenticate, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Update", "s", NULL, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Resize", "ts", NULL, bus_home_method_resize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("ChangePassword", "ss", NULL, bus_home_method_change_password, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Lock", NULL, NULL, bus_home_method_lock, 0),
+ SD_BUS_METHOD("Unlock", "s", NULL, bus_home_method_unlock, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Acquire", "sb", "h", bus_home_method_acquire, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("Ref", "b", "h", bus_home_method_ref, 0),
+ SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0),
+ SD_BUS_VTABLE_END
+};
+
+int bus_home_path(Home *h, char **ret) {
+ assert(ret);
+
+ return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret);
+}
+
+int bus_home_object_find(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ void *userdata,
+ void **found,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *e = NULL;
+ Manager *m = userdata;
+ uid_t uid;
+ Home *h;
+ int r;
+
+ r = sd_bus_path_decode(path, "/org/freedesktop/home1/home", &e);
+ if (r <= 0)
+ return 0;
+
+ if (parse_uid(e, &uid) >= 0)
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+ else
+ h = hashmap_get(m->homes_by_name, e);
+ if (!h)
+ return 0;
+
+ *found = h;
+ return 1;
+}
+
+int bus_home_node_enumerator(
+ sd_bus *bus,
+ const char *path,
+ void *userdata,
+ char ***nodes,
+ sd_bus_error *error) {
+
+ _cleanup_strv_free_ char **l = NULL;
+ Manager *m = userdata;
+ size_t k = 0;
+ Iterator i;
+ Home *h;
+ int r;
+
+ assert(nodes);
+
+ l = new0(char*, hashmap_size(m->homes_by_uid) + 1);
+ if (!l)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(h, m->homes_by_uid, i) {
+ r = bus_home_path(h, l + k);
+ if (r < 0)
+ return r;
+ }
+
+ *nodes = TAKE_PTR(l);
+ return 1;
+}
+
+static int on_deferred_change(sd_event_source *s, void *userdata) {
+ _cleanup_free_ char *path = NULL;
+ Home *h = userdata;
+ int r;
+
+ assert(h);
+
+ h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source);
+
+ r = bus_home_path(h, &path);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to generate home bus path, ignoring: %m");
+ return 0;
+ }
+
+ if (h->announced)
+ r = sd_bus_emit_properties_changed_strv(h->manager->bus, path, "org.freedesktop.home1.Home", NULL);
+ else
+ r = sd_bus_emit_object_added(h->manager->bus, path);
+ if (r < 0)
+ log_warning_errno(r, "Failed to send home change event, ignoring: %m");
+ else
+ h->announced = true;
+
+ return 0;
+}
+
+int bus_home_emit_change(Home *h) {
+ int r;
+
+ assert(h);
+
+ if (h->deferred_change_event_source)
+ return 1;
+
+ if (!h->manager->event)
+ return 0;
+
+ if (IN_SET(sd_event_get_state(h->manager->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+ return 0;
+
+ r = sd_event_add_defer(h->manager->event, &h->deferred_change_event_source, on_deferred_change, h);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate deferred change event source: %m");
+
+ r = sd_event_source_set_priority(h->deferred_change_event_source, SD_EVENT_PRIORITY_IDLE+5);
+ if (r < 0)
+ log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+ (void) sd_event_source_set_description(h->deferred_change_event_source, "deferred-change-event");
+ return 1;
+}
+
+int bus_home_emit_remove(Home *h) {
+ _cleanup_free_ char *path = NULL;
+ int r;
+
+ assert(h);
+
+ if (!h->announced)
+ return 0;
+
+ r = bus_home_path(h, &path);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_emit_object_removed(h->manager->bus, path);
+ if (r < 0)
+ return r;
+
+ h->announced = false;
+ return 1;
+}
diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h
new file mode 100644
index 0000000000000..20516b1205350
--- /dev/null
+++ b/src/home/homed-home-bus.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "homed-home.h"
+
+int bus_home_client_is_trusted(Home *h, sd_bus_message *message);
+int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete);
+
+int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_deactivate(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_realize(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *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_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);
+int bus_home_method_unlock(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_ref(sd_bus_message *message, void *userdata, sd_bus_error *error);
+int bus_home_method_release(sd_bus_message *message, void *userdata, sd_bus_error *error);
+
+extern const sd_bus_vtable home_vtable[];
+
+int bus_home_path(Home *h, char **ret);
+
+int bus_home_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
+int bus_home_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error);
+
+int bus_home_emit_change(Home *h);
+int bus_home_emit_remove(Home *h);
diff --git a/src/home/homed-home.c b/src/home/homed-home.c
new file mode 100644
index 0000000000000..f553a40a4b873
--- /dev/null
+++ b/src/home/homed-home.c
@@ -0,0 +1,2684 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#if HAVE_LINUX_MEMFD_H
+#include
+#endif
+
+#include
+#include
+
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "bus-common-errors.h"
+#include "env-util.h"
+#include "errno-list.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "home-util.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "missing.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "pwquality-util.h"
+#include "resize-fs.h"
+#include "set.h"
+#include "signal-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+#define HOME_USERS_MAX 500
+#define PENDING_OPERATIONS_MAX 100
+
+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);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref);
+
+static int suitable_home_record(UserRecord *hr) {
+ int r;
+
+ assert(hr);
+
+ if (!hr->user_name)
+ return -EUNATCH;
+
+ /* We are a bit more restrictive with what we accept as homed-managed user than what we accept in
+ * home records in general. Let's enforce the stricter rule here. */
+ if (!suitable_user_name(hr->user_name))
+ return -EINVAL;
+ if (!uid_is_valid(hr->uid))
+ return -EINVAL;
+
+ /* Insist we are outside of the dynamic and system range */
+ if (uid_is_system(hr->uid) || gid_is_system(user_record_gid(hr)) ||
+ uid_is_dynamic(hr->uid) || gid_is_dynamic(user_record_gid(hr)))
+ return -EADDRNOTAVAIL;
+
+ /* Insist that GID and UID match */
+ if (user_record_gid(hr) != (gid_t) hr->uid)
+ return -EBADSLT;
+
+ /* Similar for the realm */
+ if (hr->realm) {
+ r = suitable_realm(hr->realm);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+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;
+ int r;
+
+ assert(m);
+ assert(hr);
+
+ r = suitable_home_record(hr);
+ if (r < 0)
+ return r;
+
+ if (hashmap_contains(m->homes_by_name, hr->user_name))
+ return -EBUSY;
+
+ if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid)))
+ return -EBUSY;
+
+ if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs))
+ return -EBUSY;
+
+ if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX)
+ return -EUSERS;
+
+ nm = strdup(hr->user_name);
+ if (!nm)
+ return -ENOMEM;
+
+ if (sysfs) {
+ ns = strdup(sysfs);
+ if (!ns)
+ return -ENOMEM;
+ }
+
+ home = new(Home, 1);
+ if (!home)
+ return -ENOMEM;
+
+ *home = (Home) {
+ .manager = m,
+ .user_name = TAKE_PTR(nm),
+ .uid = hr->uid,
+ .state = _HOME_STATE_INVALID,
+ .worker_stdout_fd = -1,
+ .sysfs = TAKE_PTR(ns),
+ .signed_locally = -1,
+ };
+
+ r = hashmap_put(m->homes_by_name, home->user_name, home);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home);
+ if (r < 0)
+ return r;
+
+ if (home->sysfs) {
+ r = hashmap_put(m->homes_by_sysfs, home->sysfs, home);
+ if (r < 0)
+ return r;
+ }
+
+ r = user_record_clone(hr, USER_RECORD_LOAD_MASK_SECRET, &home->record);
+ if (r < 0)
+ return r;
+
+ (void) bus_manager_emit_auto_login_changed(m);
+ (void) bus_home_emit_change(home);
+
+ if (ret)
+ *ret = TAKE_PTR(home);
+ else
+ TAKE_PTR(home);
+
+ return 0;
+}
+
+Home *home_free(Home *h) {
+
+ if (!h)
+ return NULL;
+
+ if (h->manager) {
+ (void) bus_home_emit_remove(h);
+ (void) bus_manager_emit_auto_login_changed(h->manager);
+
+ if (h->user_name)
+ (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h);
+
+ if (uid_is_valid(h->uid))
+ (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h);
+
+ if (h->sysfs)
+ (void) hashmap_remove_value(h->manager->homes_by_sysfs, h->sysfs, h);
+
+ if (h->worker_pid > 0)
+ (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+
+ if (h->manager->gc_focus == h)
+ h->manager->gc_focus = NULL;
+ }
+
+ user_record_unref(h->record);
+ user_record_unref(h->secret);
+
+ h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+ safe_close(h->worker_stdout_fd);
+ free(h->user_name);
+ free(h->sysfs);
+
+ h->ref_event_source_please_suspend = sd_event_source_unref(h->ref_event_source_please_suspend);
+ h->ref_event_source_dont_suspend = sd_event_source_unref(h->ref_event_source_dont_suspend);
+
+ h->pending_operations = ordered_set_free(h->pending_operations);
+ h->pending_event_source = sd_event_source_unref(h->pending_event_source);
+ h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source);
+
+ h->current_operation = operation_unref(h->current_operation);
+
+ return mfree(h);
+}
+
+int home_set_record(Home *h, UserRecord *hr) {
+ _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL;
+ Home *other;
+ int r;
+
+ assert(h);
+ assert(h->user_name);
+ assert(h->record);
+ assert(hr);
+
+ if (user_record_equal(h->record, hr))
+ return 0;
+
+ r = suitable_home_record(hr);
+ if (r < 0)
+ return r;
+
+ if (!user_record_compatible(h->record, hr))
+ return -EREMCHG;
+
+ if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) ||
+ FLAGS_SET(hr->mask, USER_RECORD_SECRET))
+ return -EINVAL;
+
+ if (FLAGS_SET(h->record->mask, USER_RECORD_STATUS)) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ /* Hmm, the existing record has status fields? If so, copy them over */
+
+ v = json_variant_ref(hr->json);
+ r = json_variant_set_field(&v, "status", json_variant_by_key(h->record->json, "status"));
+ if (r < 0)
+ return r;
+
+ new_hr = user_record_new();
+ if (!new_hr)
+ return -ENOMEM;
+
+ r = user_record_load(new_hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0)
+ return r;
+
+ hr = new_hr;
+ }
+
+ other = hashmap_get(h->manager->homes_by_uid, UID_TO_PTR(hr->uid));
+ if (other && other != h)
+ return -EBUSY;
+
+ if (h->uid != hr->uid) {
+ r = hashmap_remove_and_replace(h->manager->homes_by_uid, UID_TO_PTR(h->uid), UID_TO_PTR(hr->uid), h);
+ if (r < 0)
+ return r;
+ }
+
+ user_record_unref(h->record);
+ h->record = user_record_ref(hr);
+ h->uid = h->record->uid;
+
+ /* The updated record might have a different autologin setting, trigger a PropertiesChanged event for it */
+ (void) bus_manager_emit_auto_login_changed(h->manager);
+ (void) bus_home_emit_change(h);
+
+ return 0;
+}
+
+int home_save_record(Home *h) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ char *text = NULL;
+ const char *fn;
+ int r;
+
+ assert(h);
+
+ v = json_variant_ref(h->record->json);
+ r = json_variant_normalize(&v);
+ if (r < 0)
+ log_warning_errno(r, "User record could not be normalized.");
+
+ r = json_variant_format(v, JSON_FORMAT_PRETTY|JSON_FORMAT_NEWLINE, &text);
+ if (r < 0)
+ return r;
+
+ (void) mkdir("/var/lib/systemd/", 0755);
+ (void) mkdir("/var/lib/systemd/home/", 0700);
+
+ fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity");
+
+ r = write_string_file(fn, text, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MODE_0600);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int home_unlink_record(Home *h) {
+ const char *fn;
+
+ assert(h);
+
+ fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity");
+ if (unlink(fn) < 0 && errno != ENOENT)
+ return -errno;
+
+ fn = strjoina("/run/systemd/home/", h->user_name, ".ref");
+ if (unlink(fn) < 0 && errno != ENOENT)
+ return -errno;
+
+ return 0;
+}
+
+static void home_set_state(Home *h, HomeState state) {
+ HomeState old_state, new_state;
+
+ assert(h);
+
+ old_state = home_get_state(h);
+ h->state = state;
+ new_state = home_get_state(h); /* Query the new state, since the 'state' variable might be set to -1,
+ * in which case we synthesize an high-level state on demand */
+
+ log_info("%s: changing state %s → %s", h->user_name,
+ home_state_to_string(old_state),
+ home_state_to_string(new_state));
+
+ if (HOME_STATE_IS_EXECUTING_OPERATION(old_state) && !HOME_STATE_IS_EXECUTING_OPERATION(new_state)) {
+ /* If we just finished executing some operation, process the queue of pending operations. And
+ * enqueue it for GC too. */
+
+ home_schedule_operation(h, NULL, NULL);
+ manager_enqueue_gc(h->manager, h);
+ }
+}
+
+static int home_parse_worker_stdout(int _fd, UserRecord **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_close_ int fd = _fd; /* take possession, even on failure */
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ unsigned line, column;
+ struct stat st;
+ int r;
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat stdout fd: %m");
+
+ assert(S_ISREG(st.st_mode));
+
+ if (st.st_size == 0) { /* empty record */
+ *ret = NULL;
+ return 0;
+ }
+
+ if (lseek(fd, SEEK_SET, 0) == (off_t) -1)
+ return log_error_errno(errno, "Failed to seek to beginning of memfd: %m");
+
+ f = fdopen(fd, "r");
+ if (!f)
+ return log_error_errno(errno, "Failed to reopen memfd: %m");
+
+ TAKE_FD(fd);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *text = NULL;
+
+ r = read_full_stream(f, &text, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read from client: %m");
+
+ log_debug("Got from worker: %s", text);
+ rewind(f);
+ }
+
+ r = json_parse_file(f, "stdout", JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column);
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load home record identity: %m");
+
+ *ret = TAKE_PTR(hr);
+ return 1;
+}
+
+static int home_verify_user_record(Home *h, UserRecord *hr, bool *ret_signed_locally, sd_bus_error *ret_error) {
+ int is_signed;
+
+ assert(h);
+ assert(hr);
+ assert(ret_signed_locally);
+
+ is_signed = manager_verify_user_record(h->manager, hr);
+ switch (is_signed) {
+
+ case USER_RECORD_SIGNED_EXCLUSIVE:
+ log_info("Home %s is signed exclusively by our key, accepting.", hr->user_name);
+ *ret_signed_locally = true;
+ return 0;
+
+ case USER_RECORD_SIGNED:
+ log_info("Home %s is signed by our key (and others), accepting.", hr->user_name);
+ *ret_signed_locally = false;
+ return 0;
+
+ case USER_RECORD_FOREIGN:
+ log_info("Home %s is signed by foreign key we like, accepting.", hr->user_name);
+ *ret_signed_locally = false;
+ return 0;
+
+ case USER_RECORD_UNSIGNED:
+ sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed at all, refusing.", hr->user_name);
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Home %s contains user record that is not signed at all, refusing.", hr->user_name);
+
+ case -ENOKEY:
+ sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed by any known key, refusing.", hr->user_name);
+ return log_error_errno(is_signed, "Home %s contians user record that is not signed by any known key, refusing.", hr->user_name);
+
+ default:
+ assert(is_signed < 0);
+ return log_error_errno(is_signed, "Failed to verify signature on user record for %s, refusing fixation: %m", hr->user_name);
+ }
+}
+
+static int convert_worker_errno(Home *h, int e, sd_bus_error *error) {
+ /* Converts the error numbers the worker process returned into somewhat sensible dbus errors */
+
+ switch (e) {
+
+ case -EMSGSIZE:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot shrinked");
+ case -ETXTBSY:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrinked offline");
+ case -ERANGE:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small");
+ case -ENOLINK:
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend");
+ case -EPROTONOSUPPORT:
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system");
+ case -ENOTTY:
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend");
+ case -ESOCKTNOSUPPORT:
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on file system");
+ case -ENOKEY:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name);
+ case -EBUSY:
+ 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);
+ case -ENOEXEC:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is currently not active", h->user_name);
+ case -ENOSPC:
+ return sd_bus_error_setf(error, BUS_ERROR_NO_DISK_SPACE, "Not enough disk space for home %s", h->user_name);
+ }
+
+ return 0;
+}
+
+static void home_count_bad_authentication(Home *h, bool save) {
+ int r;
+
+ assert(h);
+
+ r = user_record_bad_authentication(h->record);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m");
+ return;
+ }
+
+ if (save) {
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ }
+}
+
+static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL;
+ bool signed_locally;
+ int r;
+
+ assert(h);
+ assert(IN_SET(h->state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
+
+ secret = TAKE_PTR(h->secret); /* Take possession */
+
+ if (ret < 0) {
+ if (ret == -ENOKEY)
+ (void) home_count_bad_authentication(h, false);
+
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Fixation failed: %m");
+ goto fail;
+ }
+ if (!hr) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Did not receive user record from worker process, fixation failed.");
+ goto fail;
+ }
+
+ r = home_verify_user_record(h, hr, &signed_locally, &error);
+ if (r < 0)
+ goto fail;
+
+ r = home_set_record(h, hr);
+ if (r < 0) {
+ log_error_errno(r, "Failed to update home record: %m");
+ goto fail;
+ }
+
+ h->signed_locally = signed_locally;
+
+ /* When we finished fixating (and don't follow-up with activation), let's count this as good authentication */
+ if (h->state == HOME_FIXATING) {
+ r = user_record_good_authentication(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+ }
+
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+
+ if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) {
+
+ r = home_start_work(h, "activate", h->record, secret);
+ if (r < 0) {
+ h->current_operation = operation_result_unref(h->current_operation, r, NULL);
+ home_set_state(h, _HOME_STATE_INVALID);
+ } else
+ home_set_state(h, h->state == HOME_FIXATING_FOR_ACTIVATION ? HOME_ACTIVATING : HOME_ACTIVATING_FOR_ACQUIRE);
+
+ return;
+ }
+
+ log_debug("Fixation of %s completed.", h->user_name);
+
+ h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+
+ /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns
+ * HOME_ABSENT vs. HOME_INACTIVE as necessary. */
+ home_set_state(h, _HOME_STATE_INVALID);
+ return;
+
+fail:
+ /* If fixation fails, we stay in unfixated state! */
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, HOME_UNFIXATED);
+}
+
+static void home_activate_finish(Home *h, int ret, UserRecord *hr) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
+
+ if (ret < 0) {
+ if (ret == -ENOKEY)
+ home_count_bad_authentication(h, true);
+
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Activation failed: %m");
+ goto finish;
+ }
+
+ if (hr) {
+ bool signed_locally;
+
+ r = home_verify_user_record(h, hr, &signed_locally, &error);
+ if (r < 0)
+ goto finish;
+
+ r = home_set_record(h, hr);
+ if (r < 0) {
+ log_error_errno(r, "Failed to update home record, ignoring: %m");
+ goto finish;
+ }
+
+ h->signed_locally = signed_locally;
+
+ r = user_record_good_authentication(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ }
+
+ log_debug("Activation of %s completed.", h->user_name);
+ r = 0;
+
+finish:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(h->state == HOME_DEACTIVATING);
+ assert(!hr); /* We don't expect a record on this operation */
+
+ if (ret < 0) {
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Deactivation of %s failed: %m", h->user_name);
+ goto finish;
+ }
+
+ log_debug("Deactivation of %s completed.", h->user_name);
+ r = 0;
+
+finish:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_remove_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ Manager *m;
+ int r;
+
+ assert(h);
+ assert(h->state == HOME_REMOVING);
+ assert(!hr); /* We don't expect a record on this operation */
+
+ m = h->manager;
+
+ if (ret < 0 && ret != -EALREADY) {
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Removing %s failed: %m", h->user_name);
+ goto fail;
+ }
+
+ /* For a couple of storage types we can't delete the actual data storage when called (such as LUKS on
+ * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally
+ * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan
+ * after completion, so that "unfixated" entries are rediscovered. */
+ if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT))
+ manager_enqueue_rescan(m);
+
+ /* The image is now removed from disk. Now also remove our stored record */
+ r = home_unlink_record(h);
+ if (r < 0) {
+ log_error_errno(r, "Removing record file failed: %m");
+ goto fail;
+ }
+
+ log_debug("Removal of %s completed.", h->user_name);
+ h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+
+ /* Unload this record from memory too now. */
+ h = home_free(h);
+ return;
+
+fail:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_create_finish(Home *h, int ret, UserRecord *hr) {
+ int r;
+
+ assert(h);
+ assert(h->state == HOME_CREATING);
+
+ if (ret < 0) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ (void) convert_worker_errno(h, ret, &error);
+ log_error_errno(ret, "Operation on %s failed: %m", h->user_name);
+ h->current_operation = operation_result_unref(h->current_operation, ret, &error);
+
+ if (h->unregister_on_failure) {
+ (void) home_unlink_record(h);
+ h = home_free(h);
+ return;
+ }
+
+ home_set_state(h, _HOME_STATE_INVALID);
+ return;
+ }
+
+ if (hr) {
+ r = home_set_record(h, hr);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update home record, ignoring: %m");
+ }
+
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to save record to disk, ignoring: %m");
+
+ log_debug("Creation of %s completed.", h->user_name);
+
+ h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_change_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+
+ if (ret < 0) {
+ if (ret == -ENOKEY)
+ (void) home_count_bad_authentication(h, true);
+
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Change operation failed: %m");
+ goto finish;
+ }
+
+ if (hr) {
+ r = home_set_record(h, hr);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update home record, ignoring: %m");
+ else {
+ r = user_record_good_authentication(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ }
+ }
+
+ log_debug("Change operation of %s completed.", h->user_name);
+ r = 0;
+
+finish:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_locking_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(h->state == HOME_LOCKING);
+
+ if (ret < 0) {
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Locking operation failed: %m");
+ goto finish;
+ }
+
+ log_debug("Locking operation of %s completed.", h->user_name);
+ h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+ home_set_state(h, HOME_LOCKED);
+ return;
+
+finish:
+ /* If a specific home doesn't know the concept of locking, then that's totally OK, don't propagate
+ * the error if we are executing a LockAllHomes() operation. */
+
+ if (h->current_operation->type == OPERATION_LOCK_ALL && r == -ENOTTY)
+ h->current_operation = operation_result_unref(h->current_operation, 0, NULL);
+ else
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(IN_SET(h->state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
+
+ if (ret < 0) {
+ if (ret == -ENOKEY)
+ (void) home_count_bad_authentication(h, true);
+
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Unlocking operation failed: %m");
+ goto finish;
+ }
+
+ r = user_record_good_authentication(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+ else {
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ }
+
+ log_debug("Unlocking operation of %s completed.", h->user_name);
+ r = 0;
+
+finish:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) {
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(IN_SET(h->state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
+
+ if (ret < 0) {
+ if (ret == -ENOKEY)
+ (void) home_count_bad_authentication(h, true);
+
+ (void) convert_worker_errno(h, ret, &error);
+ r = log_error_errno(ret, "Authentication failed: %m");
+ goto finish;
+ }
+
+ if (hr) {
+ r = home_set_record(h, hr);
+ if (r < 0)
+ log_warning_errno(r, "Failed to update home record, ignoring: %m");
+ else {
+ r = user_record_good_authentication(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ }
+ }
+
+ log_debug("Authentication of %s completed.", h->user_name);
+ r = 0;
+
+finish:
+ h->current_operation = operation_result_unref(h->current_operation, r, &error);
+ home_set_state(h, _HOME_STATE_INVALID);
+}
+
+static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ Home *h = userdata;
+ int ret;
+
+ assert(s);
+ assert(si);
+ assert(h);
+
+ assert(h->worker_pid == si->si_pid);
+ assert(h->worker_event_source);
+ assert(h->worker_stdout_fd >= 0);
+
+ (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h);
+
+ h->worker_pid = 0;
+ h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+
+ if (si->si_code != CLD_EXITED) {
+ assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED));
+ ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker process died abnormally with signal %s.", signal_to_string(si->si_status));
+ } else if (si->si_status != EXIT_SUCCESS) {
+ /* If we received an error code via sd_notify(), use it */
+ if (h->worker_error_code != 0)
+ ret = log_debug_errno(h->worker_error_code, "Worker reported error code %s.", errno_to_name(h->worker_error_code));
+ else
+ ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker exited with exit code %i.", si->si_status);
+ } else
+ ret = home_parse_worker_stdout(TAKE_FD(h->worker_stdout_fd), &hr);
+
+ h->worker_stdout_fd = safe_close(h->worker_stdout_fd);
+
+ switch (h->state) {
+
+ case HOME_FIXATING:
+ case HOME_FIXATING_FOR_ACTIVATION:
+ case HOME_FIXATING_FOR_ACQUIRE:
+ home_fixate_finish(h, ret, hr);
+ break;
+
+ case HOME_ACTIVATING:
+ case HOME_ACTIVATING_FOR_ACQUIRE:
+ home_activate_finish(h, ret, hr);
+ break;
+
+ case HOME_DEACTIVATING:
+ home_deactivate_finish(h, ret, hr);
+ break;
+
+ case HOME_LOCKING:
+ home_locking_finish(h, ret, hr);
+ break;
+
+ case HOME_UNLOCKING:
+ case HOME_UNLOCKING_FOR_ACQUIRE:
+ home_unlocking_finish(h, ret, hr);
+ break;
+
+ case HOME_CREATING:
+ home_create_finish(h, ret, hr);
+ break;
+
+ case HOME_REMOVING:
+ home_remove_finish(h, ret, hr);
+ break;
+
+ case HOME_UPDATING:
+ case HOME_UPDATING_WHILE_ACTIVE:
+ case HOME_RESIZING:
+ case HOME_RESIZING_WHILE_ACTIVE:
+ case HOME_PASSWD:
+ case HOME_PASSWD_WHILE_ACTIVE:
+ home_change_finish(h, ret, hr);
+ break;
+
+ case HOME_AUTHENTICATING:
+ case HOME_AUTHENTICATING_WHILE_ACTIVE:
+ case HOME_AUTHENTICATING_FOR_ACQUIRE:
+ home_authenticating_finish(h, ret, hr);
+ break;
+
+ default:
+ assert_not_reached("Unexpected state after worker exited");
+ }
+
+ return 0;
+}
+
+static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(erase_and_freep) char *formatted = NULL;
+ _cleanup_close_ int stdin_fd = -1, stdout_fd = -1;
+ pid_t pid = 0;
+ int r;
+
+ assert(h);
+ assert(verb);
+ assert(hr);
+
+ if (h->worker_pid != 0)
+ return -EBUSY;
+
+ assert(h->worker_stdout_fd < 0);
+ assert(!h->worker_event_source);
+
+ v = json_variant_ref(hr->json);
+
+ if (secret) {
+ JsonVariant *sub = NULL;
+
+ sub = json_variant_by_key(secret->json, "secret");
+ if (!sub)
+ return -ENOKEY;
+
+ r = json_variant_set_field(&v, "secret", sub);
+ if (r < 0)
+ return r;
+ }
+
+ r = json_variant_format(v, 0, &formatted);
+ if (r < 0)
+ return r;
+
+ stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0);
+ if (stdin_fd < 0)
+ return stdin_fd;
+
+ log_debug("Sending to worker: %s", formatted);
+
+ stdout_fd = memfd_create("homework-stdout", MFD_CLOEXEC);
+ if (stdout_fd < 0)
+ return -errno;
+
+ r = safe_fork_full("(sd-homework)",
+ (int[]) { stdin_fd, stdout_fd }, 2,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+
+ if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) {
+ log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ r = rearrange_stdio(stdin_fd, stdout_fd, STDERR_FILENO);
+ if (r < 0) {
+ log_error_errno(r, "Failed to rearrange stdin/stdout/stderr: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */
+
+ // FIXME: drop this line
+ /* execl("/usr/bin/valgrind", "/usr/bin/valgrind", "/home/lennart/projects/systemd/build/systemd-homework", */
+ /* /\* SYSTEMD_HOMEWORK_PATH,*\/ verb, NULL); */
+ execl("/home/lennart/projects/systemd/build/systemd-homework",
+ SYSTEMD_HOMEWORK_PATH, verb, NULL);
+
+ execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL);
+ log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ r = sd_event_add_child(h->manager->event, &h->worker_event_source, pid, WEXITED, home_on_worker_process, h);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(h->worker_event_source, "worker");
+
+ r = hashmap_put(h->manager->homes_by_worker_pid, PID_TO_PTR(pid), h);
+ if (r < 0) {
+ h->worker_event_source = sd_event_source_unref(h->worker_event_source);
+ return r;
+ }
+
+ h->worker_stdout_fd = TAKE_FD(stdout_fd);
+ h->worker_pid = pid;
+ h->worker_error_code = 0;
+
+ return 0;
+}
+
+static int home_ratelimit(Home *h, sd_bus_error *error) {
+ int r, ret;
+
+ assert(h);
+
+ ret = user_record_ratelimit(h->record);
+ if (ret < 0)
+ return ret;
+
+ if (h->state != HOME_UNFIXATED) {
+ r = home_save_record(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to save updated record, ignoring: %m");
+ }
+
+ if (ret == 0) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ usec_t t, n;
+
+ n = now(CLOCK_REALTIME);
+ t = user_record_ratelimit_next_try(h->record);
+
+ if (t != USEC_INFINITY && t > n)
+ return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again in %s!",
+ format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC));
+
+ return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again later.");
+ }
+
+ return 0;
+}
+
+static int home_fixate_internal(
+ Home *h,
+ UserRecord *secret,
+ HomeState for_state,
+ sd_bus_error *error) {
+
+ int r;
+
+ assert(h);
+ assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
+
+ r = home_start_work(h, "inspect", h->record, secret);
+ if (r < 0)
+ return r;
+
+ if (for_state == HOME_FIXATING_FOR_ACTIVATION) {
+ /* Remember the secret data, since we need it for the activation again, later on. */
+ user_record_unref(h->secret);
+ h->secret = user_record_ref(secret);
+ }
+
+ home_set_state(h, for_state);
+ return 0;
+}
+
+int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name);
+ case HOME_UNFIXATED:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ return home_fixate_internal(h, secret, HOME_FIXATING, error);
+}
+
+static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+ assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
+
+ r = home_start_work(h, "activate", h->record, secret);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, for_state);
+ return 0;
+}
+
+int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_UNFIXATED:
+ return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error);
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_ACTIVE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_ACTIVE, "Home %s is already active.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_INACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ return home_activate_internal(h, secret, HOME_ACTIVATING, error);
+}
+
+static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+ assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
+
+ r = home_start_work(h, "inspect", h->record, secret);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, for_state);
+ return 0;
+}
+
+int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) {
+ HomeState state;
+ int r;
+
+ assert(h);
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_UNFIXATED:
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ return home_authenticate_internal(h, secret, state == HOME_ACTIVE ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error);
+}
+
+static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, HOME_DEACTIVATING);
+ return 0;
+}
+
+int home_deactivate(Home *h, bool force, sd_bus_error *error) {
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_ACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ return home_deactivate_internal(h, force, error);
+}
+
+int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_INACTIVE:
+ if (h->record->storage < 0)
+ break; /* if no storage is defined we don't know what precisely to look for, hence
+ * HOME_INACTIVE is OK in that case too. */
+
+ if (IN_SET(user_record_test_image_path(h->record), USER_TEST_MAYBE, USER_TEST_UNDEFINED))
+ break; /* And if the image path test isn't conclusive, let's also go on */
+
+ _fallthrough_;
+ case HOME_UNFIXATED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name);
+ case HOME_ABSENT:
+ break;
+ case HOME_ACTIVE:
+ case HOME_LOCKED:
+ default:
+ 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);
+ }
+
+ if (h->record->enforce_password_policy == false)
+ log_debug("Password quality check turned off for account, skipping.");
+ else {
+ r = quality_check_password(h->record, secret, error);
+ if (r < 0)
+ return r;
+ }
+
+ r = home_start_work(h, "create", h->record, secret);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, HOME_CREATING);
+ return 0;
+}
+
+int home_remove(Home *h, sd_bus_error *error) {
+ HomeState state;
+ int r;
+
+ assert(h);
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_ABSENT: /* If the home directory is absent, then this is just like unregistering */
+ return home_unregister(h, error);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_UNFIXATED:
+ case HOME_INACTIVE:
+ break;
+ case HOME_ACTIVE:
+ default:
+ 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);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, HOME_REMOVING);
+ return 0;
+}
+
+static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_binding, UserRecordLoadFlags flags, UserRecord **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *nr = NULL;
+ JsonVariant *binding;
+ int r;
+
+ assert(hr);
+ assert(with_binding);
+ assert(ret);
+
+ assert_se(v = json_variant_ref(hr->json));
+
+ binding = json_variant_by_key(with_binding->json, "binding");
+ if (binding) {
+ r = json_variant_set_field(&v, "binding", binding);
+ if (r < 0)
+ return r;
+ }
+
+ nr = user_record_new();
+ if (!nr)
+ return -ENOMEM;
+
+ r = user_record_load(nr, v, flags);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(nr);
+ return 0;
+}
+
+static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) {
+ _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL;
+ int r, c;
+
+ assert(h);
+ assert(verb);
+ assert(hr);
+
+ if (!user_record_compatible(hr, h->record))
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Updated user record is not compatible with existing one.");
+ c = user_record_compare_last_change(hr, h->record); /* refuse downgrades */
+ if (c < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_DOWNGRADE, "Refusing to update to older home record.");
+
+ if (!secret && FLAGS_SET(hr->mask, USER_RECORD_SECRET)) {
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_SECRET, &saved_secret);
+ if (r < 0)
+ return r;
+
+ secret = saved_secret;
+ }
+
+ r = manager_verify_user_record(h->manager, hr);
+ switch (r) {
+
+ case USER_RECORD_UNSIGNED:
+ if (h->signed_locally <= 0) /* If the existing record is not owned by us, don't accept an
+ * unsigned new record. i.e. only implicitly sign new records
+ * that where previously signed by us too. */
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+
+ /* The updated record is not signed, then do so now */
+ r = manager_sign_user_record(h->manager, hr, &signed_hr, error);
+ if (r < 0)
+ return r;
+
+ hr = signed_hr;
+ break;
+
+ case USER_RECORD_SIGNED_EXCLUSIVE:
+ case USER_RECORD_SIGNED:
+ case USER_RECORD_FOREIGN:
+ /* Has already been signed. Great! */
+ break;
+
+ case -ENOKEY:
+ default:
+ return r;
+ }
+
+ r = user_record_extend_with_binding(hr, h->record, USER_RECORD_LOAD_MASK_SECRET, &new_hr);
+ if (r < 0)
+ return r;
+
+ if (c == 0) {
+ /* different payload but same lastChangeUSec field? That's not cool! */
+
+ r = user_record_masked_equal(new_hr, h->record, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing.");
+ }
+
+ r = home_start_work(h, verb, new_hr, secret);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
+ HomeState state;
+ int r;
+
+ assert(h);
+ assert(hr);
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_UNFIXATED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ r = home_update_internal(h, "update", hr, NULL, error);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, state == HOME_ACTIVE ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING);
+ return 0;
+}
+
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) {
+ _cleanup_(user_record_unrefp) UserRecord *c = NULL;
+ HomeState state;
+ int r;
+
+ assert(h);
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_UNFIXATED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) {
+ if (h->record->disk_size == UINT64_MAX)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not disk size to resize to specified.");
+
+ c = user_record_ref(h->record); /* Shortcut if size is unspecified or matches the record */
+ } else {
+ _cleanup_(user_record_unrefp) UserRecord *signed_c = NULL;
+
+ if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+
+ r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c);
+ if (r < 0)
+ return r;
+
+ r = user_record_set_disk_size(c, disk_size);
+ if (r == -ERANGE)
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "Requested size for home %s out of acceptable range.", h->user_name);
+ if (r < 0)
+ return r;
+
+ r = user_record_update_last_changed(c, false);
+ if (r == -ECHRNG)
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name);
+ if (r < 0)
+ return r;
+
+ r = manager_sign_user_record(h->manager, c, &signed_c, error);
+ if (r < 0)
+ return r;
+
+ user_record_unref(c);
+ c = TAKE_PTR(signed_c);
+ }
+
+ r = home_update_internal(h, "resize", c, secret, error);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, state == HOME_ACTIVE ? HOME_RESIZING_WHILE_ACTIVE : HOME_RESIZING);
+ return 0;
+}
+
+int home_passwd(Home *h,
+ UserRecord *new_secret,
+ UserRecord *old_secret,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *c = NULL, *merged_secret = NULL, *signed_c = NULL;
+ HomeState state;
+ int r;
+
+ assert(h);
+
+ if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name);
+ if (strv_isempty(new_secret->password))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No password to set specified.");
+
+ state = home_get_state(h);
+ switch (state) {
+ case HOME_UNFIXATED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name);
+ case HOME_ABSENT:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c);
+ if (r < 0)
+ return r;
+
+ merged_secret = user_record_new();
+ if (!merged_secret)
+ return -ENOMEM;
+
+ r = user_record_merge_secret(merged_secret, old_secret);
+ if (r < 0)
+ return r;
+
+ r = user_record_merge_secret(merged_secret, new_secret);
+ if (r < 0)
+ return r;
+
+ r = user_record_make_hashed_password(c, new_secret);
+ if (r < 0)
+ return r;
+
+ r = user_record_update_last_changed(c, true);
+ if (r == -ECHRNG)
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name);
+ if (r < 0)
+ return r;
+
+ r = manager_sign_user_record(h->manager, c, &signed_c, error);
+ if (r < 0)
+ return r;
+
+ if (c->enforce_password_policy == false)
+ log_debug("Password quality check turned off for account, skipping.");
+ else {
+ r = quality_check_password(c, merged_secret, error);
+ if (r < 0)
+ return r;
+ }
+
+ r = home_update_internal(h, "passwd", signed_c, merged_secret, error);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, state == HOME_ACTIVE ? HOME_PASSWD_WHILE_ACTIVE : HOME_PASSWD);
+ return 0;
+}
+
+int home_unregister(Home *h, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_UNFIXATED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s is not registered.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ break;
+ case HOME_ACTIVE:
+ default:
+ 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_unlink_record(h);
+ if (r < 0)
+ return r;
+
+ /* And destroy the whole entry. The caller needs to be prepared for that. */
+ h = home_free(h);
+ return 1;
+}
+
+int home_lock(Home *h, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ switch (home_get_state(h)) {
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name);
+ case HOME_LOCKED:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name);
+ case HOME_ACTIVE:
+ break;
+ default:
+ 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);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, HOME_LOCKING);
+ return 0;
+}
+
+static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+ assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
+
+ r = home_start_work(h, "unlock", h->record, secret);
+ if (r < 0)
+ return r;
+
+ home_set_state(h, for_state);
+ return 0;
+}
+
+int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
+ int r;
+ assert(h);
+
+ r = home_ratelimit(h, error);
+ if (r < 0)
+ return r;
+
+ switch (home_get_state(h)) {
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ case HOME_ACTIVE:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name);
+ case HOME_LOCKED:
+ break;
+ default:
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ }
+
+ return home_unlock_internal(h, secret, HOME_UNLOCKING, error);
+}
+
+HomeState home_get_state(Home *h) {
+ assert(h);
+
+ /* When the state field is initialized, it counts. */
+ if (h->state >= 0)
+ return h->state;
+
+ /* Otherwise, let's see if the home directory is mounted. If so, we assume for sure the home
+ * directory is active */
+ if (user_record_test_home_directory(h->record) == USER_TEST_MOUNTED)
+ return HOME_ACTIVE;
+
+ /* And if we see the image being gone, we report this as absent */
+ if (user_record_test_image_path(h->record) == USER_TEST_ABSENT)
+ return HOME_ABSENT;
+
+ /* And for all other cases we return "inactive". */
+ return HOME_INACTIVE;
+}
+
+void home_process_notify(Home *h, char **l) {
+ const char *e;
+ int error;
+ int r;
+
+ assert(h);
+
+ e = strv_env_get(l, "ERRNO");
+ if (!e) {
+ log_debug("Got notify message lacking ERRNO= field, ignoring.");
+ return;
+ }
+
+ r = safe_atoi(e, &error);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to parse receieved error number, ignoring: %s", e);
+ return;
+ }
+ if (error <= 0) {
+ log_debug("Error number is out of range: %i", error);
+ return;
+ }
+
+ h->worker_error_code = error;
+}
+
+int home_killall(Home *h) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ assert(h);
+
+ if (!uid_is_valid(h->uid))
+ return 0;
+
+ assert(h->uid > 0); /* We never should be UID 0 */
+
+ /* Let's kill everything matching the specified UID */
+ r = safe_fork("(sd-killer)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT|FORK_LOG, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+
+ if (setresgid(h->uid, h->uid, h->uid) < 0) {
+ log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", h->uid);
+ _exit(EXIT_FAILURE);
+ }
+
+ if (setgroups(0, NULL) < 0) {
+ log_error_errno(errno, "Failed to reset auxiliary groups list: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (setresuid(h->uid, h->uid, h->uid) < 0) {
+ log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid);
+ _exit(EXIT_FAILURE);
+ }
+
+ if (kill(-1, SIGKILL) < 0) {
+ log_error_errno(errno, "Failed to kill all processes of UID " UID_FMT ": %m", h->uid);
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Let's also kill everything in the user's slice */
+ if (asprintf(&unit, "user-" UID_FMT ".slice", h->uid) < 0)
+ return log_oom();
+
+ r = sd_bus_call_method(
+ h->manager->bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "KillUnit",
+ &error,
+ NULL,
+ "ssi", unit, "all", SIGKILL);
+ if (r < 0)
+ log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ? LOG_DEBUG : LOG_WARNING,
+ r, "Failed to kill login processes of user, ignoring: %s", bus_error_message(&error, r));
+
+ return 1;
+}
+
+static int home_get_disk_status_luks(
+ Home *h,
+ HomeState state,
+ uint64_t *ret_disk_size,
+ uint64_t *ret_disk_usage,
+ uint64_t *ret_disk_free,
+ uint64_t *ret_disk_ceiling,
+ uint64_t *ret_disk_floor) {
+
+ uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX,
+ disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX,
+ stat_used = UINT64_MAX, fs_size = UINT64_MAX, header_size = 0;
+
+ struct statfs sfs;
+ const char *hd;
+ int r;
+
+ assert(h);
+ assert(ret_disk_size);
+ assert(ret_disk_usage);
+ assert(ret_disk_free);
+ assert(ret_disk_ceiling);
+
+ if (state != HOME_ABSENT) {
+ const char *ip;
+
+ ip = user_record_image_path(h->record);
+ if (ip) {
+ struct stat st;
+
+ if (stat(ip, &st) < 0)
+ log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", ip);
+ else if (S_ISREG(st.st_mode)) {
+ _cleanup_free_ char *parent = NULL;
+
+ disk_size = st.st_size;
+ stat_used = st.st_blocks * 512;
+
+ parent = dirname_malloc(ip);
+ if (!parent)
+ return log_oom();
+
+ if (statfs(parent, &sfs) < 0)
+ log_debug_errno(r, "Failed to statfs() %s, ignoring: %m", parent);
+ else
+ disk_ceiling = stat_used + sfs.f_bsize * sfs.f_bavail;
+
+ } else if (S_ISBLK(st.st_mode)) {
+ _cleanup_free_ char *szbuf = NULL;
+ char p[SYS_BLOCK_PATH_MAX("/size")];
+
+ /* Let's read the size off sysfs, so that we don't have to open the device */
+ xsprintf_sys_block_path(p, "/size", st.st_rdev);
+ r = read_one_line_file(p, &szbuf);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read %s, ignoring: %m", p);
+ else {
+ uint64_t sz;
+
+ r = safe_atou64(szbuf, &sz);
+ if (r < 0)
+ log_debug_errno(r, "Failed to parse %s, ignoring: %s", p, szbuf);
+ else
+ disk_size = sz * 512;
+ }
+ } else
+ log_debug("Image path is not a block device or regular file, not able to acquire size.");
+ }
+ }
+
+ if (!HOME_STATE_IS_ACTIVE(state))
+ goto finish;
+
+ hd = user_record_home_directory(h->record);
+ if (!hd)
+ goto finish;
+
+ if (statfs(hd, &sfs) < 0) {
+ log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", hd);
+ goto finish;
+ }
+
+ disk_free = sfs.f_bsize * sfs.f_bavail;
+ fs_size = sfs.f_bsize * sfs.f_blocks;
+ if (disk_size != UINT64_MAX && disk_size > fs_size)
+ header_size = disk_size - fs_size;
+
+ /* We take a perspective from the user here (as opposed to from the host): the used disk space is the
+ * difference from the limit and what's free. This makes a difference if sparse mode is not used: in
+ * that case the image is pre-allocated and thus appears all used from the host PoV but is not used
+ * up at all yet from the user's PoV.
+ *
+ * That said, we use use the stat() reported loopback file size as upper boundary: our footprint can
+ * never be larger than what we take up on the lowest layers. */
+
+ if (disk_size != UINT64_MAX && disk_size > disk_free) {
+ disk_usage = disk_size - disk_free;
+
+ if (stat_used != UINT64_MAX && disk_usage > stat_used)
+ disk_usage = stat_used;
+ } else
+ disk_usage = stat_used;
+
+ /* If we have the magic, determine floor preferably by magic */
+ disk_floor = minimal_size_by_fs_magic(sfs.f_type) + header_size;
+
+finish:
+ /* If we don't know the magic, go by file system name */
+ if (disk_floor == UINT64_MAX)
+ disk_floor = minimal_size_by_fs_name(user_record_file_system_type(h->record));
+
+ *ret_disk_size = disk_size;
+ *ret_disk_usage = disk_usage;
+ *ret_disk_free = disk_free;
+ *ret_disk_ceiling = disk_ceiling;
+ *ret_disk_floor = disk_floor;
+
+ return 0;
+}
+
+static int home_get_disk_status_directory(
+ Home *h,
+ HomeState state,
+ uint64_t *ret_disk_size,
+ uint64_t *ret_disk_usage,
+ uint64_t *ret_disk_free,
+ uint64_t *ret_disk_ceiling,
+ uint64_t *ret_disk_floor) {
+
+ uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX,
+ disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX;
+ _cleanup_free_ char *devnode = NULL;
+ struct statfs sfs;
+ struct dqblk req;
+ const char *path = NULL;
+ dev_t devno;
+ int r;
+
+ assert(ret_disk_size);
+ assert(ret_disk_usage);
+ assert(ret_disk_free);
+ assert(ret_disk_ceiling);
+ assert(ret_disk_floor);
+
+ if (HOME_STATE_IS_ACTIVE(state))
+ path = user_record_home_directory(h->record);
+
+ if (!path) {
+ if (state == HOME_ABSENT)
+ goto finish;
+
+ path = user_record_image_path(h->record);
+ }
+
+ if (!path)
+ goto finish;
+
+ if (statfs(path, &sfs) < 0)
+ log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", path);
+ else {
+ disk_free = sfs.f_bsize * sfs.f_bavail;
+ disk_size = sfs.f_bsize * sfs.f_blocks;
+
+ /* We don't initialize disk_usage from statfs() data here, since the device is likely not used
+ * by us alone, and disk_usage should only reflect our own use. */
+ }
+
+ if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME)) {
+
+ r = btrfs_is_subvol(path);
+ if (r < 0)
+ log_debug_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path);
+ else if (r > 0) {
+ BtrfsQuotaInfo qi;
+
+ r = btrfs_subvol_get_subtree_quota(path, 0, &qi);
+ if (r < 0)
+ log_debug_errno(r, "Failed to query btrfs subtree quota, ignoring: %m");
+ else {
+ disk_usage = qi.referenced;
+
+ if (disk_free != UINT64_MAX) {
+ disk_ceiling = qi.referenced + disk_free;
+
+ if (disk_size != UINT64_MAX && disk_ceiling > disk_size)
+ disk_ceiling = disk_size;
+ }
+
+ if (qi.referenced_max != UINT64_MAX) {
+ if (disk_size != UINT64_MAX)
+ disk_size = MIN(qi.referenced_max, disk_size);
+ else
+ disk_size = qi.referenced_max;
+ }
+
+ if (disk_size != UINT64_MAX) {
+ if (disk_size > disk_usage)
+ disk_free = disk_size - disk_usage;
+ else
+ disk_free = 0;
+ }
+ }
+
+ goto finish;
+ }
+ }
+
+ if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_FSCRYPT)) {
+
+ r = get_block_device(path, &devno);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to determine block device of %s, ignoring: %m", path);
+ goto finish;
+ }
+ if (devno == 0) {
+ log_debug("File system %s not backed by a block device, can't get quota, ignoring.", path);
+ goto finish;
+ }
+ r = device_path_make_major_minor(S_IFBLK, devno, &devnode);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to derive block device path for file system %s, ignoring: %m", path);
+ goto finish;
+ }
+
+ if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) {
+
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_debug_errno(errno, "No UID quota support on %s.", path);
+ goto finish;
+ }
+
+ if (errno != ESRCH) {
+ log_debug_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid);
+ goto finish;
+ }
+
+ disk_usage = 0; /* No record of this user? then nothing was used */
+ } else {
+ if (FLAGS_SET(req.dqb_valid, QIF_SPACE) && disk_free != UINT64_MAX) {
+ disk_ceiling = req.dqb_curspace + disk_free;
+
+ if (disk_size != UINT64_MAX && disk_ceiling > disk_size)
+ disk_ceiling = disk_size;
+ }
+
+ if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS)) {
+ uint64_t q;
+
+ /* Take the minimum of the quota and the available disk space here */
+ q = req.dqb_bhardlimit * QIF_DQBLKSIZE;
+ if (disk_size != UINT64_MAX)
+ disk_size = MIN(disk_size, q);
+ else
+ disk_size = q;
+ }
+ if (FLAGS_SET(req.dqb_valid, QIF_SPACE)) {
+ disk_usage = req.dqb_curspace;
+
+ if (disk_size != UINT64_MAX) {
+ if (disk_size > disk_usage)
+ disk_free = disk_size - disk_usage;
+ else
+ disk_free = 0;
+ }
+ }
+ }
+ }
+
+finish:
+ *ret_disk_size = disk_size;
+ *ret_disk_usage = disk_usage;
+ *ret_disk_free = disk_free;
+ *ret_disk_ceiling = disk_ceiling;
+ *ret_disk_floor = disk_floor;
+
+ return 0;
+}
+
+int home_augment_status(
+ Home *h,
+ UserRecordLoadFlags flags,
+ UserRecord **ret) {
+
+ uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX;
+ _cleanup_(json_variant_unrefp) JsonVariant *j = NULL, *v = NULL, *m = NULL, *status = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ char ids[SD_ID128_STRING_MAX];
+ HomeState state;
+ sd_id128_t id;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ /* We are supposed to add this, this can't be on hence. */
+ assert(!FLAGS_SET(flags, USER_RECORD_STRIP_STATUS));
+
+ r = sd_id128_get_machine(&id);
+ if (r < 0)
+ return r;
+
+ state = home_get_state(h);
+
+ switch (h->record->storage) {
+
+ case USER_LUKS:
+ r = home_get_disk_status_luks(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_CLASSIC:
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ case USER_CIFS:
+ r = home_get_disk_status_directory(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor);
+ if (r < 0)
+ return r;
+
+ break;
+
+ default:
+ ; /* unset */
+ }
+
+ if (disk_floor == UINT64_MAX || (disk_usage != UINT64_MAX && disk_floor < disk_usage))
+ disk_floor = disk_usage;
+ if (disk_floor == UINT64_MAX || disk_floor < USER_DISK_SIZE_MIN)
+ disk_floor = USER_DISK_SIZE_MIN;
+ if (disk_ceiling == UINT64_MAX || disk_ceiling > USER_DISK_SIZE_MAX)
+ disk_ceiling = USER_DISK_SIZE_MAX;
+
+ r = json_build(&status,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))),
+ JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")),
+ JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)),
+ JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)),
+ JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)),
+ JSON_BUILD_PAIR_CONDITION(disk_ceiling != UINT64_MAX, "diskCeiling", JSON_BUILD_UNSIGNED(disk_ceiling)),
+ JSON_BUILD_PAIR_CONDITION(disk_floor != UINT64_MAX, "diskFloor", JSON_BUILD_UNSIGNED(disk_floor)),
+ JSON_BUILD_PAIR_CONDITION(h->signed_locally >= 0, "signedLocally", JSON_BUILD_BOOLEAN(h->signed_locally))
+ ));
+ if (r < 0)
+ return r;
+
+ j = json_variant_ref(h->record->json);
+ v = json_variant_ref(json_variant_by_key(j, "status"));
+ m = json_variant_ref(json_variant_by_key(v, sd_id128_to_string(id, ids)));
+
+ r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally"));
+ if (r < 0)
+ return r;
+
+ r = json_variant_merge(&m, status);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&v, ids, m);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&j, "status", v);
+ if (r < 0)
+ return r;
+
+ ur = user_record_new();
+ if (!ur)
+ return -ENOMEM;
+
+ r = user_record_load(ur, j, flags);
+ if (r < 0)
+ return r;
+
+ ur->incomplete =
+ FLAGS_SET(h->record->mask, USER_RECORD_PRIVILEGED) &&
+ !FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED);
+
+ *ret = TAKE_PTR(ur);
+ return 0;
+}
+
+static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+ Home *h = userdata;
+
+ assert(s);
+ assert(h);
+
+ if (h->ref_event_source_please_suspend == s)
+ h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend);
+
+ if (h->ref_event_source_dont_suspend == s)
+ h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend);
+
+ if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+ return 0;
+
+ log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name);
+
+ o = operation_new(OPERATION_PIPE_EOF, NULL);
+ if (!o) {
+ log_oom();
+ return 0;
+ }
+
+ home_schedule_operation(h, o, NULL);
+ return 0;
+}
+
+int home_create_fifo(Home *h, bool please_suspend) {
+ _cleanup_close_ int ret_fd = -1;
+ sd_event_source **ss;
+ const char *fn, *suffix;
+ int r;
+
+ assert(h);
+
+ if (please_suspend) {
+ suffix = ".please-suspend";
+ ss = &h->ref_event_source_please_suspend;
+ } else {
+ suffix = ".dont-suspend";
+ ss = &h->ref_event_source_dont_suspend;
+ }
+
+ fn = strjoina("/run/systemd/home/", h->user_name, suffix);
+
+ if (!*ss) {
+ _cleanup_close_ int ref_fd = -1;
+
+ (void) mkdir("/run/systemd/home/", 0755);
+ if (mkfifo(fn, 0600) < 0 && errno != EEXIST)
+ return log_error_errno(errno, "Failed to create FIFO %s: %m", fn);
+
+ ref_fd = open(fn, O_RDONLY|O_CLOEXEC|O_NONBLOCK);
+ if (ref_fd < 0)
+ return log_error_errno(errno, "Failed to open FIFO %s for reading: %m", fn);
+
+ r = sd_event_add_io(h->manager->event, ss, ref_fd, 0, on_home_ref_eof, h);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate reference FIFO event source: %m");
+
+ (void) sd_event_source_set_description(*ss, "acquire-ref");
+
+ r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_io_fd_own(*ss, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pass ownership of FIFO event fd to event source: %m");
+
+ TAKE_FD(ref_fd);
+ }
+
+ ret_fd = open(fn, O_WRONLY|O_CLOEXEC|O_NONBLOCK);
+ if (ret_fd < 0)
+ return log_error_errno(errno, "Failed to open FIFO %s for writing: %m", fn);
+
+ return TAKE_FD(ret_fd);
+}
+
+static int home_dispatch_acquire(Home *h, Operation *o) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int (*call)(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) = NULL;
+ HomeState for_state;
+ int r;
+
+ assert(h);
+ assert(o);
+ assert(o->type == OPERATION_ACQUIRE);
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ for_state = HOME_FIXATING_FOR_ACQUIRE;
+ call = home_fixate_internal;
+ break;
+
+ case HOME_ABSENT:
+ r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ break;
+
+ case HOME_INACTIVE:
+ for_state = HOME_ACTIVATING_FOR_ACQUIRE;
+ call = home_activate_internal;
+ break;
+
+ case HOME_ACTIVE:
+ for_state = HOME_AUTHENTICATING_FOR_ACQUIRE;
+ call = home_authenticate_internal;
+ break;
+
+ case HOME_LOCKED:
+ for_state = HOME_UNLOCKING_FOR_ACQUIRE;
+ call = home_unlock_internal;
+ break;
+
+ default:
+ /* All other cases means we are currently executing an operation, which means the job remains
+ * pending. */
+ return 0;
+ }
+
+ assert(!h->current_operation);
+
+ if (call) {
+ r = home_ratelimit(h, &error);
+ if (r >= 0)
+ r = call(h, o->secret, for_state, &error);
+ }
+
+ if (r != 0) /* failure or completed */
+ operation_result(o, r, &error);
+ else /* ongoing */
+ h->current_operation = operation_ref(o);
+
+ return 1;
+}
+
+static int home_dispatch_release(Home *h, Operation *o) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(o);
+ assert(o->type == OPERATION_RELEASE);
+
+ if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+ /* If there's now a reference again, then let's abort the release attempt */
+ r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name);
+ else {
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ r = 0; /* done */
+ break;
+
+ case HOME_LOCKED:
+ r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
+ break;
+
+ case HOME_ACTIVE:
+ r = home_deactivate_internal(h, false, &error);
+ break;
+
+ default:
+ /* All other cases means we are currently executing an operation, which means the job remains
+ * pending. */
+ return 0;
+ }
+ }
+
+ assert(!h->current_operation);
+
+ if (r <= 0) /* failure or completed */
+ operation_result(o, r, &error);
+ else /* ongoing */
+ h->current_operation = operation_ref(o);
+
+ return 1;
+}
+
+static int home_dispatch_lock_all(Home *h, Operation *o) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(o);
+ assert(o->type == OPERATION_LOCK_ALL);
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ log_info("Home %s is not active, no locking necessary.", h->user_name);
+ r = 0; /* done */
+ break;
+
+ case HOME_LOCKED:
+ log_info("Home %s is already locked.", h->user_name);
+ r = 0; /* done */
+ break;
+
+ case HOME_ACTIVE:
+ log_info("Locking home %s.", h->user_name);
+ r = home_lock(h, &error);
+ break;
+
+ default:
+ /* All other cases means we are currently executing an operation, which means the job remains
+ * pending. */
+ return 0;
+ }
+
+ assert(!h->current_operation);
+
+ if (r != 0) /* failure or completed */
+ operation_result(o, r, &error);
+ else /* ongoing */
+ h->current_operation = operation_ref(o);
+
+ return 1;
+}
+
+static int home_dispatch_pipe_eof(Home *h, Operation *o) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(o);
+ assert(o->type == OPERATION_PIPE_EOF);
+
+ if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend)
+ return 1; /* Hmm, there's a reference again, let's cancel this */
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name);
+ break;
+
+ case HOME_DEACTIVATING:
+ log_info("Home %s is already being deactivated, automatic deactivated unnecessary.", h->user_name);
+ break;
+
+ case HOME_ACTIVE:
+ r = home_deactivate_internal(h, false, &error);
+ if (r < 0)
+ log_warning_errno(r, "Failed to deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r));
+ break;
+
+ case HOME_LOCKED:
+ default:
+ /* If the device is locked or any operation is being executed, let's leave this pending */
+ return 0;
+ }
+
+ /* Note that we don't call operation_fail() or operation_success() here, because this kind of
+ * operation has no message associated with it, and thus there's no need to propagate success. */
+
+ assert(!o->message);
+ return 1;
+}
+
+static int home_dispatch_deactivate_force(Home *h, Operation *o) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(h);
+ assert(o);
+ assert(o->type == OPERATION_DEACTIVATE_FORCE);
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name);
+ break;
+
+ case HOME_DEACTIVATING:
+ log_debug("Home %s is already being deactivated, forced deactivation due to unplug unnecessary.", h->user_name);
+ break;
+
+ case HOME_ACTIVE:
+ case HOME_LOCKED:
+ r = home_deactivate_internal(h, true, &error);
+ if (r < 0)
+ log_warning_errno(r, "Failed to forcibly deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r));
+ break;
+
+ default:
+ /* If any operation is being executed, let's leave this pending */
+ return 0;
+ }
+
+ /* Note that we don't call operation_fail() or operation_success() here, because this kind of
+ * operation has no message associated with it, and thus there's no need to propagate success. */
+
+ assert(!o->message);
+ return 1;
+}
+
+static int on_pending(sd_event_source *s, void *userdata) {
+ Home *h = userdata;
+ Operation *o;
+ int r;
+
+ assert(s);
+ assert(h);
+
+ o = ordered_set_first(h->pending_operations);
+ if (o) {
+ static int (* const operation_table[_OPERATION_MAX])(Home *h, Operation *o) = {
+ [OPERATION_ACQUIRE] = home_dispatch_acquire,
+ [OPERATION_RELEASE] = home_dispatch_release,
+ [OPERATION_LOCK_ALL] = home_dispatch_lock_all,
+ [OPERATION_PIPE_EOF] = home_dispatch_pipe_eof,
+ [OPERATION_DEACTIVATE_FORCE] = home_dispatch_deactivate_force,
+ };
+
+ assert(operation_table[o->type]);
+ r = operation_table[o->type](h, o);
+ if (r != 0) {
+ /* The operation completed, let's remove it from the pending list, and exit while
+ * leaving the event source enabled as it is. */
+ assert_se(ordered_set_remove(h->pending_operations, o) == o);
+ operation_unref(o);
+ return 0;
+ }
+ }
+
+ /* Nothing to do anymore, let's turn off this event source */
+ r = sd_event_source_set_enabled(s, SD_EVENT_OFF);
+ if (r < 0)
+ return log_error_errno(r, "Failed to disable event source: %m");
+
+ return 0;
+}
+
+int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) {
+ int r;
+
+ assert(h);
+
+ if (o) {
+ if (ordered_set_size(h->pending_operations) >= PENDING_OPERATIONS_MAX)
+ return sd_bus_error_setf(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested");
+
+ r = ordered_set_ensure_allocated(&h->pending_operations, &operation_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = ordered_set_put(h->pending_operations, o);
+ if (r < 0)
+ return r;
+
+ operation_ref(o);
+ }
+
+ if (!h->pending_event_source) {
+ r = sd_event_add_defer(h->manager->event, &h->pending_event_source, on_pending, h);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate pending defer event source: %m");
+
+ (void) sd_event_source_set_description(h->pending_event_source, "pending");
+
+ r = sd_event_source_set_priority(h->pending_event_source, SD_EVENT_PRIORITY_IDLE);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_event_source_set_enabled(h->pending_event_source, SD_EVENT_ON);
+ if (r < 0)
+ return log_error_errno(r, "Failed to trigger pending event source: %m");
+
+ return 0;
+}
+
+static int home_get_image_path_seat(Home *h, char **ret) {
+ _cleanup_(sd_device_unrefp) sd_device *d = NULL;
+ _cleanup_free_ char *c = NULL;
+ const char *ip, *seat;
+ struct stat st;
+ int r;
+
+ assert(h);
+
+ if (user_record_storage(h->record) != USER_LUKS)
+ return -ENXIO;
+
+ ip = user_record_image_path(h->record);
+ if (!ip)
+ return -ENXIO;
+
+ if (!path_startswith(ip, "/dev/"))
+ return -ENXIO;
+
+ if (stat(ip, &st) < 0)
+ return -errno;
+
+ if (!S_ISBLK(st.st_mode))
+ return -ENOTBLK;
+
+ r = sd_device_new_from_devnum(&d, 'b', st.st_rdev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_property_value(d, "ID_SEAT", &seat);
+ if (r == -ENOENT) /* no property means seat0 */
+ seat = "seat0";
+ else if (r < 0)
+ return r;
+
+ c = strdup(seat);
+ if (!c)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
+
+int home_auto_login(Home *h, char ***ret_seats) {
+ _cleanup_free_ char *seat = NULL, *seat2 = NULL;
+
+ assert(h);
+ assert(ret_seats);
+
+ (void) home_get_image_path_seat(h, &seat);
+
+ if (h->record->auto_login > 0 && !streq_ptr(seat, "seat0")) {
+ /* For now, when the auto-login boolean is set for a user, let's make it mean
+ * "seat0". Eventually we can extend the concept and allow configuration of any kind of seat,
+ * but let's keep simple initially, most likely the feature is interesting on single-user
+ * systems anyway, only.
+ *
+ * We filter out users marked for auto-login in we know for sure their home directory is
+ * absent. */
+
+ if (user_record_test_image_path(h->record) != USER_TEST_ABSENT) {
+ seat2 = strdup("seat0");
+ if (!seat2)
+ return -ENOMEM;
+ }
+ }
+
+ if (seat || seat2) {
+ _cleanup_strv_free_ char **list = NULL;
+ size_t i = 0;
+
+ list = new(char*, 3);
+ if (!list)
+ return -ENOMEM;
+
+ if (seat)
+ list[i++] = TAKE_PTR(seat);
+ if (seat2)
+ list[i++] = TAKE_PTR(seat2);
+
+ list[i] = NULL;
+ *ret_seats = TAKE_PTR(list);
+ return 1;
+ }
+
+ *ret_seats = NULL;
+ return 0;
+}
+
+int home_set_current_message(Home *h, sd_bus_message *m) {
+ assert(h);
+
+ if (!m)
+ return 0;
+
+ if (h->current_operation)
+ return -EBUSY;
+
+ h->current_operation = operation_new(OPERATION_IMMEDIATE, m);
+ if (!h->current_operation)
+ return -ENOMEM;
+
+ return 1;
+}
+
+static const char* const home_state_table[_HOME_STATE_MAX] = {
+ [HOME_UNFIXATED] = "unfixated",
+ [HOME_ABSENT] = "absent",
+ [HOME_INACTIVE] = "inactive",
+ [HOME_FIXATING] = "fixating",
+ [HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation",
+ [HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire",
+ [HOME_ACTIVATING] = "activating",
+ [HOME_ACTIVATING_FOR_ACQUIRE] = "activating-for-acquire",
+ [HOME_DEACTIVATING] = "deactivating",
+ [HOME_ACTIVE] = "active",
+ [HOME_LOCKING] = "locking",
+ [HOME_LOCKED] = "locked",
+ [HOME_UNLOCKING] = "unlocking",
+ [HOME_UNLOCKING_FOR_ACQUIRE] = "unlocking-for-acquire",
+ [HOME_CREATING] = "creating",
+ [HOME_REMOVING] = "removing",
+ [HOME_UPDATING] = "updating",
+ [HOME_UPDATING_WHILE_ACTIVE] = "updating-while-active",
+ [HOME_RESIZING] = "resizing",
+ [HOME_RESIZING_WHILE_ACTIVE] = "resizing-while-active",
+ [HOME_PASSWD] = "passwd",
+ [HOME_PASSWD_WHILE_ACTIVE] = "passwd-while-active",
+ [HOME_AUTHENTICATING] = "authenticating",
+ [HOME_AUTHENTICATING_WHILE_ACTIVE] = "authenticating-while-active",
+ [HOME_AUTHENTICATING_FOR_ACQUIRE] = "authenticating-for-acquire",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(home_state, HomeState);
diff --git a/src/home/homed-home.h b/src/home/homed-home.h
new file mode 100644
index 0000000000000..c75b06722c625
--- /dev/null
+++ b/src/home/homed-home.h
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+typedef struct Home Home;
+
+#include "homed-manager.h"
+#include "homed-operation.h"
+#include "list.h"
+#include "ordered-set.h"
+#include "user-record.h"
+
+typedef enum HomeState {
+ HOME_UNFIXATED, /* home exists, but local record does not */
+ HOME_ABSENT, /* local record exists, but home does not */
+ HOME_INACTIVE, /* record and home exist, but is not logged in */
+ HOME_FIXATING, /* generating local record from home */
+ HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */
+ HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */
+ HOME_ACTIVATING,
+ HOME_ACTIVATING_FOR_ACQUIRE, /* activating because Acquire() was called */
+ HOME_DEACTIVATING,
+ HOME_ACTIVE, /* logged in right now */
+ HOME_LOCKING,
+ HOME_LOCKED,
+ HOME_UNLOCKING,
+ HOME_UNLOCKING_FOR_ACQUIRE, /* unlocking because Acquire() was called */
+ HOME_CREATING,
+ HOME_REMOVING,
+ HOME_UPDATING,
+ HOME_UPDATING_WHILE_ACTIVE,
+ HOME_RESIZING,
+ HOME_RESIZING_WHILE_ACTIVE,
+ HOME_PASSWD,
+ HOME_PASSWD_WHILE_ACTIVE,
+ HOME_AUTHENTICATING,
+ HOME_AUTHENTICATING_WHILE_ACTIVE,
+ HOME_AUTHENTICATING_FOR_ACQUIRE, /* authenticating because Acquire() was called */
+ _HOME_STATE_MAX,
+ _HOME_STATE_INVALID = -1
+} HomeState;
+
+static inline bool HOME_STATE_IS_ACTIVE(HomeState state) {
+ return IN_SET(state,
+ HOME_ACTIVE,
+ HOME_UPDATING_WHILE_ACTIVE,
+ HOME_RESIZING_WHILE_ACTIVE,
+ HOME_PASSWD_WHILE_ACTIVE,
+ HOME_AUTHENTICATING_WHILE_ACTIVE,
+ HOME_AUTHENTICATING_FOR_ACQUIRE);
+}
+
+static inline bool HOME_STATE_IS_EXECUTING_OPERATION(HomeState state) {
+ return IN_SET(state,
+ HOME_FIXATING,
+ HOME_FIXATING_FOR_ACTIVATION,
+ HOME_FIXATING_FOR_ACQUIRE,
+ HOME_ACTIVATING,
+ HOME_ACTIVATING_FOR_ACQUIRE,
+ HOME_DEACTIVATING,
+ HOME_LOCKING,
+ HOME_UNLOCKING,
+ HOME_UNLOCKING_FOR_ACQUIRE,
+ HOME_CREATING,
+ HOME_REMOVING,
+ HOME_UPDATING,
+ HOME_UPDATING_WHILE_ACTIVE,
+ HOME_RESIZING,
+ HOME_RESIZING_WHILE_ACTIVE,
+ HOME_PASSWD,
+ HOME_PASSWD_WHILE_ACTIVE,
+ HOME_AUTHENTICATING,
+ HOME_AUTHENTICATING_WHILE_ACTIVE,
+ HOME_AUTHENTICATING_FOR_ACQUIRE);
+}
+
+struct Home {
+ Manager *manager;
+ char *user_name;
+ uid_t uid;
+
+ char *sysfs; /* When found via plugged in device, the sysfs path to it */
+
+ /* Note that the 'state' field is only set to a state while we are doing something (i.e. activating,
+ * deactivating, creating, removing, and such), or when the home is an "unfixated" one. When we are
+ * done with an operation we invalidate the state. This is hint for home_get_state() to check the
+ * state on request as needed from the mount table and similar.*/
+ HomeState state;
+ int signed_locally; /* signed only by us */
+
+ UserRecord *record;
+
+ pid_t worker_pid;
+ int worker_stdout_fd;
+ sd_event_source *worker_event_source;
+ int worker_error_code;
+
+ /* The message we are currently processing, and thus need to reply to on completion */
+ Operation *current_operation;
+
+ /* Stores the raw, plaintext passwords, but only for short periods of time */
+ UserRecord *secret;
+
+ /* When we create a home and that fails, we should possibly unregister the record altogether
+ * again, which is remembered in this boolean. */
+ bool unregister_on_failure;
+
+ /* The reading side of a FIFO stored in /run/systemd/home/, the writing side being used for reference
+ * counting. The references dropped to zero as soon as we see EOF. This concept exists twice: once
+ * for clients that are fine if we suspend the home directory on system suspend, and once for cliets
+ * that are not ok with that. This allows us to determine for each home whether there are any clients
+ * that support unsuspend. */
+ sd_event_source *ref_event_source_please_suspend;
+ sd_event_source *ref_event_source_dont_suspend;
+
+ /* Any pending operations we still need to execute. These are for operations we want to queue if we
+ * can't execute them right-away. */
+ OrderedSet *pending_operations;
+
+ /* A defer event source that processes pending acquire/release/eof events. We have a common
+ * dispatcher that processes all three kinds of events. */
+ sd_event_source *pending_event_source;
+
+ /* Did we send out a D-Bus notification about this entry? */
+ bool announced;
+
+ /* Used to coalesce bus PropertiesChanged events */
+ sd_event_source *deferred_change_event_source;
+};
+
+int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret);
+Home *home_free(Home *h);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Home*, home_free);
+
+int home_set_record(Home *h, UserRecord *hr);
+int home_save_record(Home *h);
+int home_unlink_record(Home *h);
+
+int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_activate(Home *h, 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_remove(Home *h, sd_bus_error *error);
+int home_update(Home *h, UserRecord *new_record, sd_bus_error *error);
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, 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);
+int home_lock(Home *h, sd_bus_error *error);
+int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error);
+
+HomeState home_get_state(Home *h);
+
+void home_process_notify(Home *h, char **l);
+
+int home_killall(Home *h);
+
+int home_augment_status(Home *h, UserRecordLoadFlags flags, UserRecord **ret);
+
+int home_create_fifo(Home *h, bool please_suspend);
+int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error);
+
+int home_auto_login(Home *h, char ***ret_seats);
+
+int home_set_current_message(Home *h, sd_bus_message *m);
+
+const char *home_state_to_string(HomeState state);
+HomeState home_state_from_string(const char *s);
diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c
new file mode 100644
index 0000000000000..38fa6f230573a
--- /dev/null
+++ b/src/home/homed-manager-bus.c
@@ -0,0 +1,690 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-util.h"
+#include "format-util.h"
+#include "homed-bus.h"
+#include "homed-home-bus.h"
+#include "homed-manager-bus.h"
+#include "homed-manager.h"
+#include "strv.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+static int property_get_auto_login(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Manager *m = userdata;
+ Iterator i;
+ Home *h;
+ int r;
+
+ assert(bus);
+ assert(reply);
+ assert(m);
+
+ r = sd_bus_message_open_container(reply, 'a', "(sso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+ _cleanup_(strv_freep) char **seats = NULL;
+ _cleanup_free_ char *home_path = NULL;
+ char **s;
+
+ r = home_auto_login(h, &seats);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to determine whether home '%s' is candidate for auto-login, ignoring: %m", h->user_name);
+ continue;
+ }
+ if (!r)
+ continue;
+
+ r = bus_home_path(h, &home_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate home bus path: %m");
+
+ STRV_FOREACH(s, seats) {
+ r = sd_bus_message_append(reply, "(sso)", h->user_name, *s, home_path);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
+static int method_get_home_by_name(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *path = NULL;
+ const char *user_name;
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_read(message, "s", &user_name);
+ if (r < 0)
+ return r;
+ if (!valid_user_group_name(user_name))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+ h = hashmap_get(m->homes_by_name, user_name);
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+ r = bus_home_path(h, &path);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(
+ message, "usussso",
+ (uint32_t) h->uid,
+ home_state_to_string(home_get_state(h)),
+ h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+ h->record ? user_record_real_name(h->record) : NULL,
+ h->record ? user_record_home_directory(h->record) : NULL,
+ h->record ? user_record_shell(h->record) : NULL,
+ path);
+}
+
+static int method_get_home_by_uid(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *path = NULL;
+ Manager *m = userdata;
+ uint32_t uid;
+ int r;
+ Home *h;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_read(message, "u", &uid);
+ if (r < 0)
+ return r;
+ if (!uid_is_valid(uid))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid);
+
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid);
+
+ /* Note that we don't use bus_home_path() here, but build the path manually, since if we are queried
+ * for a UID we should also generate the bus path with a UID, and bus_home_path() uses our more
+ * typical bus path by name. */
+ if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0)
+ return -ENOMEM;
+
+ return sd_bus_reply_method_return(
+ message, "ssussso",
+ h->user_name,
+ home_state_to_string(home_get_state(h)),
+ h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+ h->record ? user_record_real_name(h->record) : NULL,
+ h->record ? user_record_home_directory(h->record) : NULL,
+ h->record ? user_record_shell(h->record) : NULL,
+ path);
+}
+
+static int method_list_homes(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ Manager *m = userdata;
+ Iterator i;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(susussso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(h, m->homes_by_uid, i) {
+ _cleanup_free_ char *path = NULL;
+
+ r = bus_home_path(h, &path);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(
+ reply, "(susussso)",
+ h->user_name,
+ (uint32_t) h->uid,
+ home_state_to_string(home_get_state(h)),
+ h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID,
+ h->record ? user_record_real_name(h->record) : NULL,
+ h->record ? user_record_home_directory(h->record) : NULL,
+ h->record ? user_record_shell(h->record) : NULL,
+ path);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int method_get_user_record_by_name(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *json = NULL, *path = NULL;
+ Manager *m = userdata;
+ const char *user_name;
+ bool incomplete;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_read(message, "s", &user_name);
+ if (r < 0)
+ return r;
+ if (!valid_user_group_name(user_name))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+ h = hashmap_get(m->homes_by_name, user_name);
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+ r = bus_home_get_record_json(h, message, &json, &incomplete);
+ if (r < 0)
+ return r;
+
+ r = bus_home_path(h, &path);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(
+ message, "sbo",
+ json,
+ incomplete,
+ path);
+}
+
+static int method_get_user_record_by_uid(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *json = NULL, *path = NULL;
+ Manager *m = userdata;
+ bool incomplete;
+ uint32_t uid;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_read(message, "u", &uid);
+ if (r < 0)
+ return r;
+ if (!uid_is_valid(uid))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid);
+
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid);
+
+ r = bus_home_get_record_json(h, message, &json, &incomplete);
+ if (r < 0)
+ return r;
+
+ if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0)
+ return -ENOMEM;
+
+ return sd_bus_reply_method_return(
+ message, "sbo",
+ json,
+ incomplete,
+ path);
+}
+
+static int generic_home_method(
+ Manager *m,
+ sd_bus_message *message,
+ sd_bus_message_handler_t handler,
+ sd_bus_error *error) {
+
+ const char *user_name;
+ Home *h;
+ int r;
+
+ r = sd_bus_message_read(message, "s", &user_name);
+ if (r < 0)
+ return r;
+
+ if (!valid_user_group_name(user_name))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name);
+
+ h = hashmap_get(m->homes_by_name, user_name);
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name);
+
+ return handler(message, h, error);
+}
+
+static int method_activate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_activate, error);
+}
+
+static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ 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) {
+ _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL;
+ struct passwd *pw;
+ struct group *gr;
+ bool signed_locally;
+ Home *other;
+ int r;
+
+ assert(m);
+ assert(hr);
+ assert(ret);
+
+ r = user_record_is_supported(hr, error);
+ if (r < 0)
+ return r;
+
+ other = hashmap_get(m->homes_by_name, hr->user_name);
+ if (other)
+ return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name);
+
+ pw = getpwnam(hr->user_name);
+ if (pw)
+ return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name);
+
+ gr = getgrnam(hr->user_name);
+ if (gr)
+ return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name);
+
+ r = manager_verify_user_record(m, hr);
+ switch (r) {
+
+ case USER_RECORD_UNSIGNED:
+ /* If the record is unsigned, then let's sign it with our own key */
+ r = manager_sign_user_record(m, hr, &signed_hr, error);
+ if (r < 0)
+ return r;
+
+ hr = signed_hr;
+ _fallthrough_;
+
+ case USER_RECORD_SIGNED_EXCLUSIVE:
+ signed_locally = true;
+ break;
+
+ case USER_RECORD_SIGNED:
+ case USER_RECORD_FOREIGN:
+ signed_locally = false;
+ break;
+
+ case -ENOKEY:
+ return sd_bus_error_setf(error, BUS_ERROR_BAD_SIGNATURE, "Specified user record for %s is signed by a key we don't recognize, refusing.", hr->user_name);
+
+ default:
+ return sd_bus_error_set_errnof(error, r, "Failed to validate signature for '%s': %m", hr->user_name);
+ }
+
+ if (uid_is_valid(hr->uid)) {
+ other = hashmap_get(m->homes_by_uid, UID_TO_PTR(hr->uid));
+ if (other)
+ return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by home %s, refusing.", hr->uid, other->user_name);
+
+ pw = getpwuid(hr->uid);
+ if (pw)
+ return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by NSS user %s, refusing.", hr->uid, pw->pw_name);
+
+ gr = getgrgid(hr->uid);
+ if (gr)
+ return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use as GID by NSS group %s, refusing.", hr->uid, gr->gr_name);
+ } else {
+ r = manager_augment_record_with_uid(m, hr);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to acquire UID for '%s': %m", hr->user_name);
+ }
+
+ r = home_new(m, hr, NULL, ret);
+ if (r < 0)
+ return r;
+
+ (*ret)->signed_locally = signed_locally;
+ return r;
+}
+
+static int method_register_home(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED, &hr, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.create-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = validate_and_allocate_home(m, hr, &h, error);
+ if (r < 0)
+ return r;
+
+ r = home_save_record(h);
+ if (r < 0) {
+ home_free(h);
+ return r;
+ }
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ 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) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.home1.create-home",
+ NULL,
+ true,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = validate_and_allocate_home(m, hr, &h, error);
+ if (r < 0)
+ return r;
+
+ r = home_create(h, hr, error);
+ if (r < 0)
+ goto fail;
+
+ assert(r == 0);
+ h->unregister_on_failure = true;
+ assert(!h->current_operation);
+
+ r = home_set_current_message(h, message);
+ if (r < 0)
+ return r;
+
+ return 1;
+
+fail:
+ (void) home_unlink_record(h);
+ h = home_free(h);
+ return r;
+}
+
+static int method_realize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_realize, error);
+}
+
+static int method_remove_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_remove, error);
+}
+
+static int method_fixate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_fixate, error);
+}
+
+static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_authenticate, error);
+}
+
+static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error);
+ 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);
+}
+
+static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_resize, error);
+}
+
+static int method_change_password_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_change_password, error);
+}
+
+static int method_lock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_lock, error);
+}
+
+static int method_unlock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_unlock, error);
+}
+
+static int method_acquire_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_acquire, error);
+}
+
+static int method_ref_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_ref, error);
+}
+
+static int method_release_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return generic_home_method(userdata, message, bus_home_method_release, error);
+}
+
+static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+ bool waiting = false;
+ Manager *m = userdata;
+ Iterator i;
+ Home *h;
+ int r;
+
+ assert(m);
+
+ /* This is called from logind when we are preparing for system suspend. We enqueue a lock operation
+ * for every suitable home we have and only when all of them completed we send a reply indicating
+ * completion. */
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+ /* Automatically suspend all homes that have at least one client referencing it that asked
+ * for "please suspend", and no client that asked for "please do not suspend". */
+ if (h->ref_event_source_dont_suspend ||
+ !h->ref_event_source_please_suspend)
+ continue;
+
+ if (!o) {
+ o = operation_new(OPERATION_LOCK_ALL, message);
+ if (!o)
+ return -ENOMEM;
+ }
+
+ log_info("Automatically locking of home of user %s.", h->user_name);
+
+ r = home_schedule_operation(h, o, error);
+ if (r < 0)
+ return r;
+
+ waiting = true;
+ }
+
+ if (waiting) /* At least one lock operation was enqeued, let's leave here without a reply: it will
+ * be sent as soon as the last of the lock operations completed. */
+ return 1;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+const sd_bus_vtable manager_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+
+ SD_BUS_PROPERTY("AutoLogin", "a(sso)", property_get_auto_login, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+
+ SD_BUS_METHOD("GetHomeByName", "s", "usussso", method_get_home_by_name, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetHomeByUID", "u", "ssussso", method_get_home_by_uid, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetUserRecordByName", "s", "sbo", method_get_user_record_by_name, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("GetUserRecordByUID", "u", "sbo", method_get_user_record_by_uid, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("ListHomes", NULL, "a(susussso)", method_list_homes, SD_BUS_VTABLE_UNPRIVILEGED),
+
+ /* The following methods directly execute an operation on a home, without ref-counting, queing or
+ * anything, and are accessible through homectl. */
+ SD_BUS_METHOD("ActivateHome", "ss", NULL, method_activate_home, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("DeactivateHome", "s", NULL, method_deactivate_home, 0),
+ SD_BUS_METHOD("RegisterHome", "s", NULL, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Add JSON record to homed, but don't create actual $HOME */
+ SD_BUS_METHOD("UnregisterHome", "s", NULL, method_unregister_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record from homed, but don't remove actual $HOME */
+ SD_BUS_METHOD("CreateHome", "s", NULL, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Add JSON record, and create $HOME for it */
+ SD_BUS_METHOD("RealizeHome", "ss", NULL, method_realize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */
+ SD_BUS_METHOD("RemoveHome", "s", NULL, method_remove_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record and remove $HOME */
+ SD_BUS_METHOD("FixateHome", "ss", NULL, method_fixate_home, SD_BUS_VTABLE_SENSITIVE), /* Investigate $HOME and propagate contained JSON record into our database */
+ SD_BUS_METHOD("AuthenticateHome", "ss", NULL, method_authenticate_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Just check credentials */
+ SD_BUS_METHOD("UpdateHome", "s", NULL, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Update JSON record of existing user */
+ SD_BUS_METHOD("ResizeHome", "sts", NULL, method_resize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("ChangePasswordHome", "sss", NULL, method_change_password_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("LockHome", "s", NULL, method_lock_home, 0), /* Prepare active home for system suspend: flush out passwords, suspend access */
+ SD_BUS_METHOD("UnlockHome", "ss", NULL, method_unlock_home, SD_BUS_VTABLE_SENSITIVE), /* Make $HOME usable after system resume again */
+
+ /* The following methods implement ref-counted activation, and are what the PAM module calls (and
+ * what "homectl with" runs). In contrast to the methods above which fail if an operation is already
+ * being executed on a home directory, these ones will queue the request, and are thus more
+ * reliable. Moreover, they are a bit smarter: AcquireHome() will fixate, activate, unlock, or
+ * authenticate depending on the state of the home, so that the end result is always the same
+ * (i.e. the home directory is accessible), and we always validate the specified passwords. RefHome()
+ * will not authenticate, and thus only works if home is already active. */
+ SD_BUS_METHOD("AcquireHome", "ssb", "h", method_acquire_home, SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD("RefHome", "sb", "h", method_ref_home, 0),
+ SD_BUS_METHOD("ReleaseHome", "s", NULL, method_release_home, 0),
+
+ /* An operation that acts on all homes that allow it */
+ SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0),
+
+ SD_BUS_VTABLE_END
+};
+
+static int on_deferred_auto_login(sd_event_source *s, void *userdata) {
+ Manager *m = userdata;
+ int r;
+
+ assert(m);
+
+ m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source);
+
+ r = sd_bus_emit_properties_changed(
+ m->bus,
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "AutoLogin", NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to send AutoLogin property change event, ignoring: %m");
+
+ return 0;
+}
+
+int bus_manager_emit_auto_login_changed(Manager *m) {
+ int r;
+ assert(m);
+
+ if (m->deferred_auto_login_event_source)
+ return 0;
+
+ if (!m->event)
+ return 0;
+
+ if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+ return 0;
+
+ r = sd_event_add_defer(m->event, &m->deferred_auto_login_event_source, on_deferred_auto_login, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate auto login event source: %m");
+
+ r = sd_event_source_set_priority(m->deferred_auto_login_event_source, SD_EVENT_PRIORITY_IDLE+10);
+ if (r < 0)
+ log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+ (void) sd_event_source_set_description(m->deferred_auto_login_event_source, "deferred-auto-login");
+ return 1;
+}
diff --git a/src/home/homed-manager-bus.h b/src/home/homed-manager-bus.h
new file mode 100644
index 0000000000000..40e1cc3d86d60
--- /dev/null
+++ b/src/home/homed-manager-bus.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+extern const sd_bus_vtable manager_vtable[];
diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c
new file mode 100644
index 0000000000000..0c952c8b67dfd
--- /dev/null
+++ b/src/home/homed-manager.c
@@ -0,0 +1,1676 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "btrfs-util.h"
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "clean-ipc.h"
+#include "conf-files.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "gpt.h"
+#include "home-util.h"
+#include "homed-home-bus.h"
+#include "homed-home.h"
+#include "homed-manager-bus.h"
+#include "homed-manager.h"
+#include "homed-varlink.h"
+#include "io-util.h"
+#include "mkdir.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "udev-util.h"
+#include "user-record-sign.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+
+/* Where to look for private/public keys that are used to sign the user records. We are not using
+ * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that
+ * since we want to auto-generate a persistent private/public key pair if we need to. */
+#define KEY_PATHS_NULSTR \
+ "/etc/systemd/home/\0" \
+ "/run/systemd/home/\0" \
+ "/var/lib/systemd/home/\0" \
+ "/usr/local/lib/systemd/home/\0" \
+ "/usr/lib/systemd/home/\0"
+
+static bool uid_is_home(uid_t uid) {
+ return uid >= HOME_UID_MIN && uid <= HOME_UID_MAX;
+}
+/* Takes a value generated randomly or by hashing and turns it into a UID in the right range */
+
+#define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN)
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare_func, Home, home_free);
+
+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_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);
+
+static void manager_watch_home(Manager *m) {
+ struct statfs sfs;
+ int r;
+
+ assert(m);
+
+ m->inotify_event_source = sd_event_source_unref(m->inotify_event_source);
+ m->scan_slash_home = false;
+
+ if (statfs("/home/", &sfs) < 0) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to statfs() /home/ directory, disabling automatic scanning.");
+ return;
+ }
+
+ if (is_network_fs(&sfs)) {
+ log_info("/home/ is a network file system, disabling automatic scanning.");
+ return;
+ }
+
+ if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) {
+ log_info("/home/ is on autofs, disabling automatic scanning.");
+ return;
+ }
+
+ m->scan_slash_home = true;
+
+ r = sd_event_add_inotify(m->event, &m->inotify_event_source, "/home/", IN_CREATE|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ONLYDIR|IN_MOVED_TO|IN_MOVED_FROM|IN_DELETE, on_home_inotify, m);
+ if (r < 0)
+ log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to create inotify watch on /home/, ignoring.");
+
+ (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify");
+}
+
+static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+ Manager *m = userdata;
+ const char *e, *n;
+
+ assert(m);
+ assert(event);
+
+ if ((event->mask & (IN_Q_OVERFLOW|IN_MOVE_SELF|IN_DELETE_SELF|IN_IGNORED|IN_UNMOUNT)) != 0) {
+
+ if (FLAGS_SET(event->mask, IN_Q_OVERFLOW))
+ log_debug("/home/ inotify queue overflow, rescanning.");
+ else if (FLAGS_SET(event->mask, IN_MOVE_SELF))
+ log_info("/home/ moved or renamed, recreating watch and rescanning.");
+ else if (FLAGS_SET(event->mask, IN_DELETE_SELF))
+ log_info("/home/ deleted, recreating watch and rescanning.");
+ else if (FLAGS_SET(event->mask, IN_UNMOUNT))
+ log_info("/home/ unmounted, recreating watch and rescanning.");
+ else if (FLAGS_SET(event->mask, IN_IGNORED))
+ log_info("/home/ watch invalidated, recreating watch and rescanning.");
+
+ manager_watch_home(m);
+ (void) manager_gc_images(m);
+ (void) manager_enumerate_images(m);
+ (void) bus_manager_emit_auto_login_changed(m);
+ return 0;
+ }
+
+ /* For the other inotify events, let's ignore all events for file names that don't match our
+ * expectations */
+ if (isempty(event->name))
+ return 0;
+ e = endswith(event->name, FLAGS_SET(event->mask, IN_ISDIR) ? ".homedir" : ".home");
+ if (!e)
+ return 0;
+
+ n = strndupa(event->name, e - event->name);
+ if (!suitable_user_name(n))
+ return 0;
+
+ if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) {
+ if (FLAGS_SET(event->mask, IN_CREATE))
+ log_debug("/home/%s has been created, having a look.", event->name);
+ else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+ log_debug("/home/%s has been modified, having a look.", event->name);
+ else if (FLAGS_SET(event->mask, IN_MOVED_TO))
+ log_debug("/home/%s has been moved in, having a look.", event->name);
+
+ (void) manager_assess_image(m, -1, "/home/", event->name);
+ (void) bus_manager_emit_auto_login_changed(m);
+ }
+
+ if ((event->mask & (IN_DELETE|IN_MOVED_FROM|IN_DELETE)) != 0) {
+ Home *h;
+
+ if (FLAGS_SET(event->mask, IN_DELETE))
+ log_debug("/home/%s has been deleted, revalidating.", event->name);
+ else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE))
+ log_debug("/home/%s has been closed after writing, revalidating.", event->name);
+ else if (FLAGS_SET(event->mask, IN_MOVED_FROM))
+ log_debug("/home/%s has been moved away, revalidating.", event->name);
+
+ h = hashmap_get(m->homes_by_name, n);
+ if (h) {
+ manager_revalidate_image(m, h);
+ (void) bus_manager_emit_auto_login_changed(m);
+ }
+ }
+
+ return 0;
+}
+
+int manager_new(Manager **ret) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ assert(ret);
+
+ m = new0(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_set_watchdog(m->event, true);
+
+ m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops);
+ if (!m->homes_by_uid)
+ return -ENOMEM;
+
+ m->homes_by_name = hashmap_new(&homes_by_name_hash_ops);
+ if (!m->homes_by_name)
+ return -ENOMEM;
+
+ m->homes_by_worker_pid = hashmap_new(&homes_by_worker_pid_hash_ops);
+ if (!m->homes_by_worker_pid)
+ return -ENOMEM;
+
+ m->homes_by_sysfs = hashmap_new(&homes_by_sysfs_hash_ops);
+ if (!m->homes_by_sysfs)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+Manager* manager_free(Manager *m) {
+ assert(m);
+
+ hashmap_free(m->homes_by_uid);
+ hashmap_free(m->homes_by_name);
+ hashmap_free(m->homes_by_worker_pid);
+ hashmap_free(m->homes_by_sysfs);
+
+ m->inotify_event_source = sd_event_source_unref(m->inotify_event_source);
+
+ bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+ sd_bus_flush_close_unref(m->bus);
+ sd_event_unref(m->event);
+
+ m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source);
+ m->device_monitor = sd_device_monitor_unref(m->device_monitor);
+
+ m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source);
+ m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source);
+ m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source);
+
+ if (m->private_key)
+ EVP_PKEY_free(m->private_key);
+
+ hashmap_free(m->public_keys);
+
+ varlink_server_unref(m->varlink_server);
+
+ return mfree(m);
+}
+
+int manager_verify_user_record(Manager *m, UserRecord *hr) {
+ EVP_PKEY *pkey;
+ Iterator i;
+ int r;
+
+ assert(m);
+ assert(hr);
+
+ if (!m->private_key && hashmap_isempty(m->public_keys)) {
+ r = user_record_has_signature(hr);
+ if (r < 0)
+ return r;
+
+ return r ? -ENOKEY : USER_RECORD_UNSIGNED;
+ }
+
+ /* Is it our own? */
+ if (m->private_key) {
+ r = user_record_verify(hr, m->private_key);
+ switch (r) {
+
+ case USER_RECORD_FOREIGN:
+ /* This record is not signed by this key, but let's see below */
+ break;
+
+ case USER_RECORD_SIGNED: /* Signed by us, but also by others, let's propagate that */
+ case USER_RECORD_SIGNED_EXCLUSIVE: /* Signed by us, and nothing else, ditto */
+ case USER_RECORD_UNSIGNED: /* Not signed at all, ditto */
+ default:
+ return r;
+ }
+ }
+
+ HASHMAP_FOREACH(pkey, m->public_keys, i) {
+ r = user_record_verify(hr, pkey);
+ switch (r) {
+
+ case USER_RECORD_FOREIGN:
+ /* This record is not signed by this key, but let's see our other keys */
+ break;
+
+ case USER_RECORD_SIGNED: /* It's signed by this key we are happy with, but which is not our own. */
+ case USER_RECORD_SIGNED_EXCLUSIVE:
+ return USER_RECORD_FOREIGN;
+
+ case USER_RECORD_UNSIGNED: /* It's not signed at all */
+ default:
+ return r;
+ }
+ }
+
+ return -ENOKEY;
+}
+
+static int manager_add_home_by_record(
+ Manager *m,
+ const char *name,
+ int dir_fd,
+ const char *fname) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr;
+ unsigned line, column;
+ int r, is_signed;
+ Home *h;
+
+ assert(m);
+ assert(name);
+ assert(fname);
+
+ r = json_parse_file_at(NULL, dir_fd, fname, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse identity record at %s:%u%u: %m", fname, line, column);
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(hr->user_name, name))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Identity's user name %s does not match file name %s, refusing.", hr->user_name, name);
+
+ is_signed = manager_verify_user_record(m, hr);
+ switch (is_signed) {
+
+ case -ENOKEY:
+ return log_warning_errno(is_signed, "User record %s is not signed by any accepted key, ignoring.", fname);
+ case USER_RECORD_UNSIGNED:
+ return log_warning_errno(SYNTHETIC_ERRNO(EPERM), "User record %s is not signed at all, ignoring.", fname);
+ case USER_RECORD_SIGNED:
+ log_info("User record %s is signed by us (and others), accepting.", fname);
+ break;
+ case USER_RECORD_SIGNED_EXCLUSIVE:
+ log_info("User record %s is signed only by us, accepting.", fname);
+ break;
+ case USER_RECORD_FOREIGN:
+ log_info("User record %s is signed by registered key from others, accepting.", fname);
+ break;
+ default:
+ assert(is_signed < 0);
+ return log_error_errno(is_signed, "Failed to verify signature of user record in %s: %m", fname);
+ }
+
+ h = hashmap_get(m->homes_by_name, name);
+ if (h) {
+ r = home_set_record(h, hr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update home record for %s: %m", name);
+
+ /* If we acquired a record now for a previously unallocated entry, then reset the state. This
+ * makes sure home_get_state() will check for the availability of the image file dynamically
+ * in order to detect to distuingish HOME_INACTIVE and HOME_ABSENT. */
+ if (h->state == HOME_UNFIXATED)
+ h->state = _HOME_STATE_INVALID;
+ } else {
+ r = home_new(m, hr, NULL, &h);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new home object: %m");
+
+ log_info("Added registered home for user %s.", hr->user_name);
+ }
+
+ /* Only entries we exclusively signed are writable to us, hence remember the result */
+ h->signed_locally = is_signed == USER_RECORD_SIGNED_EXCLUSIVE;
+
+ return 1;
+}
+
+static int manager_enumerate_records(Manager *m) {
+ _cleanup_closedir_ DIR *d = NULL;
+ struct dirent *de;
+
+ assert(m);
+
+ d = opendir("/var/lib/systemd/home/");
+ if (!d)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to open /var/lib/systemd/home/: %m");
+
+ FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read record directory: %m")) {
+ _cleanup_free_ char *n = NULL;
+ const char *e;
+
+ if (!dirent_is_file(de))
+ continue;
+
+ e = endswith(de->d_name, ".identity");
+ if (!e)
+ continue;
+
+ n = strndup(de->d_name, e - de->d_name);
+ if (!n)
+ return log_oom();
+
+ if (!suitable_user_name(n))
+ continue;
+
+ (void) manager_add_home_by_record(m, n, dirfd(d), de->d_name);
+ }
+
+ return 0;
+}
+
+static int search_quota(uid_t uid, const char *exclude_quota_path) {
+ struct stat exclude_st = {};
+ dev_t previous_devno = 0;
+ const char *where;
+ int r;
+
+ /* Checks whether the specified UID owns any files on the files system, but ignore any file system
+ * backing the specified file. The file is used when operating on home directories, where it's OK if
+ * the UID of them already owns files. */
+
+ if (exclude_quota_path && stat(exclude_quota_path, &exclude_st) < 0) {
+ if (errno != ENOENT)
+ return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", exclude_quota_path);
+ }
+
+ /* Check a few usual suspects where regular users might own files. Note that this is by no means
+ * comprehensive, but should cover most cases. Note that in an ideal world every user would be
+ * registered in NSS and avoid our own UID range, but for all other cases, it's a good idea to be
+ * paranoid and check quota if we can. */
+ FOREACH_STRING(where, "/home/", "/tmp/", "/var/", "/var/mail/", "/var/tmp/", "/var/spool/") {
+ _cleanup_free_ char *devnode = NULL;
+ struct dqblk req;
+ struct stat st;
+
+ if (stat(where, &st) < 0) {
+ log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to stat %s, ignoring: %m", where);
+ continue;
+ }
+
+ if (major(st.st_dev) == 0) {
+ log_debug("Directory %s is not on a real block device, not checking quota for UID use.", where);
+ continue;
+ }
+
+ if (st.st_dev == exclude_st.st_dev) { /* If an exclude path is specified, then ignore quota
+ * reported on the same block device as that path. */
+ log_debug("Directory %s is where the home directory is located, not checking quota for UID use.", where);
+ continue;
+ }
+
+ if (st.st_dev == previous_devno) { /* Does this directory have the same devno as the previous
+ * one we tested? If so, there's no point in testing this
+ * again. */
+ log_debug("Directory %s is on same device as previous tested directory, not checking quota for UID use a second time.", where);
+ continue;
+ }
+
+ previous_devno = st.st_dev;
+
+ r = device_path_make_major_minor(S_IFBLK, st.st_dev, &devnode);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive block device path for %s: %m", where);
+
+ if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, uid, (caddr_t) &req) < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno))
+ log_debug_errno(errno, "No UID quota support on %s, ignoring.", where);
+ else
+ log_warning_errno(errno, "Failed to query quota on %s, ignoring.", where);
+
+ continue;
+ }
+
+ if ((FLAGS_SET(req.dqb_valid, QIF_SPACE) && req.dqb_curspace > 0) ||
+ (FLAGS_SET(req.dqb_valid, QIF_INODES) && req.dqb_curinodes > 0)) {
+ log_debug_errno(errno, "Quota reports UID " UID_FMT " occupies disk space on %s.", uid, where);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int manager_acquire_uid(
+ Manager *m,
+ uid_t start_uid,
+ const char *user_name,
+ const char *exclude_quota_path,
+ uid_t *ret) {
+
+ static const uint8_t hash_key[] = {
+ 0xa3, 0xb8, 0x82, 0x69, 0x9a, 0x71, 0xf7, 0xa9,
+ 0xe0, 0x7c, 0xf6, 0xf1, 0x21, 0x69, 0xd2, 0x1e
+ };
+
+ enum {
+ PHASE_SUGGESTED,
+ PHASE_HASHED,
+ PHASE_RANDOM
+ } phase = PHASE_SUGGESTED;
+
+ unsigned n_tries = 100;
+ int r;
+
+ assert(m);
+ assert(ret);
+
+ for (;;) {
+ struct passwd *pw;
+ struct group *gr;
+ uid_t candidate;
+ Home *other;
+
+ if (--n_tries <= 0)
+ return -EBUSY;
+
+ switch (phase) {
+
+ case PHASE_SUGGESTED:
+ phase = PHASE_HASHED;
+
+ if (!uid_is_home(start_uid))
+ continue;
+
+ candidate = start_uid;
+ break;
+
+ case PHASE_HASHED:
+ phase = PHASE_RANDOM;
+
+ if (!user_name)
+ continue;
+
+ candidate = UID_CLAMP_INTO_HOME_RANGE(siphash24(user_name, strlen(user_name), hash_key));
+ break;
+
+ case PHASE_RANDOM:
+ random_bytes(&candidate, sizeof(candidate));
+ candidate = UID_CLAMP_INTO_HOME_RANGE(candidate);
+ break;
+
+ default:
+ assert_not_reached("unknown phase");
+ }
+
+ other = hashmap_get(m->homes_by_uid, UID_TO_PTR(candidate));
+ if (other) {
+ log_debug("Candidate UID " UID_FMT " already used by another home directory (%s), let's try another.", candidate, other->user_name);
+ continue;
+ }
+
+ pw = getpwuid(candidate);
+ if (pw) {
+ log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name);
+ continue;
+ }
+
+ gr = getgrgid((gid_t) candidate);
+ if (gr) {
+ log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name);
+ continue;
+ }
+
+ r = search_ipc(candidate, (gid_t) candidate);
+ if (r < 0)
+ continue;
+ if (r > 0) {
+ log_debug_errno(r, "Candidate UID " UID_FMT " already owns IPC objects, let's try another: %m", candidate);
+ continue;
+ }
+
+ r = search_quota(candidate, exclude_quota_path);
+ if (r != 0)
+ continue;
+
+ *ret = candidate;
+ return 0;
+ }
+}
+
+static int manager_add_home_by_image(
+ Manager *m,
+ const char *user_name,
+ const char *realm,
+ const char *image_path,
+ const char *sysfs,
+ UserStorage storage,
+ uid_t start_uid) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ uid_t uid;
+ Home *h;
+ int r;
+
+ assert(m);
+
+ assert(m);
+ assert(user_name);
+ assert(image_path);
+ assert(storage >= 0);
+ assert(storage < _USER_STORAGE_MAX);
+
+ h = hashmap_get(m->homes_by_name, user_name);
+ if (h) {
+ bool same;
+
+ if (h->state != HOME_UNFIXATED) {
+ log_debug("Found an image for user %s which already has a record, skipping.", user_name);
+ return 0; /* ignore images that synthesize a user we already have a record for */
+ }
+
+ same = user_record_storage(h->record) == storage;
+ if (same) {
+ if (h->sysfs && sysfs)
+ same = path_equal(h->sysfs, sysfs);
+ else if (!!h->sysfs != !!sysfs)
+ same = false;
+ else {
+ const char *p;
+
+ p = user_record_image_path(h->record);
+ same = p && path_equal(p, image_path);
+ }
+ }
+
+ if (!same) {
+ log_debug("Found a multiple images for a user '%s', ignoring image '%s'.", user_name, image_path);
+ return 0;
+ }
+ } else {
+ /* Check NSS, in case there's another user or group by this name */
+ if (getpwnam(user_name) || getgrnam(user_name)) {
+ log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path);
+ return 0;
+ }
+ }
+
+ if (h && uid_is_valid(h->uid))
+ uid = h->uid;
+ else {
+ r = manager_acquire_uid(m, start_uid, user_name, IN_SET(storage, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT) ? image_path : NULL, &uid);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to acquire unused UID for %s: %m", user_name);
+ }
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_synthesize(hr, user_name, realm, image_path, storage, uid, (gid_t) uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to synthesize home record for %s (image %s): %m", user_name, image_path);
+
+ if (h) {
+ r = home_set_record(h, hr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update home record for %s: %m", user_name);
+ } else {
+ r = home_new(m, hr, sysfs, &h);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate new home object: %m");
+
+ h->state = HOME_UNFIXATED;
+
+ log_info("Discovered new home for user %s through image %s.", user_name, image_path);
+ }
+
+ return 1;
+}
+
+int manager_augment_record_with_uid(
+ Manager *m,
+ UserRecord *hr) {
+
+ const char *exclude_quota_path = NULL;
+ uid_t start_uid = UID_INVALID, uid;
+ int r;
+
+ assert(m);
+ assert(hr);
+
+ if (uid_is_valid(hr->uid))
+ return 0;
+
+ if (IN_SET(hr->storage, USER_CLASSIC, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT)) {
+ const char * ip;
+
+ ip = user_record_image_path(hr);
+ if (ip) {
+ struct stat st;
+
+ if (stat(ip, &st) < 0) {
+ if (errno != ENOENT)
+ log_warning_errno(errno, "Failed to stat(%s): %m", ip);
+ } else if (uid_is_home(st.st_uid)) {
+ start_uid = st.st_uid;
+ exclude_quota_path = ip;
+ }
+ }
+ }
+
+ r = manager_acquire_uid(m, start_uid, hr->user_name, exclude_quota_path, &uid);
+ if (r < 0)
+ return r;
+
+ log_debug("Acquired new UID " UID_FMT " for %s.", uid, hr->user_name);
+
+ r = user_record_add_binding(
+ hr,
+ _USER_STORAGE_INVALID,
+ NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ NULL,
+ NULL, 0,
+ NULL,
+ uid,
+ (gid_t) uid);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static int manager_assess_image(
+ Manager *m,
+ int dir_fd,
+ const char *dir_path,
+ const char *dentry_name) {
+
+ char *luks_suffix, *directory_suffix;
+ _cleanup_free_ char *path = NULL;
+ struct stat st;
+ int r;
+
+ assert(m);
+ assert(dir_path);
+ assert(dentry_name);
+
+ luks_suffix = endswith(dentry_name, ".home");
+ if (luks_suffix)
+ directory_suffix = NULL;
+ else
+ directory_suffix = endswith(dentry_name, ".homedir");
+
+ /* Early filter out: by name */
+ if (!luks_suffix && !directory_suffix)
+ return 0;
+
+ path = path_join(dir_path, dentry_name);
+ if (!path)
+ return log_oom();
+
+ /* Follow symlinks here, to allow people to link in stuff to make them available locally. */
+ if (dir_fd >= 0)
+ r = fstatat(dir_fd, dentry_name, &st, 0);
+ else
+ r = stat(path, &st);
+ if (r < 0)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to stat directory entry '%s', ignoring: %m", dentry_name);
+
+ if (S_ISREG(st.st_mode)) {
+ _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
+
+ if (!luks_suffix)
+ return 0;
+
+ n = strndup(dentry_name, luks_suffix - dentry_name);
+ if (!n)
+ return log_oom();
+
+ r = split_user_name_realm(n, &user_name, &realm);
+ if (r == -EINVAL) /* Not the right format: ignore */
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to split image name into user name/realm: %m");
+
+ return manager_add_home_by_image(m, user_name, realm, path, NULL, USER_LUKS, UID_INVALID);
+ }
+
+ if (S_ISDIR(st.st_mode)) {
+ _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL;
+ _cleanup_close_ int fd = -1;
+ UserStorage storage;
+
+ if (!directory_suffix)
+ return 0;
+
+ n = strndup(dentry_name, directory_suffix - dentry_name);
+ if (!n)
+ return log_oom();
+
+ r = split_user_name_realm(n, &user_name, &realm);
+ if (r == -EINVAL) /* Not the right format: ignore */
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to split image name into user name/realm: %m");
+
+ if (dir_fd >= 0)
+ fd = openat(dir_fd, dentry_name, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ else
+ fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to open directory '%s', ignoring: %m", path);
+
+ if (fstat(fd, &st) < 0)
+ return log_warning_errno(errno, "Failed to fstat() %s, ignoring: %m", path);
+
+ assert(S_ISDIR(st.st_mode)); /* Must hold, we used O_DIRECTORY above */
+
+ r = btrfs_is_subvol_fd(fd);
+ if (r < 0)
+ return log_warning_errno(errno, "Failed to determine whether %s is a btrfs subvolume: %m", path);
+ if (r > 0)
+ storage = USER_SUBVOLUME;
+ else {
+ struct fscrypt_policy policy;
+
+ if (ioctl(fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+
+ if (errno == ENODATA)
+ log_debug_errno(errno, "Determined %s is not fscrypt encrypted.", path);
+ else if (ERRNO_IS_NOT_SUPPORTED(errno))
+ log_debug_errno(errno, "Determined %s is not fscrypt encrypted because kernel or file system don't support it.", path);
+ else
+ log_debug_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY failed with unexpected error code on %s, ignoring: %m", path);
+
+ storage = USER_DIRECTORY;
+ } else
+ storage = USER_FSCRYPT;
+ }
+
+ return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid);
+ }
+
+ return 0;
+}
+
+int manager_enumerate_images(Manager *m) {
+ _cleanup_closedir_ DIR *d = NULL;
+ struct dirent *de;
+
+ assert(m);
+
+ if (!m->scan_slash_home)
+ return 0;
+
+ d = opendir("/home/");
+ if (!d)
+ return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno,
+ "Failed to open /home/: %m");
+
+ FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read /home/ directory: %m"))
+ (void) manager_assess_image(m, dirfd(d), "/home", de->d_name);
+
+ return 0;
+}
+
+static int manager_connect_bus(Manager *m) {
+ int r;
+
+ assert(m);
+ assert(!m->bus);
+
+ r = sd_bus_default_system(&m->bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to system bus: %m");
+
+ r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/home1", "org.freedesktop.home1.Manager", manager_vtable, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add manager object vtable: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/home1/home", "org.freedesktop.home1.Home", home_vtable, bus_home_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add image object vtable: %m");
+
+ r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/home1/home", bus_home_node_enumerator, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add image enumerator: %m");
+
+ r = sd_bus_add_object_manager(m->bus, NULL, "/org/freedesktop/home1/home");
+ if (r < 0)
+ return log_error_errno(r, "Failed to add object manager: %m");
+
+ r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.home1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(m->bus, m->event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ (void) sd_bus_set_exit_on_disconnect(m->bus, true);
+
+ return 0;
+}
+
+static int manager_bind_varlink(Manager *m) {
+ int r;
+
+ assert(m);
+ assert(!m->varlink_server);
+
+ r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate varlink server object: %m");
+
+ varlink_server_set_userdata(m->varlink_server, m);
+
+ r = varlink_server_bind_method_many(
+ m->varlink_server,
+ "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record,
+ "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
+ "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
+ if (r < 0)
+ return log_error_errno(r, "Failed to register varlink methods: %m");
+
+ (void) mkdir_p("/run/systemd/userdb", 0755);
+
+ r = varlink_server_listen_address(m->varlink_server, "/run/systemd/userdb/io.systemd.Home", 0666);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind to varlink socket: %m");
+
+ r = varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+ return 0;
+}
+
+static ssize_t read_datagram(int fd, struct ucred *ret_sender, void **ret) {
+ _cleanup_free_ void *buffer = NULL;
+ ssize_t n, m;
+
+ assert(fd >= 0);
+ assert(ret_sender);
+ assert(ret);
+
+ n = next_datagram_size_fd(fd);
+ if (n < 0)
+ return n;
+
+ buffer = malloc(n + 2);
+ if (!buffer)
+ return -ENOMEM;
+
+ if (ret_sender) {
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(struct ucred))];
+ } control;
+ bool found_ucred = false;
+ struct cmsghdr *cmsg;
+ struct msghdr mh;
+ struct iovec iov;
+
+ /* Pass one extra byte, as a size check */
+ iov = IOVEC_MAKE(buffer, n + 1);
+
+ mh = (struct msghdr) {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+
+ m = recvmsg(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
+ if (m < 0)
+ return -errno;
+
+ cmsg_close_all(&mh);
+
+ /* Ensure the size matches what we determined before */
+ if (m != n)
+ return -EMSGSIZE;
+
+ CMSG_FOREACH(cmsg, &mh)
+ if (cmsg->cmsg_level == SOL_SOCKET &&
+ cmsg->cmsg_type == SCM_CREDENTIALS &&
+ cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) {
+
+ memcpy(ret_sender, CMSG_DATA(cmsg), sizeof(struct ucred));
+ found_ucred = true;
+ }
+
+ if (!found_ucred)
+ *ret_sender = (struct ucred) {
+ .pid = 0,
+ .uid = UID_INVALID,
+ .gid = GID_INVALID,
+ };
+ } else {
+ m = recv(fd, buffer, n + 1, MSG_DONTWAIT);
+ if (m < 0)
+ return -errno;
+
+ /* Ensure the size matches what we determined before */
+ if (m != n)
+ return -EMSGSIZE;
+ }
+
+ /* For safety reasons: let's always NUL terminate. */
+ ((char*) buffer)[n] = 0;
+ *ret = TAKE_PTR(buffer);
+
+ return 0;
+}
+
+static int on_notify_socket(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ void *datagram = NULL;
+ struct ucred sender;
+ Manager *m = userdata;
+ ssize_t n;
+ Home *h;
+
+ assert(s);
+ assert(m);
+
+ n = read_datagram(fd, &sender, &datagram);
+ if (IN_SET(n, -EAGAIN, -EINTR))
+ return 0;
+ if (n < 0)
+ return log_error_errno(n, "Failed to read notify datagram: %m");
+
+ if (sender.pid <= 0) {
+ log_warning("Received notify datagram without valid sender PID, ignoring.");
+ return 0;
+ }
+
+ h = hashmap_get(m->homes_by_worker_pid, PID_TO_PTR(sender.pid));
+ if (!h) {
+ log_warning("Recieved notify datagram of unknown process, ignoring.");
+ return 0;
+ }
+
+ l = strv_split(datagram, "\n");
+ if (!l)
+ return log_oom();
+
+ home_process_notify(h, l);
+ return 0;
+}
+
+static int manager_listen_notify(Manager *m) {
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ int r;
+
+ assert(m);
+ assert(!m->notify_socket_event_source);
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to create listening socket: %m");
+
+ r = sockaddr_un_set_path(&sa.un, "/run/systemd/home/notify");
+ if (r < 0)
+ return log_error_errno(r, "Failed to set AF_UNIX socket path: %m");
+
+ (void) mkdir_parents(sa.un.sun_path, 0755);
+ (void) sockaddr_un_unlink(&sa.un);
+
+ if (bind(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0)
+ return log_error_errno(errno, "Failed to bind to socket: %m");
+
+ r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(m->event, &m->notify_socket_event_source, fd, EPOLLIN, on_notify_socket, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event source for notify socket: %m");
+
+ (void) sd_event_source_set_description(m->notify_socket_event_source, "notify-socket");
+
+ /* Make sure we process sd_notify() before SIGCHLD for any worker, so that we always know the error
+ * number of a client before it exits. */
+ r = sd_event_source_set_priority(m->notify_socket_event_source, SD_EVENT_PRIORITY_NORMAL - 5);
+ if (r < 0)
+ return log_error_errno(r, "Failed to alter priority of NOTIFY_SOCKET event source: %m");
+
+ r = sd_event_source_set_io_fd_own(m->notify_socket_event_source, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pass ownership of notify socket: %m");
+
+ return TAKE_FD(fd);
+}
+
+static int manager_add_device(Manager *m, sd_device *d) {
+ _cleanup_free_ char *user_name = NULL, *realm = NULL, *node = NULL;
+ const char *tabletype, *parttype, *partname, *partuuid, *sysfs;
+ sd_id128_t id;
+ int r;
+
+ assert(m);
+ assert(d);
+
+ r = sd_device_get_syspath(d, &sysfs);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire sysfs path of device: %m");
+
+ r = sd_device_get_property_value(d, "ID_PART_TABLE_TYPE", &tabletype);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire ID_PART_TABLE_TYPE device property, ignoring: %m");
+
+ if (!streq(tabletype, "gpt")) {
+ log_debug("Found partition (%s) on non-GPT table, ignoring.", sysfs);
+ return 0;
+ }
+
+ r = sd_device_get_property_value(d, "ID_PART_ENTRY_TYPE", &parttype);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire ID_PART_ENTRY_TYPE device property, ignoring: %m");
+ r = sd_id128_from_string(parttype, &id);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to parse ID_PART_ENTRY_TYPE field '%s', ignoring: %m", parttype);
+ if (!sd_id128_equal(id, GPT_USER_HOME)) {
+ log_debug("Found partition (%s) we don't care about, ignoring.", sysfs);
+ return 0;
+ }
+
+ r = sd_device_get_property_value(d, "ID_PART_ENTRY_NAME", &partname);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to acquire ID_PART_ENTRY_NAME device property, ignoring: %m");
+
+ r = split_user_name_realm(partname, &user_name, &realm);
+ if (r == -EINVAL)
+ return log_warning_errno(r, "Found partition with correct partition type but a non-parsable partition name '%s', ignoring.", partname);
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate partition name '%s': %m", partname);
+
+ r = sd_device_get_property_value(d, "ID_FS_UUID", &partuuid);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to acquire ID_FS_UUID device property, ignoring: %m");
+
+ r = sd_id128_from_string(partuuid, &id);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse ID_FS_UUID field '%s', ignoring: %m", partuuid);
+
+ if (asprintf(&node, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0)
+ return log_oom();
+
+ return manager_add_home_by_image(m, user_name, realm, node, sysfs, USER_LUKS, UID_INVALID);
+}
+
+static int manager_on_device(sd_device_monitor *monitor, sd_device *d, void *userdata) {
+ Manager *m = userdata;
+ int r;
+
+ assert(m);
+ assert(d);
+
+ if (device_for_action(d, DEVICE_ACTION_REMOVE)) {
+ const char *sysfs;
+ Home *h;
+
+ r = sd_device_get_syspath(d, &sysfs);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to acquire sysfs path from device: %m");
+ return 0;
+ }
+
+ log_info("block device %s has been removed.", sysfs);
+
+ /* Let's see if we previously synthesized a home record from this device, if so, let's just
+ * revalidate that. Otherwise let's revalidate them all, but asynchronously. */
+ h = hashmap_get(m->homes_by_sysfs, sysfs);
+ if (h)
+ manager_revalidate_image(m, h);
+ else
+ manager_enqueue_gc(m, NULL);
+ } else
+ (void) manager_add_device(m, d);
+
+ (void) bus_manager_emit_auto_login_changed(m);
+ return 0;
+}
+
+static int manager_watch_devices(Manager *m) {
+ int r;
+
+ assert(m);
+ assert(!m->device_monitor);
+
+ r = sd_device_monitor_new(&m->device_monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate device monitor: %m");
+
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "block", NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to configure device monitor match: %m");
+
+ r = sd_device_monitor_attach_event(m->device_monitor, m->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach device monitor to event loop: %m");
+
+ r = sd_device_monitor_start(m->device_monitor, manager_on_device, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ return 0;
+}
+
+static int manager_enumerate_devices(Manager *m) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ sd_device *d;
+ int r;
+
+ assert(m);
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_subsystem(e, "block", true);
+ if (r < 0)
+ return r;
+
+ FOREACH_DEVICE(e, d)
+ (void) manager_add_device(m, d);
+
+ return 0;
+}
+
+static int manager_load_key_pair(Manager *m) {
+ _cleanup_(fclosep) FILE *f = NULL;
+ struct stat st;
+ int r;
+
+ assert(m);
+
+ if (m->private_key) {
+ EVP_PKEY_free(m->private_key);
+ m->private_key = NULL;
+ }
+
+ r = search_and_fopen_nulstr("local.private", "re", NULL, KEY_PATHS_NULSTR, &f);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to read private key file: %m");
+
+ if (fstat(fileno(f), &st) < 0)
+ return log_error_errno(errno, "Failed to stat private key file: %m");
+
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_error_errno(r, "Private key file is not regular: %m");
+
+ if (st.st_uid != 0 || (st.st_mode & 0077) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Private key file is readable by more than the root user");
+
+ m->private_key = PEM_read_PrivateKey(f, NULL, NULL, NULL);
+ if (!m->private_key)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key pair");
+
+ log_info("Successfully loaded private key pair.");
+
+ return 1;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free);
+
+static int manager_generate_key_pair(Manager *m) {
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL;
+ _cleanup_(unlink_and_freep) char *temp_public = NULL, *temp_private = NULL;
+ _cleanup_fclose_ FILE *fpublic = NULL, *fprivate = NULL;
+ int r;
+
+ if (m->private_key) {
+ EVP_PKEY_free(m->private_key);
+ m->private_key = NULL;
+ }
+
+ ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
+ if (!ctx)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context.");
+
+ if (EVP_PKEY_keygen_init(ctx) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context.");
+
+ log_info("Generating key pair for signing local user identity records.");
+
+ if (EVP_PKEY_keygen(ctx, &m->private_key) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair");
+
+ log_info("Successfully created Ed25519 key pair.");
+
+ (void) mkdir_p("/var/lib/systemd/home", 0755);
+
+ /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */
+ r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public);
+ if (r < 0)
+ return log_error_errno(errno, "Failed ot open key file for writing: %m");
+
+ if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key.");
+
+ r = fflush_and_check(fpublic);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write private key: %m");
+
+ fpublic = safe_fclose(fpublic);
+
+ /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */
+ r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private);
+ if (r < 0)
+ return log_error_errno(errno, "Failed ot open key file for writing: %m");
+
+ if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair.");
+
+ r = fflush_and_check(fprivate);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write private key: %m");
+
+ fprivate = safe_fclose(fprivate);
+
+ /* Both are written now, move them into place */
+
+ if (rename(temp_public, "/var/lib/systemd/home/local.public") < 0)
+ return log_error_errno(errno, "Failed to move public key file into place: %m");
+ temp_public = mfree(temp_public);
+
+ if (rename(temp_private, "/var/lib/systemd/home/local.private") < 0) {
+ (void) unlink_noerrno("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */
+ return log_error_errno(errno, "Failed to move privtate key file into place: %m");
+ }
+ temp_private = mfree(temp_private);
+
+ return 1;
+}
+
+int manager_acquire_key_pair(Manager *m) {
+ int r;
+
+ assert(m);
+
+ /* Already there? */
+ if (m->private_key)
+ return 1;
+
+ /* First try to load key off disk */
+ r = manager_load_key_pair(m);
+ if (r != 0)
+ return r;
+
+ /* Didn't work, generate a new one */
+ return manager_generate_key_pair(m);
+}
+
+int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error) {
+ int r;
+
+ assert(m);
+ assert(u);
+ assert(ret);
+
+ r = manager_acquire_key_pair(m);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_PRIVATE_KEY, "Can't sign without local key.");
+
+ return user_record_sign(u, m->private_key, ret);
+}
+
+DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free);
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY*, EVP_PKEY_free);
+
+static int manager_load_public_key_one(Manager *m, const char *path) {
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *fn = NULL;
+ struct stat st;
+ int r;
+
+ assert(m);
+
+ if (streq(basename(path), "local.public")) /* we already loaded the private key, which includes the public one */
+ return 0;
+
+ f = fopen(path, "re");
+ if (!f) {
+ if (errno == ENOENT)
+ return 0;
+
+ return log_error_errno(errno, "Failed to open public key %s: %m", path);
+ }
+
+ if (fstat(fileno(f), &st) < 0)
+ return log_error_errno(errno, "Failed to stat public key %s: %m", path);
+
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_error_errno(r, "Public key file %s is not a regular file: %m", path);
+
+ if (st.st_uid != 0 || (st.st_mode & 0022) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path);
+
+ r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL);
+ if (!pkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path);
+
+ fn = strdup(basename(path));
+ if (!fn)
+ return log_oom();
+
+ r = hashmap_put(m->public_keys, fn, pkey);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add public key to set: %m");
+
+ TAKE_PTR(fn);
+ TAKE_PTR(pkey);
+
+ return 0;
+}
+
+static int manager_load_public_keys(Manager *m) {
+ _cleanup_strv_free_ char **files = NULL;
+ char **i;
+ int r;
+
+ assert(m);
+
+ m->public_keys = hashmap_free(m->public_keys);
+
+ r = conf_files_list_nulstr(
+ &files,
+ ".public",
+ NULL,
+ CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED,
+ KEY_PATHS_NULSTR);
+ if (r < 0)
+ return log_error_errno(r, "Failed to assemble list of public key directories: %m");
+
+ STRV_FOREACH(i, files)
+ (void) manager_load_public_key_one(m, *i);
+
+ return 0;
+}
+
+int manager_startup(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = manager_listen_notify(m);
+ if (r < 0)
+ return r;
+
+ r = manager_connect_bus(m);
+ if (r < 0)
+ return r;
+
+ r = manager_bind_varlink(m);
+ if (r < 0)
+ return r;
+
+ r = manager_load_key_pair(m); /* only try to load it, don't generate any */
+ if (r < 0)
+ return r;
+
+ r = manager_load_public_keys(m);
+ if (r < 0)
+ return r;
+
+ manager_watch_home(m);
+ (void) manager_watch_devices(m);
+
+ (void) manager_enumerate_records(m);
+ (void) manager_enumerate_images(m);
+ (void) manager_enumerate_devices(m);
+
+ /* Let's clean up home directories whose devices got removed while we were not running */
+ (void) manager_enqueue_gc(m, NULL);
+
+ return 0;
+}
+
+void manager_revalidate_image(Manager *m, Home *h) {
+ int r;
+
+ assert(m);
+ assert(h);
+
+ /* Frees an automatically discovered image, if it's synthetic and its image disappeared. Unmounts any
+ * image if it's mounted but it's image vanished. */
+
+ if (h->current_operation || !ordered_set_isempty(h->pending_operations))
+ return;
+
+ if (h->state == HOME_UNFIXATED) {
+ r = user_record_test_image_path(h->record);
+ if (r < 0)
+ log_warning_errno(r, "Can't determine if image of %s exists, freeing unfixated user: %m", h->user_name);
+ else if (r == USER_TEST_ABSENT)
+ log_info("Image for %s disappeared, freeing unfixated user.", h->user_name);
+ else
+ return;
+
+ home_free(h);
+
+ } else if (h->state < 0) {
+
+ r = user_record_test_home_directory(h->record);
+ if (r < 0) {
+ log_warning_errno(r, "Unable to determine state of home directory, ignoring: %m");
+ return;
+ }
+
+ if (r == USER_TEST_MOUNTED) {
+ r = user_record_test_image_path(h->record);
+ if (r < 0) {
+ log_warning_errno(r, "Unable to determine state of image path, ignoring: %m");
+ return;
+ }
+
+ if (r == USER_TEST_ABSENT) {
+ _cleanup_(operation_unrefp) Operation *o = NULL;
+
+ log_notice("Backing image disappeared while home directory %s was mounted, unmounting it forcibly.", h->user_name);
+ /* Wowza, the thing is mounted, but the device is gone? Act on it. */
+
+ r = home_killall(h);
+ if (r < 0)
+ log_warning_errno(r, "Failed to kill processes of user %s, ignoring: %m", h->user_name);
+
+ /* We enqueue the operation here, after all the home directory might
+ * currently already run some operation, and we can deactivate it only after
+ * that's complete. */
+ o = operation_new(OPERATION_DEACTIVATE_FORCE, NULL);
+ if (!o) {
+ log_oom();
+ return;
+ }
+
+ r = home_schedule_operation(h, o, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to enqueue forced home directory %s deactivation, ignoring: %m", h->user_name);
+ }
+ }
+ }
+}
+
+int manager_gc_images(Manager *m) {
+ Home *h;
+
+ assert_se(m);
+
+ if (m->gc_focus) {
+ /* Focus on a specific home */
+
+ h = TAKE_PTR(m->gc_focus);
+ manager_revalidate_image(m, h);
+ } else {
+ /* Gc all */
+ Iterator i;
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i)
+ manager_revalidate_image(m, h);
+ }
+
+ return 0;
+}
+
+static int on_deferred_rescan(sd_event_source *s, void *userdata) {
+ Manager *m = userdata;
+
+ assert(m);
+
+ m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source);
+
+ manager_enumerate_devices(m);
+ manager_enumerate_images(m);
+ return 0;
+}
+
+int manager_enqueue_rescan(Manager *m) {
+ int r;
+
+ assert(m);
+
+ if (m->deferred_rescan_event_source)
+ return 0;
+
+ if (!m->event)
+ return 0;
+
+ if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+ return 0;
+
+ r = sd_event_add_defer(m->event, &m->deferred_rescan_event_source, on_deferred_rescan, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate rescan event source: %m");
+
+ r = sd_event_source_set_priority(m->deferred_rescan_event_source, SD_EVENT_PRIORITY_IDLE+1);
+ if (r < 0)
+ log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+ (void) sd_event_source_set_description(m->deferred_rescan_event_source, "deferred-rescan");
+ return 1;
+}
+
+static int on_deferred_gc(sd_event_source *s, void *userdata) {
+ Manager *m = userdata;
+
+ assert(m);
+
+ m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source);
+
+ manager_gc_images(m);
+ return 0;
+}
+
+int manager_enqueue_gc(Manager *m, Home *focus) {
+ int r;
+
+ assert(m);
+
+ /* This enqueues a request to GC dead homes. It may be called with focus=NULL in which case all homes
+ * will be scanned, or with the parameter set, in which case only that home is checked. */
+
+ if (!m->event)
+ return 0;
+
+ if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING))
+ return 0;
+
+ /* If a focus home is specified, then remember to focus just on this home. Otherwise invalidate any
+ * focus that might be set to look at all homes. */
+
+ if (m->deferred_gc_event_source) {
+ if (m->gc_focus != focus) /* not the same focus, then look at everything */
+ m->gc_focus = NULL;
+
+ return 0;
+ } else
+ m->gc_focus = focus; /* start focussed */
+
+ r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate gc event source: %m");
+
+ r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE);
+ if (r < 0)
+ log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");
+
+ (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc");
+ return 1;
+}
diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h
new file mode 100644
index 0000000000000..00298a3d2dcbc
--- /dev/null
+++ b/src/home/homed-manager.h
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "sd-bus.h"
+#include "sd-device.h"
+#include "sd-event.h"
+
+typedef struct Manager Manager;
+
+#include "hashmap.h"
+#include "homed-home.h"
+#include "varlink.h"
+
+#define HOME_UID_MIN 60001
+#define HOME_UID_MAX 60513
+
+struct Manager {
+ sd_event *event;
+ sd_bus *bus;
+
+ Hashmap *polkit_registry;
+
+ Hashmap *homes_by_uid;
+ Hashmap *homes_by_name;
+ Hashmap *homes_by_worker_pid;
+ Hashmap *homes_by_sysfs;
+
+ bool scan_slash_home;
+
+ sd_event_source *inotify_event_source;
+
+ /* An even source we receieve sd_notify() messages from our worker from */
+ sd_event_source *notify_socket_event_source;
+
+ sd_device_monitor *device_monitor;
+
+ sd_event_source *deferred_rescan_event_source;
+ sd_event_source *deferred_gc_event_source;
+ sd_event_source *deferred_auto_login_event_source;
+
+ Home *gc_focus;
+
+ VarlinkServer *varlink_server;
+
+ EVP_PKEY *private_key; /* actually a pair of private and public key */
+ Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */
+};
+
+int manager_new(Manager **ret);
+Manager* manager_free(Manager *m);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+int manager_startup(Manager *m);
+
+int manager_augment_record_with_uid(Manager *m, UserRecord *hr);
+
+int manager_enqueue_rescan(Manager *m);
+int manager_enqueue_gc(Manager *m, Home *focus);
+
+int manager_verify_user_record(Manager *m, UserRecord *hr);
+
+int manager_acquire_key_pair(Manager *m);
+int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error);
+
+int bus_manager_emit_auto_login_changed(Manager *m);
diff --git a/src/home/homed-operation.c b/src/home/homed-operation.c
new file mode 100644
index 0000000000000..80dc555cd0e66
--- /dev/null
+++ b/src/home/homed-operation.c
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "fd-util.h"
+#include "homed-operation.h"
+
+Operation *operation_new(OperationType type, sd_bus_message *m) {
+ Operation *o;
+
+ assert(type >= 0);
+ assert(type < _OPERATION_MAX);
+
+ o = new(Operation, 1);
+ if (!o)
+ return NULL;
+
+ *o = (Operation) {
+ .type = type,
+ .n_ref = 1,
+ .message = sd_bus_message_ref(m),
+ .send_fd = -1,
+ .result = -1,
+ };
+
+ return o;
+}
+
+static Operation *operation_free(Operation *o) {
+ int r;
+
+ if (!o)
+ return NULL;
+
+ if (o->message && o->result >= 0) {
+
+ if (o->result) {
+ /* Propagate success */
+ if (o->send_fd < 0)
+ r = sd_bus_reply_method_return(o->message, NULL);
+ else
+ r = sd_bus_reply_method_return(o->message, "h", o->send_fd);
+
+ } else {
+ /* Propagate failure */
+ if (sd_bus_error_is_set(&o->error))
+ r = sd_bus_reply_method_error(o->message, &o->error);
+ else
+ r = sd_bus_reply_method_errnof(o->message, o->ret, "Failed to execute operation: %m");
+ }
+ if (r < 0)
+ log_warning_errno(r, "Failed ot reply to %s method call, ignoring: %m", sd_bus_message_get_member(o->message));
+ }
+
+ sd_bus_message_unref(o->message);
+ user_record_unref(o->secret);
+ safe_close(o->send_fd);
+ sd_bus_error_free(&o->error);
+
+ return mfree(o);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(Operation, operation, operation_free);
+
+void operation_result(Operation *o, int ret, const sd_bus_error *error) {
+ assert(o);
+
+ if (ret >= 0)
+ o->result = true;
+ else {
+ o->ret = ret;
+
+ sd_bus_error_free(&o->error);
+ sd_bus_error_copy(&o->error, error);
+
+ o->result = false;
+ }
+}
diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h
new file mode 100644
index 0000000000000..224de9185253e
--- /dev/null
+++ b/src/home/homed-operation.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "user-record.h"
+
+typedef enum OperationType {
+ OPERATION_ACQUIRE, /* enqueued on AcquireHome() */
+ OPERATION_RELEASE, /* enqueued on ReleaseHome() */
+ OPERATION_LOCK_ALL, /* enqueued on LockAllHomes() */
+ OPERATION_PIPE_EOF, /* enqueued when we see EOF on the per-home reference pipes */
+ OPERATION_DEACTIVATE_FORCE, /* enqueued on hard $HOME unplug */
+ OPERATION_IMMEDIATE, /* this is never enqueued, it's just a marker we immediately started executing an operation without enqueuing anything first. */
+ _OPERATION_MAX,
+ _OPERATION_INVALID = -1,
+} OperationType;
+
+/* Encapsulates an operation on one or more home directories. This has two uses:
+ *
+ * 1) For queuing an operation when we need to execute one for some reason but there's already one being
+ * executed.
+ *
+ * 2) When executing an operation without enqueuing it first (OPERATION_IMMEDIATE)
+ *
+ * Note that a single operation object can encapsulate operations on multiple home directories. This is used
+ * for the LockAllHomes() operation, which is one operation but applies to all homes at once. In case the
+ * operation applies to multiple homes the reference counter is increased once for each, and thus the
+ * operation is fully completed only after it reached zero again.
+ *
+ * The object (optionally) contains a reference of the D-Bus message triggering the operation, which is
+ * replied to when the operation is fully completed, i.e. when n_ref reaches zero.
+ */
+
+typedef struct Operation {
+ unsigned n_ref;
+ OperationType type;
+ sd_bus_message *message;
+
+ UserRecord *secret;
+ int send_fd; /* pipe fd for AcquireHome() which is taken already when we start the operation */
+
+ int result; /* < 0 if not completed yet, == 0 on failure, > 0 on success */
+ sd_bus_error error;
+ int ret;
+} Operation;
+
+Operation *operation_new(OperationType type, sd_bus_message *m);
+Operation *operation_ref(Operation *operation);
+Operation *operation_unref(Operation *operation);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_unref);
+
+void operation_result(Operation *o, int ret, const sd_bus_error *error);
+
+static inline Operation* operation_result_unref(Operation *o, int ret, const sd_bus_error *error) {
+ if (!o)
+ return NULL;
+
+ operation_result(o, ret, error);
+ return operation_unref(o);
+}
diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c
new file mode 100644
index 0000000000000..4ef0c716171d5
--- /dev/null
+++ b/src/home/homed-varlink.c
@@ -0,0 +1,368 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "group-record.h"
+#include "homed-varlink.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "format-util.h"
+
+typedef struct LookupParameters {
+ const char *user_name;
+ const char *group_name;
+ union {
+ uid_t uid;
+ gid_t gid;
+ };
+ const char *service;
+} LookupParameters;
+
+static bool client_is_trusted(Varlink *link, Home *h) {
+ uid_t peer_uid;
+ int r;
+
+ assert(link);
+ assert(h);
+
+ r = varlink_get_peer_uid(link, &peer_uid);
+ if (r < 0) {
+ log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
+ return false;
+ }
+
+ return peer_uid == 0 || peer_uid == h->uid;
+}
+
+static int build_user_json(Home *h, bool trusted, JsonVariant **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *augmented = NULL;
+ UserRecordLoadFlags flags;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+ if (trusted)
+ flags |= USER_RECORD_ALLOW_PRIVILEGED;
+ else
+ flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+ r = home_augment_status(h, flags, &augmented);
+ if (r < 0)
+ return r;
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(augmented->json)),
+ JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(augmented->incomplete))));
+}
+
+static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) {
+ assert(p);
+ assert(h);
+
+ if (p->user_name && !streq(p->user_name, h->user_name))
+ return false;
+
+ if (uid_is_valid(p->uid) && h->uid != p->uid)
+ return false;
+
+ return true;
+}
+
+int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 },
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ LookupParameters p = {
+ .uid = UID_INVALID,
+ };
+ Manager *m = userdata;
+ bool trusted;
+ Home *h;
+ int r;
+
+ assert(parameters);
+ assert(m);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.Home"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ trusted = client_is_trusted(link, h);
+
+ if (uid_is_valid(p.uid))
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid));
+ else if (p.user_name)
+ h = hashmap_get(m->homes_by_name, p.user_name);
+ else {
+ Iterator i;
+
+ /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify()
+ * for all entries but the last, so that clients can stream the results, and easily process
+ * them piecemeal. */
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+ if (!home_user_match_lookup_parameters(&p, h))
+ continue;
+
+ if (v) {
+ /* An entry set from the previous iteration? Then send it now */
+ r = varlink_notify(link, v);
+ if (r < 0)
+ return r;
+
+ v = json_variant_unref(v);
+ }
+
+ r = build_user_json(h, trusted, &v);
+ if (r < 0)
+ return r;
+ }
+
+ if (!v)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, v);
+ }
+
+ if (!h)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ if (!home_user_match_lookup_parameters(&p, h))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_user_json(h, trusted, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+static int build_group_json(Home *h, JsonVariant **ret) {
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ g = group_record_new();
+ if (!g)
+ return -ENOMEM;
+
+ r = group_record_synthesize(g, h->record);
+ if (r < 0)
+ return r;
+
+ assert(!FLAGS_SET(g->mask, USER_RECORD_SECRET));
+ assert(!FLAGS_SET(g->mask, USER_RECORD_PRIVILEGED));
+
+ return json_build(ret,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(g->json))));
+}
+
+static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) {
+ assert(p);
+ assert(h);
+
+ if (p->group_name && !streq(h->user_name, p->group_name))
+ return false;
+
+ if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid)
+ return false;
+
+ return true;
+}
+
+int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ LookupParameters p = {
+ .gid = GID_INVALID,
+ };
+ Manager *m = userdata;
+ Home *h;
+ int r;
+
+ assert(parameters);
+ assert(m);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.Home"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ if (gid_is_valid(p.gid))
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid));
+ else if (p.group_name)
+ h = hashmap_get(m->homes_by_name, p.group_name);
+ else {
+ Iterator i;
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+ if (!home_group_match_lookup_parameters(&p, h))
+ continue;
+
+ if (v) {
+ r = varlink_notify(link, v);
+ if (r < 0)
+ return r;
+
+ v = json_variant_unref(v);
+ }
+
+ r = build_group_json(h, &v);
+ if (r < 0)
+ return r;
+ }
+
+ if (!v)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, v);
+ }
+
+ if (!h)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ if (!home_group_match_lookup_parameters(&p, h))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_group_json(h, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ Manager *m = userdata;
+ LookupParameters p = {};
+ Home *h;
+ int r;
+
+ assert(parameters);
+ assert(m);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (!streq_ptr(p.service, "io.systemd.Home"))
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+
+ if (p.user_name) {
+ const char *last = NULL;
+ char **i;
+
+ h = hashmap_get(m->homes_by_name, p.user_name);
+ if (!h)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ if (p.group_name) {
+ if (!strv_contains(h->record->member_of, p.group_name))
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+ }
+
+ STRV_FOREACH(i, h->record->member_of) {
+ if (last) {
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last))));
+ if (r < 0)
+ return r;
+ }
+
+ last = *i;
+ }
+
+ if (last)
+ return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last))));
+
+ } else if (p.group_name) {
+ const char *last = NULL;
+ Iterator i;
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+
+ if (!strv_contains(h->record->member_of, p.group_name))
+ continue;
+
+ if (last) {
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+ if (r < 0)
+ return r;
+ }
+
+ last = h->user_name;
+ }
+
+ if (last)
+ return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name))));
+ } else {
+ const char *last_user_name = NULL, *last_group_name = NULL;
+ Iterator i;
+
+ HASHMAP_FOREACH(h, m->homes_by_name, i) {
+ char **j;
+
+ STRV_FOREACH(j, h->record->member_of) {
+
+ if (last_user_name) {
+ assert(last_group_name);
+
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+
+ if (r < 0)
+ return r;
+ }
+
+ last_user_name = h->user_name;
+ last_group_name = *j;
+ }
+ }
+
+ if (last_user_name) {
+ assert(last_group_name);
+ return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+ }
+ }
+
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+}
diff --git a/src/home/homed-varlink.h b/src/home/homed-varlink.h
new file mode 100644
index 0000000000000..4454d2344221f
--- /dev/null
+++ b/src/home/homed-varlink.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "homed-manager.h"
+
+int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
diff --git a/src/home/homed.c b/src/home/homed.c
new file mode 100644
index 0000000000000..cd445354a1e13
--- /dev/null
+++ b/src/home/homed.c
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "daemon-util.h"
+#include "homed-manager.h"
+#include "log.h"
+#include "main-func.h"
+#include "signal-util.h"
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ log_setup_service();
+
+ // FIXME
+ log_set_max_level(LOG_DEBUG);
+
+ umask(0022);
+
+ if (argc != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.Home", 1) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -1) >= 0);
+
+ r = manager_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Could not create manager: %m");
+
+ r = manager_startup(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start up daemon: %m");
+
+ notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
+
+ r = sd_event_loop(m->event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/homework.c b/src/home/homework.c
new file mode 100644
index 0000000000000..95b5d442002a1
--- /dev/null
+++ b/src/home/homework.c
@@ -0,0 +1,5017 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "blkid-util.h"
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "chattr-util.h"
+#include "chown-recursive.h"
+#include "copy.h"
+#include "crypt-util.h"
+#include "dirent-util.h"
+#include "dm-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "fsck-util.h"
+#include "hexdecoct.h"
+#include "home-util.h"
+#include "id128-util.h"
+#include "io-util.h"
+#include "json.h"
+#include "log.h"
+#include "loop-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "missing_magic.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "mountpoint-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "resize-fs.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "umask-util.h"
+#include "user-record-util.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "virt.h"
+#include "xattr-util.h"
+
+/* Round down to the nearest 1K size. Note that Linux generally handles block devices with 512 blocks only,
+ * but actually doesn't accept uneven numbers in many cases. To avoid any confusion around this we'll
+ * strictly round disk sizes down to the next 1K boundary.*/
+#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023))
+
+/* Make sure a bad password always results in a 3s delay, no matter what */
+#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC)
+
+static bool supported_fstype(const char *fstype) {
+ /* Limit the set of supported file systems a bit, as protection against little tested kernel file
+ * systems. Also, we only support the resize ioctls for these file systems. */
+ return STR_IN_SET(fstype, "ext4", "btrfs", "xfs");
+}
+
+static const char *mount_options_for_fstype(const char *fstype) {
+ if (streq(fstype, "ext4"))
+ return "noquota,user_xattr";
+ if (streq(fstype, "xfs"))
+ return "noquota";
+ if (streq(fstype, "btrfs"))
+ return "noacl";
+ return NULL;
+}
+
+static int probe_file_system_by_fd(
+ int fd,
+ char **ret_fstype,
+ sd_id128_t *ret_uuid) {
+
+ _cleanup_(blkid_free_probep) blkid_probe b = NULL;
+ _cleanup_free_ char *s = NULL;
+ const char *fstype = NULL, *uuid = NULL;
+ sd_id128_t id;
+ int r;
+
+ assert(fd >= 0);
+ assert(ret_fstype);
+ assert(ret_uuid);
+
+ b = blkid_new_probe();
+ if (!b)
+ return -ENOMEM;
+
+ errno = 0;
+ r = blkid_probe_set_device(b, fd, 0, 0);
+ if (r != 0)
+ return errno > 0 ? -errno : -ENOMEM;
+
+ (void) blkid_probe_enable_superblocks(b, 1);
+ (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID);
+
+ errno = 0;
+ r = blkid_do_safeprobe(b);
+ if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */
+ return -ENOPKG;
+ if (r != 0)
+ return errno > 0 ? -errno : -EIO;
+
+ (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL);
+ if (!fstype)
+ return -ENOPKG;
+
+ (void) blkid_probe_lookup_value(b, "UUID", &uuid, NULL);
+ if (!uuid)
+ return -ENOPKG;
+
+ r = sd_id128_from_string(uuid, &id);
+ if (r < 0)
+ return r;
+
+ s = strdup(fstype);
+ if (!s)
+ return -ENOMEM;
+
+ *ret_fstype = TAKE_PTR(s);
+ *ret_uuid = id;
+
+ return 0;
+}
+
+static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id128_t *ret_uuid) {
+ _cleanup_close_ int fd = -1;
+
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (fd < 0)
+ return -errno;
+
+ return probe_file_system_by_fd(fd, ret_fstype, ret_uuid);
+}
+
+static int block_get_size_by_fd(int fd, uint64_t *ret) {
+ struct stat st;
+
+ assert(fd >= 0);
+ assert(ret);
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+
+ if (!S_ISBLK(st.st_mode))
+ return -ENOTBLK;
+
+ if (ioctl(fd, BLKGETSIZE64, ret) < 0)
+ return -errno;
+
+ return 0;
+}
+
+static int block_get_size_by_path(const char *path, uint64_t *ret) {
+ _cleanup_close_ int fd = -1;
+
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (fd < 0)
+ return -errno;
+
+ return block_get_size_by_fd(fd, ret);
+}
+
+static int run_fsck(const char *node, const char *fstype) {
+ int r, exit_status;
+ pid_t fsck_pid;
+
+ assert(node);
+ assert(fstype);
+
+ r = fsck_exists(fstype);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if fsck for file system %s exists: %m", fstype);
+ if (r == 0) {
+ log_warning("No fsck for file system %s installed, ignoring.", fstype);
+ return 0;
+ }
+
+ r = safe_fork("(fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execl("/sbin/fsck", "/sbin/fsck", "-aTl", node, NULL);
+ log_error_errno(errno, "Failed to execute fsck: %m");
+ _exit(FSCK_OPERATIONAL_ERROR);
+ }
+
+ exit_status = wait_for_terminate_and_check("fsck", fsck_pid, WAIT_LOG_ABNORMAL);
+ if (exit_status < 0)
+ return exit_status;
+ if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) {
+ log_warning("fsck failed with exit status %i.", exit_status);
+
+ if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing.");
+
+ log_warning("Ignoring fsck error.");
+ }
+
+ log_info("File system check completed.");
+
+ return 1;
+}
+
+static int luks_setup(
+ const char *node,
+ const char *dm_name,
+ sd_id128_t uuid,
+ const char *cipher,
+ const char *cipher_mode,
+ uint64_t volume_key_size,
+ char **passwords,
+ bool discard,
+ struct crypt_device **ret,
+ sd_id128_t *ret_found_uuid,
+ void **ret_volume_key,
+ size_t *ret_volume_key_size) {
+
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ sd_id128_t p;
+ size_t ks;
+ char **pp;
+ int r;
+
+ assert(node);
+ assert(dm_name);
+ assert(ret);
+
+ r = crypt_init(&cd, node);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ r = crypt_load(cd, CRYPT_LUKS2, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ ks = (size_t) r;
+
+ if (!sd_id128_is_null(uuid) || ret_found_uuid) {
+ const char *s;
+
+ s = crypt_get_uuid(cd);
+ if (!s)
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID.");
+
+ r = sd_id128_from_string(s, &p);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID.");
+
+ /* Check that the UUID matches, if specified */
+ if (!sd_id128_is_null(uuid) &&
+ !sd_id128_equal(uuid, p))
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has wrong UUID.");
+ }
+
+ if (cipher && !streq_ptr(cipher, crypt_get_cipher(cd)))
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher.");
+
+ if (cipher_mode && !streq_ptr(cipher_mode, crypt_get_cipher_mode(cd)))
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher mode.");
+
+ if (volume_key_size != UINT64_MAX && ks != volume_key_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong volume key size.");
+
+ vk = malloc(ks);
+ if (!vk)
+ return log_oom();
+
+ STRV_FOREACH(pp, passwords) {
+ size_t vks = ks;
+
+ r = crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ vk,
+ &vks,
+ *pp,
+ strlen(*pp));
+ if (r < 0) {
+ log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords));
+ continue;
+ }
+
+ r = crypt_activate_by_volume_key(
+ cd,
+ dm_name,
+ vk, vks,
+ discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to unlock LUKS superblock: %m");
+
+ log_info("Setting up LUKS device /dev/mapper/%s completed.", dm_name);
+
+ *ret = TAKE_PTR(cd);
+
+ if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */
+ *ret_found_uuid = p;
+ if (ret_volume_key)
+ *ret_volume_key = TAKE_PTR(vk);
+ if (ret_volume_key_size)
+ *ret_volume_key_size = vks;
+
+ return 0;
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock.");
+}
+
+static int luks_open(
+ const char *dm_name,
+ char **passwords,
+ struct crypt_device **ret,
+ sd_id128_t *ret_found_uuid,
+ void **ret_volume_key,
+ size_t *ret_volume_key_size) {
+
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ sd_id128_t p;
+ size_t ks;
+ char **pp;
+ int r;
+
+ assert(dm_name);
+ assert(ret);
+
+ /* Opens a LUKS device that is already set up. Re-validates the password while doing so (which also
+ * provides us with the volume key, which we want). */
+
+ r = crypt_init_by_name(&cd, dm_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ r = crypt_load(cd, CRYPT_LUKS2, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to load LUKS superblock: %m");
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ ks = (size_t) r;
+
+ if (ret_found_uuid) {
+ const char *s;
+
+ s = crypt_get_uuid(cd);
+ if (!s)
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID.");
+
+ r = sd_id128_from_string(s, &p);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID.");
+ }
+
+ vk = malloc(ks);
+ if (!vk)
+ return log_oom();
+
+ STRV_FOREACH(pp, passwords) {
+ size_t vks = ks;
+
+ r = crypt_volume_key_get(
+ cd,
+ CRYPT_ANY_SLOT,
+ vk,
+ &vks,
+ *pp,
+ strlen(*pp));
+ if (r < 0) {
+ log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords));
+ continue;
+ }
+
+ log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", dm_name);
+
+ /* This is needed so that crypt_resize() can operate correctly for pre-existing LUKS
+ * devices. We need to tell libcryptsetup the volume key explicitly, so that it is in the
+ * kernel keyring. */
+ r = crypt_activate_by_volume_key(cd, NULL, vk, vks, CRYPT_ACTIVATE_KEYRING_KEY);
+ if (r < 0)
+ return log_error_errno(r, "Failed to upload volume key again: %m");
+
+ log_info("Successfully re-activated LUKS device.");
+
+ *ret = TAKE_PTR(cd);
+
+ if (ret_found_uuid)
+ *ret_found_uuid = p;
+ if (ret_volume_key)
+ *ret_volume_key = TAKE_PTR(vk);
+ if (ret_volume_key_size)
+ *ret_volume_key_size = ks;
+
+ return 0;
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock.");
+}
+
+static int fs_validate(
+ const char *dm_node,
+ sd_id128_t uuid,
+ char **ret_fstype,
+ sd_id128_t *ret_found_uuid) {
+
+ _cleanup_free_ char *fstype = NULL;
+ sd_id128_t u;
+ int r;
+
+ assert(dm_node);
+ assert(ret_fstype);
+
+ r = probe_file_system_by_path(dm_node, &fstype, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to probe file system: %m");
+
+ /* Limit the set of supported file systems a bit, as protection against little tested kernel file
+ * systems. Also, we only support the resize ioctls for these file systems. */
+ if (!supported_fstype(fstype))
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Image contains unsupported file system: %s", strna(fstype));
+
+ if (!sd_id128_is_null(uuid) &&
+ !sd_id128_equal(uuid, u))
+ return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "File system has wrong UUID.");
+
+ log_info("Probing file system completed (found %s).", fstype);
+
+ *ret_fstype = TAKE_PTR(fstype);
+
+ if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */
+ *ret_found_uuid = u;
+
+ return 0;
+}
+
+static int mount_node(const char *node, const char *fstype, bool discard) {
+ _cleanup_free_ char *joined = NULL;
+ const char *options, *discard_option;
+ int r;
+
+ options = mount_options_for_fstype(fstype);
+
+ discard_option = discard ? "discard" : "nodiscard";
+
+ if (options) {
+ joined = strjoin(options, ",", discard_option);
+ if (!joined)
+ return log_oom();
+
+ options = joined;
+ } else
+ options = discard_option;
+
+ r = mount_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, MS_NODEV|MS_NOSUID|MS_RELATIME, strempty(options));
+ if (r < 0)
+ return r;
+
+ log_info("Mounting file system completed.");
+ return 0;
+}
+
+static int unshare_and_mount(const char *node, const char *fstype, bool discard) {
+ int r;
+
+ if (unshare(CLONE_NEWNS) < 0)
+ return log_error_errno(errno, "Couldn't unshare file system namespace: %m");
+
+ r = mount_verbose(LOG_ERR, "/run", "/run", NULL, MS_SLAVE|MS_REC, NULL); /* Mark /run as MS_SLAVE in our new namespace */
+ if (r < 0)
+ return r;
+
+ (void) mkdir_p("/run/systemd/user-home-mount", 0700);
+
+ if (node)
+ return mount_node(node, fstype, discard);
+
+ return 0;
+}
+
+typedef struct HomeSetup {
+ char *dm_name;
+ char *dm_node;
+
+ LoopDevice *loop;
+ struct crypt_device *crypt_device;
+ int root_fd;
+ sd_id128_t found_partition_uuid;
+ sd_id128_t found_luks_uuid;
+ sd_id128_t found_fs_uuid;
+
+ void *found_fscrypt_salt;
+ size_t found_fscrypt_salt_size;
+
+ void *volume_key;
+ size_t volume_key_size;
+
+ bool undo_dm;
+ bool undo_mount;
+
+ uint64_t partition_offset;
+ uint64_t partition_size;
+} HomeSetup;
+
+#define HOME_SETUP_INIT \
+ { \
+ .root_fd = -1, \
+ .partition_offset = UINT64_MAX, \
+ .partition_size = UINT64_MAX, \
+ }
+
+static int home_setup_undo(HomeSetup *setup) {
+ int r = 0, q;
+
+ assert(setup);
+
+ setup->root_fd = safe_close(setup->root_fd);
+
+ if (setup->undo_mount) {
+ q = umount_verbose("/run/systemd/user-home-mount");
+ if (q < 0)
+ r = q;
+ }
+
+ if (setup->undo_dm && setup->crypt_device && setup->dm_name) {
+ q = crypt_deactivate(setup->crypt_device, setup->dm_name);
+ if (q < 0)
+ r = q;
+ }
+
+ setup->undo_mount = false;
+ setup->undo_dm = false;
+
+ setup->dm_name = mfree(setup->dm_name);
+ setup->dm_node = mfree(setup->dm_node);
+
+ setup->loop = loop_device_unref(setup->loop);
+ crypt_free(setup->crypt_device);
+ setup->crypt_device = NULL;
+
+ explicit_bzero_safe(setup->volume_key, setup->volume_key_size);
+ setup->volume_key = mfree(setup->volume_key);
+ setup->volume_key_size = 0;
+
+ setup->found_fscrypt_salt = mfree(setup->found_fscrypt_salt);
+ setup->found_fscrypt_salt_size = 0;
+
+ return r;
+}
+
+static int make_dm_names(const char *user_name, char **ret_dm_name, char **ret_dm_node) {
+ _cleanup_free_ char *name = NULL, *node = NULL;
+
+ assert(user_name);
+ assert(ret_dm_name);
+ assert(ret_dm_node);
+
+ name = strjoin("home-", user_name);
+ if (!name)
+ return log_oom();
+
+ node = path_join("/dev/mapper/", name);
+ if (!node)
+ return log_oom();
+
+ *ret_dm_name = TAKE_PTR(name);
+ *ret_dm_node = TAKE_PTR(node);
+ return 0;
+}
+
+static int luks_validate(
+ int fd,
+ const char *label,
+ sd_id128_t partition_uuid,
+ sd_id128_t *ret_partition_uuid,
+ uint64_t *ret_offset,
+ uint64_t *ret_size) {
+
+ _cleanup_(blkid_free_probep) blkid_probe b = NULL;
+ sd_id128_t found_partition_uuid = SD_ID128_NULL;
+ const char *fstype = NULL, *pttype = NULL;
+ blkid_loff_t offset = 0, size = 0;
+ blkid_partlist pl;
+ bool found = false;
+ int r, i, n;
+
+ assert(fd >= 0);
+ assert(label);
+ assert(ret_offset);
+ assert(ret_size);
+
+ b = blkid_new_probe();
+ if (!b)
+ return -ENOMEM;
+
+ errno = 0;
+ r = blkid_probe_set_device(b, fd, 0, 0);
+ if (r != 0)
+ return errno > 0 ? -errno : -ENOMEM;
+
+ (void) blkid_probe_enable_superblocks(b, 1);
+ (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE);
+ (void) blkid_probe_enable_partitions(b, 1);
+ (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS);
+
+ errno = 0;
+ r = blkid_do_safeprobe(b);
+ if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */
+ return -ENOPKG;
+ if (r != 0)
+ return errno > 0 ? -errno : -EIO;
+
+ (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL);
+ if (streq_ptr(fstype, "crypto_LUKS")) {
+ /* Directly a LUKS image */
+ *ret_offset = 0;
+ *ret_size = UINT64_MAX; /* full disk */
+ *ret_partition_uuid = SD_ID128_NULL;
+ return 0;
+ } else if (fstype)
+ return -ENOPKG;
+
+ (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL);
+ if (!streq_ptr(pttype, "gpt"))
+ return -ENOPKG;
+
+ errno = 0;
+ pl = blkid_probe_get_partitions(b);
+ if (!pl)
+ return errno > 0 ? -errno : -ENOMEM;
+
+ errno = 0;
+ n = blkid_partlist_numof_partitions(pl);
+ if (n < 0)
+ return errno > 0 ? -errno : -EIO;
+
+ for (i = 0; i < n; i++) {
+ blkid_partition pp;
+ sd_id128_t id;
+ const char *sid;
+
+ errno = 0;
+ pp = blkid_partlist_get_partition(pl, i);
+ if (!pp)
+ return errno > 0 ? -errno : -EIO;
+
+ if (!streq_ptr(blkid_partition_get_type_string(pp), "773f91ef-66d4-49b5-bd83-d683bf40ad16"))
+ continue;
+
+ if (!streq_ptr(blkid_partition_get_name(pp), label))
+ continue;
+
+ sid = blkid_partition_get_uuid(pp);
+ if (sid) {
+ r = sd_id128_from_string(sid, &id);
+ if (r < 0)
+ log_debug_errno(r, "Couldn't parse partition UUID %s, weird: %m", sid);
+
+ if (!sd_id128_is_null(partition_uuid) && !sd_id128_equal(id, partition_uuid))
+ continue;
+ }
+
+ if (found)
+ return -ENOPKG;
+
+ offset = blkid_partition_get_start(pp);
+ size = blkid_partition_get_size(pp);
+ found_partition_uuid = id;
+
+ found = true;
+ }
+
+ if (!found)
+ return -ENOPKG;
+
+ if (offset < 0)
+ return -EINVAL;
+ if ((uint64_t) offset > UINT64_MAX / 512U)
+ return -EINVAL;
+ if (size <= 0)
+ return -EINVAL;
+ if ((uint64_t) size > UINT64_MAX / 512U)
+ return -EINVAL;
+
+ *ret_offset = offset * 512U;
+ *ret_size = size * 512U;
+ *ret_partition_uuid = found_partition_uuid;
+
+ return 0;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free);
+
+static int crypt_device_to_evp_cipher(struct crypt_device *cd, const EVP_CIPHER **ret) {
+ _cleanup_free_ char *cipher_name = NULL;
+ const char *cipher, *cipher_mode, *e;
+ size_t key_size, key_bits;
+ const EVP_CIPHER *cc;
+ int r;
+
+ assert(cd);
+
+ /* Let's find the right OpenSSL EVP_CIPHER object that matches the encryption settings of the LUKS
+ * device */
+
+ cipher = crypt_get_cipher(cd);
+ if (!cipher)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device.");
+
+ cipher_mode = crypt_get_cipher_mode(cd);
+ if (!cipher_mode)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher mode from LUKS device.");
+
+ e = strchr(cipher_mode, '-');
+ if (e)
+ cipher_mode = strndupa(cipher_mode, e - cipher_mode);
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Cannot get volume key size from LUKS device.");
+
+ key_size = r;
+ key_bits = key_size * 8;
+ if (streq(cipher_mode, "xts"))
+ key_bits /= 2;
+
+ if (asprintf(&cipher_name, "%s-%zu-%s", cipher, key_bits, cipher_mode) < 0)
+ return log_oom();
+
+ cc = EVP_get_cipherbyname(cipher_name);
+ if (!cc)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected cipher mode '%s' not supported, can't encrypt JSON record.", cipher_name);
+
+ /* Verify that our key length calculations match what OpenSSL thinks */
+ r = EVP_CIPHER_key_length(cc);
+ if (r < 0 || (uint64_t) r != key_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key size of selected cipher doesn't meet out expectations.");
+
+ *ret = cc;
+ return 0;
+}
+
+static int luks_validate_home_record(
+ struct crypt_device *cd,
+ UserRecord *h,
+ const void *volume_key,
+ UserRecord **ret_luks_home_record) {
+
+ int r, token;
+
+ assert(cd);
+ assert(h);
+
+ for (token = 0;; token++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *rr = NULL;
+ _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *lhr = NULL;
+ _cleanup_free_ void *encrypted = NULL, *iv = NULL;
+ size_t decrypted_size, encrypted_size, iv_size;
+ int decrypted_size_out1, decrypted_size_out2;
+ _cleanup_free_ char *decrypted = NULL;
+ const char *text, *type;
+ crypt_token_info state;
+ JsonVariant *jr, *jiv;
+ unsigned line, column;
+ const EVP_CIPHER *cc;
+
+ state = crypt_token_status(cd, token, &type);
+ if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, give up */
+ break;
+ if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL))
+ continue;
+ if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state);
+
+ if (!streq(type, "systemd-homed"))
+ continue;
+
+ r = crypt_token_json_get(cd, token, &text);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read LUKS token %i: %m", token);
+
+ r = json_parse(text, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse LUKS token JSON data %u:%u: %m", line, column);
+
+ jr = json_variant_by_key(v, "record");
+ if (!jr)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'record' field.");
+ jiv = json_variant_by_key(v, "iv");
+ if (!jiv)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'iv' field.");
+
+ r = json_variant_unbase64(jr, &encrypted, &encrypted_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 decode record: %m");
+
+ r = json_variant_unbase64(jiv, &iv, &iv_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to base64 decode IV: %m");
+
+ r = crypt_device_to_evp_cipher(cd, &cc);
+ if (r < 0)
+ return r;
+ if (iv_size > INT_MAX || EVP_CIPHER_iv_length(cc) != (int) iv_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IV size doesn't match.");
+
+ context = EVP_CIPHER_CTX_new();
+ if (!context)
+ return log_oom();
+
+ if (EVP_DecryptInit_ex(context, cc, NULL, volume_key, iv) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context.");
+
+ decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2;
+ decrypted = new(char, decrypted_size);
+ if (!decrypted)
+ return log_oom();
+
+ if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt JSON record.");
+
+ assert((size_t) decrypted_size_out1 <= decrypted_size);
+
+ if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryptio of JSON record.");
+
+ assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size);
+ decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2;
+
+ if (memchr(decrypted, 0, decrypted_size))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inner NUL byte in JSON record, refusing.");
+
+ decrypted[decrypted_size] = 0;
+
+ r = json_parse(decrypted, JSON_PARSE_SENSITIVE, &rr, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse decrypted JSON record, refusing.");
+
+ lhr = user_record_new();
+ if (!lhr)
+ return log_oom();
+
+ r = user_record_load(lhr, rr, USER_RECORD_LOAD_EMBEDDED);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse user record: %m");
+
+ if (!user_record_compatible(h, lhr))
+ return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing.");
+
+ r = user_record_test_secret(lhr, h);
+ if (r == -ENXIO)
+ return log_error_errno(r, "Embedded LUKS user record contains no hashed password, invalid.");
+ if (r == -ENOKEY)
+ return log_error_errno(r, "Supplied password does not match password of embedded LUKS user record.");
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate password of embedded LUKS user record: %m");
+
+ log_info("Provided password unlocks embedded LUKS user record.");
+
+ *ret_luks_home_record = TAKE_PTR(lhr);
+ return 0;
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find home record in LUKS2 header, refusing.");
+}
+
+static int format_luks_token_text(
+ struct crypt_device *cd,
+ UserRecord *hr,
+ const void *volume_key,
+ char **ret) {
+
+ int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0, iv_size, key_size;
+ _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_free_ void *iv = NULL, *encrypted = NULL;
+ size_t text_length, encrypted_size;
+ _cleanup_free_ char *text = NULL;
+ const EVP_CIPHER *cc;
+
+ assert(cd);
+ assert(hr);
+ assert(volume_key);
+ assert(ret);
+
+ r = crypt_device_to_evp_cipher(cd, &cc);
+ if (r < 0)
+ return r;
+
+ key_size = EVP_CIPHER_key_length(cc);
+ iv_size = EVP_CIPHER_iv_length(cc);
+
+ if (iv_size > 0) {
+ iv = malloc(iv_size);
+ if (!iv)
+ return log_oom();
+
+ r = genuine_random_bytes(iv, iv_size, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate IV: %m");
+ }
+
+ context = EVP_CIPHER_CTX_new();
+ if (!context)
+ return log_oom();
+
+ if (EVP_EncryptInit_ex(context, cc, NULL, volume_key, iv) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context.");
+
+ r = json_variant_format(hr->json, 0, &text);
+ if (r < 0)
+ return log_error_errno(r,"Failed to format user record for LUKS: %m");
+
+ text_length = strlen(text);
+ encrypted_size = text_length + 2*key_size - 1;
+
+ encrypted = malloc(encrypted_size);
+ if (!encrypted)
+ return log_oom();
+
+ if (EVP_EncryptUpdate(context, encrypted, &encrypted_size_out1, (uint8_t*) text, text_length) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt JSON record.");
+
+ assert((size_t) encrypted_size_out1 <= encrypted_size);
+
+ if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of JSON record. ");
+
+ assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size);
+
+ r = json_build(&v,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-homed")),
+ JSON_BUILD_PAIR("keyslots", JSON_BUILD_EMPTY_ARRAY),
+ JSON_BUILD_PAIR("record", JSON_BUILD_BASE64(encrypted, encrypted_size_out1 + encrypted_size_out2)),
+ JSON_BUILD_PAIR("iv", JSON_BUILD_BASE64(iv, iv_size))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare LUKS JSON token object: %m");
+
+ r = json_variant_format(v, 0, ret);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format encrypted user record for LUKS: %m");
+
+ return 0;
+}
+
+static int luks_store_header_identity(
+ UserRecord *h,
+ struct crypt_device *cd,
+ const void *volume_key,
+ UserRecord *old_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *header_home = NULL;
+ _cleanup_free_ char *text = NULL;
+ int token = 0, r;
+
+ assert(h);
+
+ if (!cd)
+ return 0;
+
+ assert(volume_key);
+
+ /* Let's store the user's identity record in the LUKS2 "token" header data fields, in an encrypted
+ * fashion. Why that? If we'd rely on the record being embedded in the payload file system itself we
+ * would have to mount the file system before we can validate the JSON record, its signatures and
+ * whether it matches what we are looking for. However, kernel file system implementations are
+ * generally not ready to be used on untrusted media. Hence let's store the record independently of
+ * the file system, so that we can validate it first, and only then mount the file system. To keep
+ * things simple we use the same encryption settings for this record as for the file system itself. */
+
+ r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &header_home);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine new header record: %m");
+
+ if (old_home && user_record_equal(old_home, header_home)) {
+ log_debug("Not updating header home record.");
+ return 0;
+ }
+
+ r = format_luks_token_text(cd, header_home, volume_key, &text);
+ if (r < 0)
+ return r;
+
+ for (;; token++) {
+ crypt_token_info state;
+ const char *type;
+
+ state = crypt_token_status(cd, token, &type);
+ if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, we are done */
+ break;
+ if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL))
+ continue; /* Not ours */
+ if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state);
+
+ if (!streq(type, "systemd-homed"))
+ continue;
+
+ r = crypt_token_json_set(cd, token, text);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set JSON token for slot %i: %m", token);
+
+ /* Now, let's free the text so that for all further matching tokens we all crypt_json_token_set()
+ * with a NULL text in order to invalidate the tokens. */
+ text = mfree(text);
+ token++;
+ }
+
+ if (text)
+ return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Didn't find any record token to update.");
+
+ log_info("Wrote LUKS header user record.");
+
+ return 1;
+}
+
+static int run_fitrim(int root_fd) {
+ char buf[FORMAT_BYTES_MAX];
+ struct fstrim_range range = {
+ .len = UINT64_MAX,
+ };
+
+ /* If discarding is on, discard everything right after mounting, so that the discard setting takes
+ * effect on activation. */
+
+ assert(root_fd >= 0);
+
+ if (ioctl(root_fd, FITRIM, &range) < 0) {
+ if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) {
+ log_debug_errno(errno, "File system does not support FITRIM, not trimming.");
+ return 0;
+ }
+
+ return log_warning_errno(errno, "Failed to invoke FITRIM, ignoring: %m");
+ }
+
+ log_info("Discarded unused %s.",
+ format_bytes(buf, sizeof(buf), range.len));
+ return 1;
+}
+
+static int run_fallocate(int backing_fd, const struct stat *st) {
+ char buf[FORMAT_BYTES_MAX];
+
+ assert(backing_fd >= 0);
+ assert(st);
+
+ /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes
+ * effect on activation */
+
+ if (!S_ISREG(st->st_mode))
+ return 0;
+
+ if (st->st_blocks >= DIV_ROUND_UP(st->st_size, 512)) {
+ log_info("Backing file is fully allocated already.");
+ return 0;
+ }
+
+ if (fallocate(backing_fd, FALLOC_FL_KEEP_SIZE, 0, st->st_size) < 0) {
+
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_debug_errno(errno, "fallocate() not supported on file system, ignoring.");
+ return 0;
+ }
+
+ if (ERRNO_IS_DISK_SPACE(errno)) {
+ log_debug_errno(errno, "Not enough disk space to fully allocate home.");
+ return -ENOSPC; /* make recognizable */
+ }
+
+ return log_error_errno(errno, "Failed to allocate backing file blocks: %m");
+ }
+
+ log_info("Allocated additional %s.",
+ format_bytes(buf, sizeof(buf), (DIV_ROUND_UP(st->st_size, 512) - st->st_blocks) * 512));
+ return 1;
+}
+
+static int home_prepare_luks(
+ UserRecord *h,
+ bool already_activated,
+ const char *force_image_path,
+ HomeSetup *setup,
+ UserRecord **ret_luks_home) {
+
+ sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid;
+ _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL;
+ _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *volume_key = NULL;
+ bool dm_activated = false, mounted = false;
+ _cleanup_close_ int root_fd = -1;
+ size_t volume_key_size = 0;
+ uint64_t offset, size;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(setup->dm_name);
+ assert(setup->dm_node);
+
+ assert(user_record_storage(h) == USER_LUKS);
+
+ if (already_activated) {
+ struct loop_info64 info;
+ const char *n;
+
+ r = luks_open(setup->dm_name, h->password, &cd, &found_luks_uuid, &volume_key, &volume_key_size);
+ if (r < 0)
+ return r;
+
+ r = luks_validate_home_record(cd, h, volume_key, &luks_home);
+ if (r < 0)
+ return r;
+
+ n = crypt_get_device_name(cd);
+ if (!n)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name);
+
+ r = loop_device_open(n, O_RDWR, &loop);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open loopback device %s: %m", n);
+
+ if (ioctl(loop->fd, LOOP_GET_STATUS64, &info) < 0) {
+ _cleanup_free_ char *sysfs = NULL;
+ struct stat st;
+
+ if (!IN_SET(errno, ENOTTY, EINVAL))
+ return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n);
+
+ if (ioctl(loop->fd, BLKGETSIZE64, &size) < 0)
+ return log_error_errno(r, "Failed to read block device size of %s: %m", n);
+
+ if (fstat(loop->fd, &st) < 0)
+ return log_error_errno(r, "Failed to stat block device %s: %m", n);
+ assert(S_ISBLK(st.st_mode));
+
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return log_oom();
+
+ if (access(sysfs, F_OK) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", sysfs);
+
+ offset = 0;
+ } else {
+ _cleanup_free_ char *startfn = NULL, *buffer = NULL;
+
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return log_oom();
+
+ r = read_one_line_file(sysfs, &buffer);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read partition start offset: %m");
+
+ r = safe_atou64(buffer, &offset);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse partition start offset: %m");
+
+ if (offset > UINT64_MAX / 512U)
+ return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Offset too large for 64 byte range, refusing.");
+
+ offset *= 512U;
+ }
+ } else {
+ offset = info.lo_offset;
+ size = info.lo_sizelimit;
+ }
+
+ found_partition_uuid = found_fs_uuid = SD_ID128_NULL;
+
+ log_info("Discovered used loopback device %s.", loop->node);
+
+ root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0) {
+ r = log_error_errno(r, "Failed to open home directory: %m");
+ goto fail;
+ }
+ } else {
+ _cleanup_free_ char *fstype = NULL, *subdir = NULL;
+ _cleanup_close_ int fd = -1;
+ const char *ip;
+ struct stat st;
+
+ ip = force_image_path ?: user_record_image_path(h);
+
+ subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h));
+ if (!subdir)
+ return log_oom();
+
+ fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open image file %s: %m", ip);
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to fstat() image file: %m");
+ if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode))
+ return log_error_errno(errno, "Image file %s is not a regular file or block device: %m", ip);
+
+ r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate disk label: %m");
+
+ if (!user_record_luks_discard(h)) {
+ r = run_fallocate(fd, &st);
+ if (r < 0)
+ return r;
+ }
+
+ r = loop_device_make_full(fd, O_RDWR, offset, size, 0, &loop);
+ if (r == -ENOENT) {
+ log_error_errno(r, "Loopback block device support is not available on this system.");
+ return -ENOLINK; /* make recognizable */
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate loopback context: %m");
+
+ log_info("Setting up loopback device %s completed.", loop->node ?: ip);
+
+ r = luks_setup(loop->node ?: ip,
+ setup->dm_name,
+ h->luks_uuid,
+ h->luks_cipher,
+ h->luks_cipher_mode,
+ h->luks_volume_key_size,
+ h->password,
+ user_record_luks_discard(h),
+ &cd,
+ &found_luks_uuid,
+ &volume_key,
+ &volume_key_size);
+ if (r < 0)
+ return r;
+
+ dm_activated = true;
+
+ r = luks_validate_home_record(cd, h, volume_key, &luks_home);
+ if (r < 0)
+ goto fail;
+
+ r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid);
+ if (r < 0)
+ goto fail;
+
+ r = run_fsck(setup->dm_node, fstype);
+ if (r < 0)
+ goto fail;
+
+ r = unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h));
+ if (r < 0)
+ goto fail;
+
+ mounted = true;
+
+ root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0) {
+ r = log_error_errno(r, "Failed to open home directory: %m");
+ goto fail;
+ }
+
+ if (user_record_luks_discard(h))
+ (void) run_fitrim(root_fd);
+ }
+
+ setup->loop = TAKE_PTR(loop);
+ setup->crypt_device = TAKE_PTR(cd);
+ setup->root_fd = TAKE_FD(root_fd);
+ setup->found_partition_uuid = found_partition_uuid;
+ setup->found_luks_uuid = found_luks_uuid;
+ setup->found_fs_uuid = found_fs_uuid;
+ setup->partition_offset = offset;
+ setup->partition_size = size;
+ setup->volume_key = TAKE_PTR(volume_key);
+ setup->volume_key_size = volume_key_size;
+
+ setup->undo_mount = mounted;
+ setup->undo_dm = dm_activated;
+
+ if (ret_luks_home)
+ *ret_luks_home = TAKE_PTR(luks_home);
+
+ return 0;
+
+fail:
+ if (mounted)
+ (void) umount_verbose("/run/systemd/user-home-mount");
+
+ if (dm_activated)
+ (void) crypt_deactivate(cd, setup->dm_name);
+
+ return r;
+}
+
+static int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup) {
+ assert(h);
+ assert(setup);
+
+ setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+
+ return 0;
+}
+
+static int home_upload_fscrypt_key(
+ UserRecord *h,
+ const char *password,
+ const void *salt,
+ size_t salt_size,
+ const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
+ uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) {
+
+ uint8_t master[512 / 8] = {}, hashed[512 / 8] = {}, hashed2[512 / 8] = {};
+ _cleanup_free_ char *hex = NULL;
+ const char *description;
+ struct fscrypt_key key;
+ key_serial_t serial;
+ int r;
+
+ assert(h);
+
+ /* Derive a master key from the password using PBKDF2-SHA512-HMAC */
+ if (PKCS5_PBKDF2_HMAC(password, strlen(password),
+ salt, salt_size,
+ 0xFFFF, EVP_sha512(), sizeof(master), master) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+ goto finish;
+ }
+
+ /* Derive the key descriptor from the master key, also in order to be compatible with e4crypt */
+ assert_se(SHA512(master, sizeof(master), hashed) == hashed);
+ assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2);
+
+ if (match_key_descriptor) {
+ assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2));
+ if (memcmp(match_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE) != 0)
+ return -ENOANO; /* don't log here */
+ }
+
+ hex = hexmem(hashed2, 8);
+ if (!hex) {
+ r = log_oom();
+ goto finish;
+ }
+
+ description = strjoina("fscrypt:", hex);
+
+ key = (struct fscrypt_key) {
+ .mode = 0,
+ .size = sizeof(master),
+ };
+
+ assert_cc(sizeof(master) <= sizeof(key.raw));
+ memcpy(key.raw, master, sizeof(master));
+
+ /* Upload to the kernel */
+ serial = add_key("logon", description, &key, sizeof(key), KEY_SPEC_THREAD_KEYRING);
+ if (serial < 0) {
+ r = log_error_errno(errno, "Failed to install master key in keyring: %m");
+ goto finish;
+ }
+
+ log_info("Configured encryption key.");
+
+ if (ret_key_descriptor) {
+ assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2));
+ memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE);
+ }
+
+ r = 0;
+
+finish:
+ explicit_bzero_safe(master, sizeof(master));
+ explicit_bzero_safe(&key, sizeof(key));
+ return r;
+}
+
+static int home_prepare_fscrypt(UserRecord *h, bool already_activated, HomeSetup *setup) {
+ _cleanup_free_ char *salt_text = NULL;
+ struct fscrypt_policy policy;
+ bool uploaded = false;
+ const char *ip;
+ const void *salt = NULL;
+ _cleanup_free_ void *salt_buffer = NULL;
+ size_t salt_size = 0;
+ char **i;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(user_record_storage(h) == USER_FSCRYPT);
+
+ assert_se(ip = user_record_image_path(h));
+
+ setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+
+ r = fgetxattr_malloc(setup->root_fd, "trusted.fscrypt_salt", &salt_text);
+ if (r == -ENODATA) {
+ salt = h->fscrypt_salt;
+ salt_size = h->fscrypt_salt_size;
+ } else if (r < 0)
+ return log_error_errno(r, "Faile read fscrypt salt extended attribute of %s: %m", ip);
+ else if (r >= 0) {
+ r = unbase64mem_full(salt_text, r, true, &salt_buffer, &salt_size);
+ if (r < 0)
+ return log_error_errno(r, "fscrypt salt extended attribute on %s is not valid: %m", ip);
+
+ if (h->fscrypt_salt_size > 0 &&
+ memcmp_nn(h->fscrypt_salt, h->fscrypt_salt_size, salt_buffer, salt_size) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "fscrypt salt doesn't match record.");
+
+ free_and_replace(setup->found_fscrypt_salt, salt_buffer);
+ setup->found_fscrypt_salt_size = salt_size;
+ salt = setup->found_fscrypt_salt;
+ }
+
+ if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+ if (errno == ENODATA)
+ return log_error_errno(errno, "Home directory %s is not encrypted.", ip);
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_error_errno(errno, "File system does not support fscrypt: %m");
+ return -ENOLINK; /* make recognizable */
+ }
+ return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip);
+ }
+
+ STRV_FOREACH(i, h->password) {
+ r = home_upload_fscrypt_key(h, *i, salt, salt_size, policy.master_key_descriptor, NULL);
+ if (r >= 0) {
+ uploaded = true; /* This is the key we are looking for and we uploaded it into the
+ * kernel */
+ break;
+ }
+ if (r != -ENOANO)
+ return r;
+
+ /* Try another password if we got ENOANO, i.e. when the password we supplied doesn't
+ * match the policy of the home directory. */
+ }
+
+ if (!uploaded)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords.");
+
+ return 0;
+}
+
+static int home_prepare_cifs(
+ UserRecord *h,
+ bool already_activated,
+ HomeSetup *setup) {
+
+ char **pw;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(user_record_storage(h) == USER_CIFS);
+
+ if (already_activated)
+ setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ else {
+ bool mounted = false;
+
+ r = unshare_and_mount(NULL, NULL, false);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(pw, h->password) {
+ _cleanup_(unlink_and_freep) char *p = NULL;
+ _cleanup_free_ char *options = NULL;
+ _cleanup_(fclosep) FILE *f = NULL;
+ pid_t mount_pid;
+ int exit_status;
+
+ r = fopen_temporary(NULL, &f, &p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create temporary credentials file: %m");
+
+ fprintf(f,
+ "username=%s\n"
+ "password=%s\n",
+ user_record_cifs_user_name(h),
+ *pw);
+
+ if (h->cifs_domain)
+ fprintf(f, "domain=%s\n", h->cifs_domain);
+
+ r = fflush_and_check(f);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write temporary credentials file: %m");
+
+ f = safe_fclose(f);
+
+ if (asprintf(&options, "credentials=%s,uid=" UID_FMT ",forceuid,gid=" UID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o",
+ p, h->uid, h->uid, h->access_mode, h->access_mode) < 0)
+ return log_oom();
+
+ r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &mount_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execl("/bin/mount", "/bin/mount", "-n", "-t", "cifs",
+ h->cifs_service, "/run/systemd/user-home-mount",
+ "-o", options, NULL);
+
+ log_error_errno(errno, "Failed to execute fsck: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ exit_status = wait_for_terminate_and_check("mount", mount_pid, WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS);
+ if (exit_status < 0)
+ return exit_status;
+ if (exit_status != EXIT_SUCCESS)
+ return -EPROTO;
+
+ mounted = true;
+ break;
+ }
+
+ if (!mounted)
+ return log_error_errno(ENOKEY, "Failed to mount home directory with supplied password.");
+
+ setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ }
+ if (setup->root_fd < 0)
+ return log_error_errno(r, "Failed to open home directory: %m");
+
+ return 0;
+}
+
+static int home_prepare(
+ UserRecord *h,
+ bool already_activated,
+ HomeSetup *setup,
+ UserRecord **ret_header_home) {
+
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(!setup->loop);
+ assert(!setup->crypt_device);
+ assert(setup->root_fd < 0);
+ assert(!setup->undo_dm);
+ assert(!setup->undo_mount);
+
+ /* Makes a home directory accessible (through the root_fd file descriptor, not by path!). */
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ return home_prepare_luks(h, already_activated, NULL, setup, ret_header_home);
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ r = home_prepare_directory(h, already_activated, setup);
+ break;
+
+ case USER_FSCRYPT:
+ r = home_prepare_fscrypt(h, already_activated, setup);
+ break;
+
+ case USER_CIFS:
+ r = home_prepare_cifs(h, already_activated, setup);
+ break;
+
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+ }
+
+ if (r < 0)
+ return r;
+
+ if (ret_header_home)
+ *ret_header_home = NULL;
+
+ return r;
+}
+
+static int sync_and_statfs(int root_fd, struct statfs *ret) {
+ assert(root_fd >= 0);
+
+ /* Let's sync this to disk, so that the disk space reported by fstatfs() below is accurate (for file
+ * systems such as btrfs where this is determined lazily). */
+
+ if (syncfs(root_fd) < 0)
+ return log_error_errno(errno, "Failed to synchronize file system: %m");
+
+ if (ret)
+ if (fstatfs(root_fd, ret) < 0)
+ return log_error_errno(errno, "Failed to statfs() file system: %m");
+
+ log_info("Synchronized disk.");
+
+ return 0;
+}
+
+static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, struct statfs *sfs) {
+ char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], buffer4[FORMAT_BYTES_MAX];
+
+ assert(sfs);
+
+ log_info("Image size is %s, file system size is %s, file system payload size is %s, file system free is %s.",
+ format_bytes(buffer1, sizeof(buffer1), host_size),
+ format_bytes(buffer2, sizeof(buffer2), encrypted_size),
+ format_bytes(buffer3, sizeof(buffer3), (uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize),
+ format_bytes(buffer4, sizeof(buffer4), (uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize));
+}
+
+static int read_identity_file(int root_fd, JsonVariant **ret) {
+ _cleanup_(fclosep) FILE *identity_file = NULL;
+ _cleanup_close_ int identity_fd = -1;
+ unsigned line, column;
+ int r;
+
+ assert(root_fd >= 0);
+ assert(ret);
+
+ identity_fd = openat(root_fd, ".identity", O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK);
+ if (identity_fd < 0)
+ return log_error_errno(errno, "Failed to open .identity file in home directory: %m");
+
+ r = fd_verify_regular(identity_fd);
+ if (r < 0)
+ return log_error_errno(r, "Embedded identity file is not a regular file, refusing: %m");
+
+ identity_file = fdopen(identity_fd, "r");
+ if (!identity_file)
+ return log_oom();
+
+ identity_fd = -1;
+
+ r = json_parse_file(identity_file, ".identity", JSON_PARSE_SENSITIVE, ret, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "[.identity:%u:%u] Failed to parse JSON data: %m", line, column);
+
+ log_info("Read embedded .identity file.");
+
+ return 0;
+}
+
+static int write_identity_file(int root_fd, JsonVariant *v, uid_t uid) {
+ _cleanup_(json_variant_unrefp) JsonVariant *normalized = NULL;
+ _cleanup_(fclosep) FILE *identity_file = NULL;
+ _cleanup_close_ int identity_fd = -1;
+ _cleanup_free_ char *fn = NULL;
+ int r;
+
+ assert(root_fd >= 0);
+ assert(v);
+
+ normalized = json_variant_ref(v);
+
+ r = json_variant_normalize(&normalized);
+ if (r < 0)
+ log_warning_errno(r, "Failed to normalize user record, ignoring: %m");
+
+ r = tempfn_random(".identity", NULL, &fn);
+ if (r < 0)
+ return r;
+
+ identity_fd = openat(root_fd, fn, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600);
+ if (identity_fd < 0)
+ return log_error_errno(errno, "Failed to create .identity file in home directory: %m");
+
+ identity_file = fdopen(identity_fd, "w");
+ if (!identity_file) {
+ r = log_oom();
+ goto fail;
+ }
+
+ identity_fd = -1;
+
+ json_variant_dump(normalized, JSON_FORMAT_PRETTY, identity_file, NULL);
+
+ r = fflush_and_check(identity_file);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write .identity file: %m");
+ goto fail;
+ }
+
+ if (fchown(fileno(identity_file), uid, uid) < 0) {
+ log_error_errno(r, "Failed to change ownership of identity file: %m");
+ goto fail;
+ }
+
+ if (renameat(root_fd, fn, root_fd, ".identity") < 0) {
+ r = log_error_errno(errno, "Failed to move identity file into place: %m");
+ goto fail;
+ }
+
+ log_info("Wrote embedded .identity file.");
+
+ return 0;
+
+fail:
+ (void) unlinkat(root_fd, fn, 0);
+ return r;
+}
+
+static int load_embedded_identity(
+ UserRecord *h,
+ int root_fd,
+ UserRecord *header_home,
+ UserReconcileMode mode,
+ UserRecord **ret_embedded_home,
+ UserRecord **ret_new_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *intermediate_home = NULL, *new_home = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ assert(h);
+ assert(root_fd >= 0);
+
+ r = read_identity_file(root_fd, &v);
+ if (r < 0)
+ return r;
+
+ embedded_home = user_record_new();
+ if (!embedded_home)
+ return log_oom();
+
+ r = user_record_load(embedded_home, v, USER_RECORD_LOAD_EMBEDDED);
+ if (r < 0)
+ return r;
+
+ /* Insist that credentials the user supplies alos unlocks any embedded records. */
+ r = user_record_test_secret(embedded_home, h);
+ if (r == -ENXIO)
+ return log_error_errno(r, "Embedded home user record contains no hashed password, invalid.");
+ if (r == -ENOKEY)
+ return log_error_errno(r, "Supplied password does not match password of embedded home user record.");
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate password of embedded home user record: %m");
+
+ /* At this point we have three records to deal with:
+ *
+ * · The record we got passed from the host
+ * · The record included in the LUKS header (only if LUKS is used)
+ * · The record in the home directory itself (~.identity)
+ *
+ * Now we have to reconcile all three, and let the newest one win. */
+
+ if (header_home) {
+ /* Note we relax the requirements here. Instead of insisting that the host record is strictly
+ * newer, let's also be OK if its equally new. If it is, we'll however insist that the
+ * embedded record must be newer, so that we update at least one of the two. */
+
+ r = user_record_reconcile(h, header_home, mode == USER_RECONCILE_REQUIRE_NEWER ? USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL : mode, &intermediate_home);
+ if (r == -EREMCHG) /* this was supposed to be checked earlier already, but let's check this again */
+ return log_error_errno(r, "Identity stored on host and in header don't match, refusing.");
+ if (r == -ESTALE)
+ return log_error_errno(r, "Embedded identity record is newer than supplied record, refusing.");
+ if (r < 0)
+ return log_error_errno(r, "Failed to reconcile host and header identities: %m");
+ if (r == USER_RECONCILE_EMBEDDED_WON)
+ log_info("Reconciling header user identity completed (header version was newer).");
+ else if (r == USER_RECONCILE_HOST_WON) {
+ log_info("Reconciling header user identity completed (host version was newer).");
+
+ if (mode == USER_RECONCILE_REQUIRE_NEWER) /* Host version is newer than the header
+ * version, hence we'll update
+ * something. This means we can relax the
+ * requirements on the embedded
+ * identity. */
+ mode = USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL;
+ } else {
+ assert(r == USER_RECONCILE_IDENTICAL);
+ log_info("Reconciling user identities completed (host and header version were identical).");
+ }
+
+ h = intermediate_home;
+ }
+
+ r = user_record_reconcile(h, embedded_home, mode, &new_home);
+ if (r == -EREMCHG)
+ return log_error_errno(r, "Identity stored on host and in home don't match, refusing.");
+ if (r == -ESTALE)
+ return log_error_errno(r, "Embedded identity record is equally new or newer than supplied record, refusing.");
+ if (r < 0)
+ return log_error_errno(r, "Failed to reconcile host and embedded identities: %m");
+ if (r == USER_RECONCILE_EMBEDDED_WON)
+ log_info("Reconciling embedded user identity completed (embedded version was newer).");
+ else if (r == USER_RECONCILE_HOST_WON)
+ log_info("Reconciling embedded user identity completed (host version was newer).");
+ else {
+ assert(r == USER_RECONCILE_IDENTICAL);
+ log_info("Reconciling embedded user identity completed (host and embedded version were identical).");
+ }
+
+ if (ret_embedded_home)
+ *ret_embedded_home = TAKE_PTR(embedded_home);
+
+ if (ret_new_home)
+ *ret_new_home = TAKE_PTR(new_home);
+
+ return 0;
+}
+
+static int store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) {
+ _cleanup_(user_record_unrefp) UserRecord *embedded = NULL;
+ int r;
+
+ assert(h);
+ assert(root_fd >= 0);
+ assert(uid_is_valid(uid));
+
+ r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &embedded);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine new embedded record: %m");
+
+ if (old_home && user_record_equal(old_home, embedded)) {
+ log_debug("Not updating embedded home record.");
+ return 0;
+ }
+
+ /* The identity has changed, let's update it in the image */
+ r = write_identity_file(root_fd, embedded->json, h->uid);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+static uint64_t luks_volume_key_size_convert(struct crypt_device *cd) {
+ int k;
+
+ assert(cd);
+
+ k = crypt_get_volume_key_size(cd);
+ if (k <= 0)
+ return UINT64_MAX;
+
+ return (uint64_t) k;
+}
+
+static const char *file_system_type_fd(int fd) {
+ struct statfs sfs;
+
+ assert(fd >= 0);
+
+ if (fstatfs(fd, &sfs) < 0) {
+ log_debug_errno(errno, "Failed to statfs(): %m");
+ return NULL;
+ }
+
+ if (is_fs_type(&sfs, XFS_SB_MAGIC))
+ return "xfs";
+ if (is_fs_type(&sfs, EXT4_SUPER_MAGIC))
+ return "ext4";
+ if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC))
+ return "btrfs";
+
+ return 0;
+}
+
+static int extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup) {
+ int r;
+
+ assert(h);
+ assert(used);
+ assert(setup);
+
+ r = user_record_add_binding(
+ h,
+ user_record_storage(used),
+ user_record_image_path(used),
+ setup->found_partition_uuid,
+ setup->found_luks_uuid,
+ setup->found_fs_uuid,
+ setup->crypt_device ? crypt_get_cipher(setup->crypt_device) : NULL,
+ setup->crypt_device ? crypt_get_cipher_mode(setup->crypt_device) : NULL,
+ setup->crypt_device ? luks_volume_key_size_convert(setup->crypt_device) : UINT64_MAX,
+ file_system_type_fd(setup->root_fd),
+ setup->found_fscrypt_salt,
+ setup->found_fscrypt_salt_size,
+ user_record_home_directory(used),
+ used->uid,
+ (gid_t) used->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update binding in record: %m");
+
+ return 0;
+}
+
+static int chown_recursive_directory(int root_fd, uid_t uid) {
+ int r;
+
+ assert(root_fd >= 0);
+ assert(uid_is_valid(uid));
+
+ r = fd_chown_recursive(root_fd, uid, (gid_t) uid, 0777);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change ownership of files and directories: %m");
+ if (r == 0)
+ log_info("Recursive changing of ownership not necessary, skipped.");
+ else
+ log_info("Recursive changing of ownership completed.");
+
+ return 0;
+}
+
+static int move_mount(const char *user_name_and_realm, const char *target) {
+ _cleanup_free_ char *subdir = NULL;
+ const char *d;
+ int r;
+
+ assert(user_name_and_realm);
+ assert(target);
+
+ if (user_name_and_realm) {
+ subdir = path_join("/run/systemd/user-home-mount/", user_name_and_realm);
+ if (!subdir)
+ return log_oom();
+
+ d = subdir;
+ } else
+ d = "/run/systemd/user-home-mount/";
+
+ (void) mkdir_p(target, 0700);
+
+ r = mount_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = umount_verbose("/run/systemd/user-home-mount");
+ if (r < 0)
+ return r;
+
+ log_info("Moving to final mount point %s completed.", target);
+ return 0;
+}
+
+static int home_freshen(
+ UserRecord *h,
+ HomeSetup *setup,
+ UserRecord *header_home,
+ struct statfs *ret_statfs,
+ UserRecord **ret_new_home) {
+
+ _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(ret_new_home);
+
+ /* 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 = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home);
+ if (r < 0)
+ return r;
+
+ r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = chown_recursive_directory(setup->root_fd, h->uid);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(setup->root_fd, ret_statfs);
+ if (r < 0)
+ return r;
+
+ *ret_new_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_activate_luks(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL;
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_free_ char *fstype = NULL;
+ uint64_t host_size, encrypted_size;
+ const char *hdo, *hd;
+ struct statfs sfs;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_LUKS);
+ assert(ret_home);
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */
+
+ r = make_dm_names(h->user_name, &setup.dm_name, &setup.dm_node);
+ if (r < 0)
+ return r;
+
+ r = access(setup.dm_node, F_OK);
+ if (r < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup.dm_node);
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup.dm_node);
+
+ r = home_prepare_luks(h, false, NULL, &setup, &luks_home_record);
+ if (r < 0)
+ return r;
+
+ r = block_get_size_by_fd(setup.loop->fd, &host_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get loopback block device size: %m");
+
+ r = block_get_size_by_path(setup.dm_node, &encrypted_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get LUKS block device size: %m");
+
+ r = home_freshen(h, &setup, luks_home_record, &sfs, &new_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, &setup);
+ if (r < 0)
+ return r;
+
+ setup.root_fd = safe_close(setup.root_fd);
+
+ r = move_mount(user_record_user_name_and_realm(h), hd);
+ if (r < 0)
+ return r;
+
+ setup.undo_mount = false;
+
+ loop_device_relinquish(setup.loop);
+
+ r = dm_deferred_remove(setup.dm_name);
+ if (r < 0)
+ log_warning_errno(r, "Failed to relinquish dm device, ignoring: %m");
+
+ setup.undo_dm = false;
+
+ log_info("Everything completed.");
+
+ print_size_summary(host_size, encrypted_size, &sfs);
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int home_activate_directory(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL;
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ const char *hdo, *hd, *ipo, *ip;
+ int r;
+
+ assert(h);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT));
+ assert(ret_home);
+
+ assert_se(ipo = user_record_image_path(h));
+ ip = strdupa(ipo); /* copy out, since reconciliation might cause changing of the field */
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa(hdo);
+
+ r = home_prepare(h, false, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = home_freshen(h, &setup, header_home, NULL, &new_home);
+ if (r < 0)
+ return r;
+
+ setup.root_fd = safe_close(setup.root_fd);
+
+ /* Create mount point to mount over if necessary */
+ if (!path_equal(ip, hd))
+ (void) mkdir_p(hd, 0700);
+
+ /* Create a mount point (even if the directory is already placed correctly), as a way to indicate
+ * this mount point is now "activated". Moreover, we want to set per-user
+ * MS_NOSUID/MS_NOEXEC/MS_NODEV. */
+ r = mount_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = mount_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL);
+ if (r < 0) {
+ (void) umount_verbose(hd);
+ return r;
+ }
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_activate_cifs(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_free_ char *fstype = NULL;
+ const char *hdo, *hd;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_CIFS);
+ assert(ret_home);
+
+ if (!h->cifs_service)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing.");
+
+ assert_se(hdo = user_record_home_directory(h));
+ hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */
+
+ r = home_prepare_cifs(h, false, &setup);
+ if (r < 0)
+ return r;
+
+ r = home_freshen(h, &setup, NULL, NULL, &new_home);
+ if (r < 0)
+ return r;
+
+ setup.root_fd = safe_close(setup.root_fd);
+
+ r = move_mount(NULL, hd);
+ if (r < 0)
+ return r;
+
+ setup.undo_mount = false;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int user_record_validate_hashed_password(UserRecord *h) {
+ int r;
+
+ assert(h);
+
+ if (strv_isempty(h->password))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No password provided.");
+
+ r = user_record_test_secret(h, h);
+ if (r == -ENXIO) {
+ log_info("Supplied user record contains no hashed passwords, not validating password against it.");
+ return 0;
+ }
+ if (r == -ENOKEY)
+ return log_error_errno(r, "Passwords not correct or not sufficient to unlock record.");
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate password of record: %m");
+
+ log_info("Provided password unlocks user record.");
+
+ return 0;
+}
+
+static int home_activate(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+ if (!uid_is_valid(h->uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+ if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r == USER_TEST_MOUNTED)
+ return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "Home directory %s is already mounted, refusing.", user_record_home_directory(h));
+
+ r = user_record_test_image_path_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r == USER_TEST_ABSENT)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s is missing, refusing.", user_record_image_path(h));
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ r = home_activate_luks(h, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ case USER_FSCRYPT:
+ r = home_activate_directory(h, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case USER_CIFS:
+ r = home_activate_cifs(h, &new_home);
+ if (r < 0)
+ return r;
+
+ break;
+
+ default:
+ assert_not_reached("unexpected type");
+ }
+
+ /* Note that the returned object might either be a reference to an updated version of the existing
+ * home object, or a reference to a newly allocated home object. The caller has to be able to deal
+ * with both, and consider the old object out-of-date. */
+ if (user_record_equal(h, new_home)) {
+ *ret_home = NULL;
+ return 0; /* no identity change */
+ }
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1; /* identity updated */
+}
+
+static int home_deactivate(UserRecord *h, bool force) {
+ bool done = false;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+ if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Deactivating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r == USER_TEST_MOUNTED) {
+ if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0)
+ return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h));
+
+ log_info("Unmounting completed.");
+ done = true;
+ } else
+ log_info("Directory %s is already unmounted.", user_record_home_directory(h));
+
+ if (user_record_storage(h) == USER_LUKS) {
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+
+ /* Note that the DM device and loopback device are set to auto-detach, hence strictly
+ * speaking we don't have to explicitly have to detach them. However, we do that nonetheless
+ * (in case of the DM device), to avoid races: by explicitly detaching them we know when the
+ * detaching is complete. We don't bother about the loopback device because unlike the DM
+ * device it doesn't have a fixed name. */
+
+ r = make_dm_names(h->user_name, &dm_name, &dm_node);
+ if (r < 0)
+ return r;
+
+ r = crypt_init_by_name(&cd, dm_name);
+ if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT))
+ log_debug_errno(r, "LUKS device %s is already detached.", dm_name);
+ else if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+ else {
+ log_info("Discovered used LUKS device %s.", dm_node);
+
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ r = crypt_deactivate(cd, dm_name);
+ if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT))
+ log_debug_errno(r, "LUKS device %s is already detached.", dm_node);
+ else if (r < 0)
+ return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node);
+
+ log_info("LUKS device detaching completed.");
+ done = true;
+ }
+ }
+
+ if (!done)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active.");
+
+ log_info("Everything completed.");
+ return 0;
+}
+
+static int run_mkfs(
+ const char *node,
+ const char *fstype,
+ const char *label,
+ sd_id128_t uuid,
+ bool discard) {
+
+ int r;
+
+ assert(node);
+ assert(fstype);
+ assert(label);
+
+ r = mkfs_exists(fstype);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if mkfs for file system %s exists: %m", fstype);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Nt mkfs for file system %s installed.", fstype);
+
+ r = safe_fork("(mkfs)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ const char *mkfs;
+ char suuid[37];
+
+ /* Child */
+
+ mkfs = strjoina("mkfs.", fstype);
+ id128_to_uuid_string(uuid, suuid);
+
+ if (streq(fstype, "ext4"))
+ execlp(mkfs, mkfs,
+ "-L", label,
+ "-U", suuid,
+ "-I", "256",
+ "-O", "has_journal",
+ "-m", "0",
+ "-E", discard ? "lazy_itable_init=1,discard" : "lazy_itable_init=1,nodiscard",
+ node, NULL);
+ else if (streq(fstype, "btrfs")) {
+ if (discard)
+ execlp(mkfs, mkfs, "-L", label, "-U", suuid, node, NULL);
+ else
+ execlp(mkfs, mkfs, "-L", label, "-U", suuid, "--nodiscard", node, NULL);
+ } else if (streq(fstype, "xfs")) {
+ const char *j;
+
+ j = strjoina("uuid=", suuid);
+ if (discard)
+ execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", node, NULL);
+ else
+ execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", "-K", node, NULL);
+ } else {
+ log_error("Cannot make file system: %s", fstype);
+ _exit(EXIT_FAILURE);
+ }
+
+ log_error_errno(errno, "Failed to execute %s: %m", mkfs);
+ _exit(EXIT_FAILURE);
+ }
+
+ return 0;
+}
+
+static int luks_format(
+ const char *node,
+ const char *dm_name,
+ sd_id128_t uuid,
+ const char *label,
+ char **passwords,
+ char **hashed_passwords,
+ bool discard,
+ UserRecord *hr,
+ struct crypt_device **ret) {
+
+ _cleanup_(user_record_unrefp) UserRecord *reduced = NULL;
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_(erase_and_freep) void *volume_key = NULL;
+ _cleanup_free_ char *text = NULL;
+ size_t volume_key_size;
+ char suuid[37], **pp;
+ bool added = false;
+ int slot = 0, r;
+
+ assert(node);
+ assert(dm_name);
+ assert(hr);
+ assert(ret);
+
+ r = crypt_init(&cd, node);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate libcryptsetup context: %m");
+
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ /* Normally we'd, just leave volume key generation to libcryptsetup. However, we can't, since we
+ * can't extract the volume key from the library again, but we need it in order to encrypt the JSON
+ * record. Hence, let's generate it on our own, so that we can keep track of it. */
+
+ volume_key_size = user_record_luks_volume_key_size(hr);
+ volume_key = malloc(volume_key_size);
+ if (!volume_key)
+ return log_oom();
+
+ r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate volume key: %m");
+
+ /* Increase the metadata space to 4M, the largest LUKS2 supports */
+ r = crypt_set_metadata_size(cd, 4096U*1024U, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change LUKS2 metadata size: %m");
+
+ r = crypt_format(cd,
+ CRYPT_LUKS2,
+ user_record_luks_cipher(hr),
+ user_record_luks_cipher_mode(hr),
+ id128_to_uuid_string(uuid, suuid),
+ volume_key,
+ volume_key_size,
+ &(struct crypt_params_luks2) {
+ .label = label,
+ .subsystem = "systemd-home",
+ .sector_size = 512U,
+ .pbkdf = &(struct crypt_pbkdf_type) {
+ .hash = user_record_luks_pbkdf_hash_algorithm(hr),
+ .type = user_record_luks_pbkdf_type(hr),
+ .time_ms = user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC,
+ .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024,
+ .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr),
+ },
+ });
+ if (r < 0)
+ return log_error_errno(r, "Failed to format LUKS image: %m");
+
+ log_info("LUKS formatting completed.");
+
+ STRV_FOREACH(pp, passwords) {
+ r = test_password(hashed_passwords, *pp);
+ if (r < 0)
+ return log_error_errno(r, "Failed to verify whether password is valid: %m");
+ if (r == 0) /* Ignore passwords that are not available as hashed too, it's not supposed to be
+ * used for disk encryption (might be for smartcard?) */
+ continue;
+
+ r = crypt_keyslot_add_by_volume_key(
+ cd,
+ slot,
+ volume_key,
+ volume_key_size,
+ *pp,
+ strlen(*pp));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up LUKS password for slot %i: %m", slot);
+
+ log_info("Writing password to LUKS keyslot %i completed.", slot);
+ added = true;
+ }
+
+ if (!added)
+ return log_error_errno(r, "No password configured on LUKS, refusing.");
+
+ r = crypt_activate_by_volume_key(
+ cd,
+ dm_name,
+ volume_key,
+ volume_key_size,
+ discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to activate LUKS superblock: %m");
+
+ log_info("LUKS activation by volume key succeeded.");
+
+ r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &reduced);
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare home record for LUKS: %m");
+
+ r = format_luks_token_text(cd, reduced, volume_key, &text);
+ if (r < 0)
+ return r;
+
+ r = crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set LUKS JSON token: %m");
+
+ log_info("Writing user record as LUKS token completed.");
+
+ if (ret)
+ *ret = TAKE_PTR(cd);
+
+ return 0;
+}
+
+static int copy_skel(int root_fd, const char *skel) {
+ int r;
+
+ assert(root_fd >= 0);
+
+ r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE);
+ if (r == -ENOENT) {
+ log_info("Skeleton directory %s missing, ignoring.", skel);
+ return 0;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to copy in %s: %m", skel);
+
+ log_info("Copying in %s completed.", skel);
+ return 0;
+}
+
+static int change_access_mode(int root_fd, mode_t m) {
+ assert(root_fd >= 0);
+
+ if (fchmod(root_fd, m) < 0)
+ return log_error_errno(errno, "Failed to change access mode of top-level directory: %m");
+
+ log_info("Changed top-level directory access mode to 0%o.", m);
+ return 0;
+}
+
+static int home_populate(UserRecord *h, int dir_fd) {
+ int r;
+
+ assert(h);
+ assert(dir_fd >= 0);
+
+ r = copy_skel(dir_fd, user_record_skeleton_directory(h));
+ if (r < 0)
+ return r;
+
+ r = store_embedded_identity(h, dir_fd, h->uid, NULL);
+ if (r < 0)
+ return r;
+
+ r = chown_recursive_directory(dir_fd, h->uid);
+ if (r < 0)
+ return r;
+
+ r = change_access_mode(dir_fd, user_record_access_mode(h));
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_context*, fdisk_unref_context);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_partition*, fdisk_unref_partition);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_parttype*, fdisk_unref_parttype);
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_table*, fdisk_unref_table);
+
+static int make_partition_table(
+ int fd,
+ const char *label,
+ sd_id128_t uuid,
+ uint64_t *ret_offset,
+ uint64_t *ret_size,
+ sd_id128_t *ret_disk_uuid) {
+
+ _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *p = NULL, *q = NULL;
+ _cleanup_free_ char *path = NULL, *joined = NULL, *disk_uuid_as_string = NULL;
+ _cleanup_(fdisk_unref_parttypep) struct fdisk_parttype *t = NULL;
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ uint64_t offset, size;
+ sd_id128_t disk_uuid;
+ char uuids[37];
+ int r;
+
+ assert(fd >= 0);
+ assert(label);
+ assert(ret_offset);
+ assert(ret_size);
+
+ t = fdisk_new_parttype();
+ if (!t)
+ return log_oom();
+
+ r = fdisk_parttype_set_typestr(t, "773f91ef-66d4-49b5-bd83-d683bf40ad16");
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize partition type: %m");
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+ return log_oom();
+
+ r = fdisk_assign_device(c, path, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device: %m");
+
+ r = fdisk_create_disklabel(c, "gpt");
+ if (r < 0)
+ return log_error_errno(r, "Failed to create gpt disk label: %m");
+
+ p = fdisk_new_partition();
+ if (!p)
+ return log_oom();
+
+ r = fdisk_partition_set_type(p, t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set partition type: %m");
+
+ r = fdisk_partition_start_follow_default(p, 1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to place partition at beginning of space: %m");
+
+ r = fdisk_partition_partno_follow_default(p, 1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to place partition at first free partition index: %m");
+
+ r = fdisk_partition_end_follow_default(p, 1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make partition cover all free space: %m");
+
+ r = fdisk_partition_set_name(p, label);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set partition name: %m");
+
+ r = fdisk_partition_set_uuid(p, id128_to_uuid_string(uuid, uuids));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set partition UUID: %m");
+
+ r = fdisk_add_partition(c, p, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add partition: %m");
+
+ r = fdisk_write_disklabel(c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write disk label: %m");
+
+ r = fdisk_get_disklabel_id(c, &disk_uuid_as_string);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine disk label UUID: %m");
+
+ r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse disk label UUID: %m");
+
+ r = fdisk_get_partition(c, 0, &q);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read created partition metadata: %m");
+
+ assert(fdisk_partition_has_start(q));
+ offset = fdisk_partition_get_start(q);
+ if (offset > UINT64_MAX / 512U)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition offset too large.");
+
+ assert(fdisk_partition_has_size(q));
+ size = fdisk_partition_get_size(q);
+ if (size > UINT64_MAX / 512U)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition size too large.");
+
+ *ret_offset = offset * 512U;
+ *ret_size = size * 512U;
+ *ret_disk_uuid = disk_uuid;
+
+ return 0;
+}
+
+static bool supported_fs_size(const char *fstype, uint64_t host_size) {
+ uint64_t m;
+
+ m = minimal_size_by_fs_name(fstype);
+ if (m == UINT64_MAX)
+ return false;
+
+ return host_size >= m;
+}
+
+static int wait_for_devlink(const char *path) {
+ _cleanup_close_ int inotify_fd = -1;
+ usec_t until;
+ int r;
+
+ /* let's wait for a device link to show up in /dev, with a time-out. This is good to do since we
+ * return a /dev/disk/by-uuid/… link to our callers and they likely want to access it right-away,
+ * hence let's wait until udev has caught up with our changes, and wait for the symlink to be
+ * created. */
+
+ until = usec_add(now(CLOCK_MONOTONIC), 45 * USEC_PER_SEC);
+
+ for (;;) {
+ _cleanup_free_ char *dn = NULL;
+ usec_t w;
+
+ if (laccess(path, F_OK) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", path);
+ } else
+ return 0; /* Found it */
+
+ if (inotify_fd < 0) {
+ /* We need to wait for the device symlink to show up, let's create an inotify watch for it */
+ inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
+ if (inotify_fd < 0)
+ return log_error_errno(errno, "Failed to allocate inotify fd: %m");
+ }
+
+ dn = dirname_malloc(path);
+ for (;;) {
+ if (!dn)
+ return log_oom();
+
+ log_info("Watching %s", dn);
+
+ if (inotify_add_watch(inotify_fd, dn, IN_CREATE|IN_MOVED_TO|IN_ONLYDIR|IN_DELETE_SELF|IN_MOVE_SELF) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to add watch on %s: %m", dn);
+ } else
+ break;
+
+ if (empty_or_root(dn))
+ break;
+
+ dn = dirname_malloc(dn);
+ }
+
+ w = now(CLOCK_MONOTONIC);
+ if (w >= until)
+ return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Device link %s still hasn't shown up, giving up.", path);
+
+ r = fd_wait_for_event(inotify_fd, POLLIN, usec_sub_unsigned(until, w));
+ if (r < 0)
+ return log_error_errno(r, "Failed to watch inotify: %m");
+
+ (void) flush_fd(inotify_fd);
+ }
+}
+
+static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *ret) {
+ char buf[FORMAT_BYTES_MAX];
+ struct statfs sfs;
+ uint64_t m;
+
+ assert(h);
+ assert(parent_dir);
+ assert(ret);
+
+ if (h->disk_size != UINT64_MAX) {
+ *ret = DISK_SIZE_ROUND_DOWN(h->disk_size);
+ return 0;
+ }
+
+ if (statfs(parent_dir, &sfs) < 0)
+ return log_error_errno(errno, "statfs() on %s failed: %m", parent_dir);
+
+ m = sfs.f_bsize * sfs.f_bavail;
+
+ if (h->disk_size_relative == UINT64_MAX) {
+
+ if (m > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT)
+ return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large.");
+
+ *ret = DISK_SIZE_ROUND_DOWN(m * USER_DISK_SIZE_DEFAULT_PERCENT / 100);
+
+ log_info("Sizing home to %u%% of available disk space, which is %s.",
+ USER_DISK_SIZE_DEFAULT_PERCENT,
+ format_bytes(buf, sizeof(buf), *ret));
+ } else {
+ *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) m * (double) h->disk_size_relative / (double) UINT32_MAX));
+
+ log_info("Sizing home to %" PRIu64 ".%01" PRIu64 "%% of available disk space, which is %s.",
+ (h->disk_size_relative * 100) / UINT32_MAX,
+ ((h->disk_size_relative * 1000) / UINT32_MAX) % 10,
+ format_bytes(buf, sizeof(buf), *ret));
+ }
+
+ if (*ret < USER_DISK_SIZE_MIN)
+ *ret = USER_DISK_SIZE_MIN;
+
+ return 0;
+}
+
+static int home_create_luks(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL, *subdir = NULL, *disk_uuid_path = NULL, *temporary_image_path = NULL;
+ uint64_t host_size, encrypted_size, partition_offset, partition_size;
+ bool image_created = false, dm_activated = false, mounted = false;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid;
+ _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_close_ int image_fd = -1, root_fd = -1;
+ const char *fstype, *ip;
+ struct statfs sfs;
+ int r;
+
+ assert(h);
+ assert(h->storage < 0 || h->storage == USER_LUKS);
+ assert(ret_home);
+
+ assert_se(ip = user_record_image_path(h));
+
+ fstype = user_record_file_system_type(h);
+ if (!supported_fstype(fstype))
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Unsupported file system type: %s", h->file_system_type);
+
+ if (sd_id128_is_null(h->partition_uuid)) {
+ r = sd_id128_randomize(&partition_uuid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire partition UUID: %m");
+ } else
+ partition_uuid = h->partition_uuid;
+
+ if (sd_id128_is_null(h->luks_uuid)) {
+ r = sd_id128_randomize(&luks_uuid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire LUKS UUID: %m");
+ } else
+ luks_uuid = h->luks_uuid;
+
+ if (sd_id128_is_null(h->file_system_uuid)) {
+ r = sd_id128_randomize(&fs_uuid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire file system UUID: %m");
+ } else
+ fs_uuid = h->file_system_uuid;
+
+ r = make_dm_names(h->user_name, &dm_name, &dm_node);
+ if (r < 0)
+ return r;
+
+ r = access(dm_node, F_OK);
+ if (r < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node);
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", dm_node);
+
+ if (path_startswith(ip, "/dev/")) {
+ _cleanup_free_ char *sysfs = NULL;
+ uint64_t block_device_size;
+ struct stat st;
+
+ /* Let's place the home directory on a real device, i.e. an USB stick or such */
+
+ image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (image_fd < 0)
+ return log_error_errno(errno, "Failed to open device %s: %m", ip);
+
+ if (fstat(image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat device %s: %m", ip);
+ if (!S_ISBLK(st.st_mode))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing.");
+
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return log_oom();
+ if (access(sysfs, F_OK) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to check whether %s exists: %m", sysfs);
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Operating on partitions is currently not supported, sorry. Please specify a top-level block device.");
+
+ if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */
+ return log_error_errno(errno, "Failed to lock block device %s: %m", ip);
+
+ if (ioctl(image_fd, BLKGETSIZE64, &block_device_size) < 0)
+ return log_error_errno(errno, "Failed to read block device size: %m");
+
+ if (h->disk_size == UINT64_MAX) {
+
+ /* If a relative disk size is requested, apply it relative to the block device size */
+ if (h->disk_size_relative < UINT32_MAX)
+ host_size = CLAMP(DISK_SIZE_ROUND_DOWN(block_device_size * h->disk_size_relative / UINT32_MAX),
+ USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX);
+ else
+ host_size = block_device_size; /* Otherwise, take the full device */
+
+ } else if (host_size > block_device_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Selected disk size larger than backing block device, refusing.");
+ else
+ host_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+
+ if (!supported_fs_size(fstype, host_size))
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type);
+
+ /* After creation we should reference this partition by its UUID instead of the block
+ * device. That's preferable since the user might have specified a device node such as
+ * /dev/sdb to us, which might look very different when replugged. */
+ if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0)
+ return log_oom();
+
+ if (user_record_luks_discard(h)) {
+ if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0)
+ log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to issue full-device BLKDISCARD on device, ignoring: %m");
+ else
+ log_info("Full device discard completed.");
+ }
+ } else {
+ _cleanup_free_ char *parent = NULL;
+
+ parent = dirname_malloc(ip);
+ if (!parent)
+ return log_oom();
+
+ r = mkdir_p(parent, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create parent directory %s: %m", parent);
+
+ r = calculate_disk_size(h, parent, &host_size);
+ if (r < 0)
+ return r;
+
+ if (!supported_fs_size(fstype, host_size))
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type);
+
+ r = tempfn_random(ip, "homework", &temporary_image_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip);
+
+ image_fd = open(temporary_image_path, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600);
+ if (image_fd < 0)
+ return log_error_errno(errno, "Failed to create home image %s: %m", temporary_image_path);
+
+ image_created = true;
+
+ r = chattr_fd(image_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", temporary_image_path);
+
+ if (user_record_luks_discard(h))
+ r = ftruncate(image_fd, host_size);
+ else
+ r = fallocate(image_fd, 0, 0, host_size);
+ if (r < 0) {
+ if (ERRNO_IS_DISK_SPACE(errno)) {
+ log_debug_errno(errno, "Not enough disk space to allocate home.");
+ r = -ENOSPC; /* make recognizable */
+ goto fail;
+ }
+
+ r = log_error_errno(errno, "Failed to truncate home image %s: %m", temporary_image_path);
+ goto fail;
+ }
+
+ log_info("Allocating image file completed.");
+ }
+
+ r = make_partition_table(
+ image_fd,
+ user_record_user_name_and_realm(h),
+ partition_uuid,
+ &partition_offset,
+ &partition_size,
+ &disk_uuid);
+ if (r < 0)
+ goto fail;
+
+ log_info("Writing of partition table completed.");
+
+ r = loop_device_make_full(image_fd, O_RDWR, partition_offset, partition_size, 0, &loop);
+ if (r < 0) {
+ if (r == -ENOENT) { /* this means /dev/loop-control doesn't exist, i.e. we are in a container
+ * or similar and loopback bock devices are not available, return a
+ * recognizable error in this case. */
+ log_error_errno(r, "Loopback block device support is not available on this system.");
+ r = -ENOLINK;
+ goto fail;
+ }
+
+ log_error_errno(r, "Failed to set up loopback device for %s: %m", temporary_image_path);
+ goto fail;
+ }
+
+ r = loop_device_flock(loop, LOCK_EX); /* make sure udev won't read before we are done */
+ if (r < 0) {
+ log_error_errno(r, "Failed to take lock on loop device: %m");
+ goto fail;
+ }
+
+ log_info("Setting up loopback device %s completed.", loop->node ?: ip);
+
+ r = luks_format(loop->node, dm_name, luks_uuid, user_record_user_name_and_realm(h), h->password, h->hashed_password, user_record_luks_discard(h), h, &cd);
+ if (r < 0)
+ goto fail;
+
+ dm_activated = true;
+
+ r = block_get_size_by_path(dm_node, &encrypted_size);
+ if (r < 0) {
+ log_error_errno(r, "Failed to get encrypted block device size: %m");
+ goto fail;
+ }
+
+ log_info("Setting up LUKS device %s completed.", dm_node);
+
+ r = run_mkfs(dm_node, fstype, user_record_user_name_and_realm(h), fs_uuid, user_record_luks_discard(h));
+ if (r < 0)
+ goto fail;
+
+ log_info("Formatting file system completed.");
+
+ r = unshare_and_mount(dm_node, fstype, user_record_luks_discard(h));
+ if (r < 0)
+ goto fail;
+
+ mounted = true;
+
+ subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h));
+ if (!subdir) {
+ r = log_oom();
+ goto fail;
+ }
+
+ if (mkdir(subdir, 0700) < 0) {
+ r = log_error_errno(errno, "Failed to create user directory in mounted image file: %m");
+ goto fail;
+ }
+
+ root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0) {
+ r = log_error_errno(errno, "Failed to open user directory in mounted image file: %m");
+ goto fail;
+ }
+
+ r = home_populate(h, root_fd);
+ if (r < 0)
+ goto fail;
+
+ r = sync_and_statfs(root_fd, &sfs);
+ if (r < 0)
+ goto fail;
+
+ r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG, &new_home);
+ if (r < 0) {
+ log_error_errno(r, "Failed to clone record: %m");
+ goto fail;
+ }
+
+ r = user_record_add_binding(
+ new_home,
+ USER_LUKS,
+ disk_uuid_path ?: ip,
+ partition_uuid,
+ luks_uuid,
+ fs_uuid,
+ crypt_get_cipher(cd),
+ crypt_get_cipher_mode(cd),
+ luks_volume_key_size_convert(cd),
+ fstype,
+ NULL, 0,
+ NULL,
+ h->uid,
+ (gid_t) h->uid);
+ if (r < 0) {
+ log_error_errno(r, "Failed to add binding to record: %m");
+ goto fail;
+ }
+
+ root_fd = safe_close(root_fd);
+
+ r = umount_verbose("/run/systemd/user-home-mount");
+ if (r < 0)
+ goto fail;
+
+ mounted = false;
+
+ r = crypt_deactivate(cd, dm_name);
+ if (r < 0) {
+ log_error_errno(r, "Failed to deactivate LUKS device: %m");
+ goto fail;
+ }
+
+ dm_activated = false;
+
+ loop = loop_device_unref(loop);
+
+ if (disk_uuid_path)
+ (void) ioctl(image_fd, BLKRRPART, 0);
+
+ /* Let's close the image fd now. If we are operating on a real block device this will release the BSD
+ * lock that ensures udev doesn't interfere with what we are doing */
+ image_fd = safe_close(image_fd);
+
+ if (temporary_image_path) {
+ if (rename(temporary_image_path, ip) < 0) {
+ log_error_errno(errno, "Failed to rename image file: %m");
+ goto fail;
+ }
+
+ log_info("Moved image file into place.");
+ }
+
+ if (disk_uuid_path)
+ (void) wait_for_devlink(disk_uuid_path);
+
+ log_info("Everything completed.");
+
+ print_size_summary(host_size, encrypted_size, &sfs);
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+
+fail:
+ /* Let's close all files before we unmount the file system, to avoid EBUSY */
+ root_fd = safe_close(root_fd);
+
+ if (mounted)
+ (void) umount_verbose("/run/systemd/user-home-mount");
+
+ if (dm_activated)
+ (void) crypt_deactivate(cd, dm_name);
+
+ loop = loop_device_unref(loop);
+
+ if (image_created)
+ (void) unlink(temporary_image_path);
+
+ return r;
+}
+
+static int home_update_quota_btrfs(UserRecord *h, const char *path) {
+ int r;
+
+ assert(h);
+ assert(path);
+
+ if (h->disk_size == UINT64_MAX)
+ return 0;
+
+ /* If the user wants quota, enable it */
+ r = btrfs_quota_enable(path, true);
+ if (r == -ENOTTY)
+ return log_error_errno(r, "No btrfs quota support on subvolume %s.", path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable btrfs quota support on %s.", path);
+
+ r = btrfs_qgroup_set_limit(path, 0, h->disk_size);
+ if (r < 0)
+ return log_error_errno(r, "Faled to set disk quota on subvolume %s: %m", path);
+
+ log_info("Set btrfs quota.");
+
+ return 0;
+}
+
+static int home_update_quota_classic(UserRecord *h, const char *path) {
+ _cleanup_free_ char *devnode = NULL;
+ struct dqblk req;
+ dev_t devno;
+ int r;
+
+ assert(h);
+ assert(uid_is_valid(h->uid));
+ assert(path);
+
+ if (h->disk_size == UINT64_MAX)
+ return 0;
+
+ r = get_block_device(path, &devno);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine block device of %s: %m", path);
+ if (devno == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "File system %s not backed by a block device.", path);
+
+ r = device_path_make_major_minor(S_IFBLK, devno, &devnode);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive block device path for file system %s: %m", path);
+
+ if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno))
+ return log_error_errno(errno, "No UID quota support on %s.", path);
+
+ if (errno != ESRCH)
+ return log_error_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid);
+
+ zero(req);
+ } else {
+ /* Shortcut things if everything is set up properly already */
+ if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) {
+ log_info("Configured quota already matches the intended setting, not updating quota.");
+ return 0;
+ }
+ }
+
+ req.dqb_valid = QIF_BLIMITS;
+ req.dqb_bsoftlimit = req.dqb_bhardlimit = h->disk_size / QIF_DQBLKSIZE;
+
+ if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) {
+ if (errno == ESRCH)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path);
+
+ return log_error_errno(errno, "Failed to set disk quota for UID " UID_FMT ": %m", h->uid);
+ }
+
+ log_info("Updated per-UID quota.");
+
+ return 0;
+}
+
+static int home_update_quota_auto(UserRecord *h, const char *path) {
+ struct statfs sfs;
+ int r;
+
+ assert(h);
+
+ if (h->disk_size == UINT64_MAX)
+ return 0;
+
+ if (!path) {
+ path = user_record_image_path(h);
+ if (!path)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home record lacks image path.");
+ }
+
+ if (statfs(path, &sfs) < 0)
+ return log_error_errno(errno, "Failed to statfs() file system: %m");
+
+ if (is_fs_type(&sfs, XFS_SB_MAGIC) ||
+ is_fs_type(&sfs, EXT4_SUPER_MAGIC))
+ return home_update_quota_classic(h, path);
+
+ if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) {
+
+ r = btrfs_is_subvol(path);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Directory %s is not a subvolume, cannot apply quota.", path);
+
+ return home_update_quota_btrfs(h, path);
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Type of directory %s not known, cannot apply quota.", path);
+}
+
+static int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_close_ int root_fd = -1;
+ _cleanup_free_ char *d = NULL;
+ const char *ip;
+ int r;
+
+ assert(h);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME));
+ assert(ret_home);
+
+ assert_se(ip = user_record_image_path(h));
+
+ r = tempfn_random(ip, "homework", &d);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate temporary directory: %m");
+
+ (void) mkdir_parents(d, 0755);
+
+ switch (user_record_storage(h)) {
+
+ case USER_SUBVOLUME:
+ RUN_WITH_UMASK(0077)
+ r = btrfs_subvol_make(d);
+
+ if (r >= 0) {
+ log_info("Subvolume created.");
+
+ if (h->disk_size != UINT64_MAX) {
+
+ /* Enable quota for the subvolume we just created. Note we don't check for
+ * errors here and only log about debug level about this. */
+ r = btrfs_quota_enable(d, true);
+ if (r < 0)
+ log_debug_errno(r, "Failed to enable quota on %s, ignoring: %m", d);
+
+ r = btrfs_subvol_auto_qgroup(d, 0, false);
+ if (r < 0)
+ log_debug_errno(r, "Failed to set up automatic quota group on %s, ignoring: %m", d);
+
+ /* Actually configure the quota. We also ignore errors here, but we do log
+ * about them loudly, to keep things discoverable even though we don't
+ * consider lacking quota support in kernel fatal. */
+ (void) home_update_quota_btrfs(h, d);
+ }
+
+ break;
+ }
+ if (r != -ENOTTY)
+ return log_error_errno(r, "Failed to create temporary home directory subvolume %s: %m", d);
+
+ log_info("Creating subvolume %s is not supported, as file system does not support subvolumes. Falling back to regular directory.", d);
+ _fallthrough_;
+
+ case USER_DIRECTORY:
+
+ if (mkdir(d, 0700) < 0)
+ return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d);
+
+ (void) home_update_quota_classic(h, d);
+ break;
+
+ default:
+ assert_not_reached("unexpected storage");
+ }
+
+ temporary = TAKE_PTR(d); /* Needs to be destroyed now */
+
+ root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0)
+ return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+ r = home_populate(h, root_fd);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+ if (r < 0)
+ return log_error_errno(r, "Failed to clone record: %m");
+
+ r = user_record_add_binding(
+ new_home,
+ user_record_storage(h),
+ ip,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ NULL,
+ NULL, 0,
+ NULL,
+ h->uid,
+ (gid_t) h->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add binding to record: %m");
+
+ if (rename(temporary, ip) < 0)
+ return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip);
+
+ temporary = mfree(temporary);
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_create_fscrypt(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_free_ char *d = NULL, *hex = NULL, *salt_xattr = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_close_ int root_fd = -1;
+ struct fscrypt_policy policy;
+ uint8_t salt_buffer[64];
+ const void *salt = NULL;
+ size_t salt_size = 0;
+ const char *ip;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_FSCRYPT);
+ assert(ret_home);
+
+ if (strv_length(h->password) > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Record has more than one password which is not supported for fscrypt, refusing.");
+
+ assert_se(ip = user_record_image_path(h));
+
+ r = tempfn_random(ip, "homework", &d);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate temporary directory: %m");
+
+ (void) mkdir_parents(d, 0755);
+
+ if (mkdir(d, 0700) < 0)
+ return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d);
+
+ temporary = TAKE_PTR(d); /* Needs to be destroyed now */
+
+ root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0)
+ return log_error_errno(errno, "Failed to open temporary home directory: %m");
+
+ if (ioctl(root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_error_errno(errno, "File system does not support fscrypt: %m");
+ return -ENOLINK; /* make recognizable */
+ }
+ if (errno != ENODATA)
+ return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m");
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d);
+
+ policy = (struct fscrypt_policy) {
+ .version = 0,
+ .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS,
+ .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS,
+ .flags = FS_POLICY_FLAGS_PAD_32,
+ };
+
+ if (h->fscrypt_salt_size > 0) {
+ salt = h->fscrypt_salt;
+ salt_size = h->fscrypt_salt_size;
+ } else {
+ ssize_t n;
+
+ r = genuine_random_bytes(salt_buffer, sizeof(salt_buffer), RANDOM_BLOCK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate salt value: %m");
+
+ n = base64mem(salt_buffer, sizeof(salt_buffer), &salt_xattr);
+ if (n < 0)
+ return log_error_errno(n, "Failed to base64 encode salt value: %m");
+
+ salt = salt_buffer;
+ salt_size = sizeof(salt_buffer);
+ }
+
+ r = home_upload_fscrypt_key(h, h->password[0], salt, salt_size, NULL, policy.master_key_descriptor);
+ if (r < 0)
+ return r;
+
+ if (ioctl(root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0)
+ return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m");
+
+ log_info("Encryption policy set.");
+
+ (void) home_update_quota_classic(h, temporary);
+
+ r = home_populate(h, root_fd);
+ if (r < 0)
+ return r;
+
+ if (fsetxattr(root_fd, "trusted.fscrypt_salt", salt_xattr, strlen(salt_xattr), 0) < 0)
+ return log_error_errno(errno, "Failed to set salt value extended attribute on %s: %m", ip);
+
+ r = sync_and_statfs(root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+ if (r < 0)
+ return log_error_errno(r, "Failed to clone record: %m");
+
+ r = user_record_add_binding(
+ new_home,
+ USER_FSCRYPT,
+ ip,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ NULL,
+ salt, salt_size,
+ NULL,
+ h->uid,
+ (gid_t) h->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add binding to record: %m");
+
+ if (rename(temporary, ip) < 0)
+ return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip);
+
+ temporary = mfree(temporary);
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_create_cifs(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ int r, copy;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_CIFS);
+ assert(ret_home);
+
+ if (!h->cifs_service)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing.");
+
+ if (access("/sbin/mount.cifs", F_OK) < 0) {
+ if (errno == ENOENT)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "/sbin/mount.cifs is missing.");
+
+ return log_error_errno(errno, "Unable to detect whether /sbin/mount.cifs exists: %m");
+ }
+
+ r = home_prepare_cifs(h, false, &setup);
+ if (r < 0)
+ return r;
+
+ copy = fcntl(setup.root_fd, F_DUPFD_CLOEXEC, 3);
+ if (copy < 0)
+ return -errno;
+
+ d = fdopendir(copy);
+ if (!d) {
+ safe_close(copy);
+ return -errno;
+ }
+
+ errno = 0;
+ if (readdir_no_dot(d))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing.");
+ if (errno != 0)
+ return log_error_errno(errno, "Failed to detect if CIFS directory is empty: %m");
+
+ r = home_populate(h, setup.root_fd);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(setup.root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home);
+ if (r < 0)
+ return log_error_errno(r, "Failed to clone record: %m");
+
+ r = user_record_add_binding(
+ new_home,
+ USER_CIFS,
+ NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ NULL,
+ NULL, 0,
+ NULL,
+ h->uid,
+ (gid_t) h->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add binding to record: %m");
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_create(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks name, refusing.");
+ if (!uid_is_valid(h->uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r != USER_TEST_ABSENT)
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Home directory %s already exists, refusing.", user_record_home_directory(h));
+
+ /* When the user didn't specify the storage type to use, fix it to be LUKS -- unless we run in a
+ * container where loopback devices and LUKS/DM are not available. Note that we typically default to
+ * the assumption of "classic" storage for most operations. However, if we create a new home, then
+ * let's user LUKS if nothing is specified. */
+ if (h->storage < 0) {
+ UserStorage new_storage;
+
+ r = detect_container();
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine whether we are in a container: %m");
+ if (r > 0) {
+ new_storage = USER_DIRECTORY;
+
+ r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC);
+ if (r < 0)
+ log_debug_errno(r, "Failed to determine file system of /home, ignoring: %m");
+
+ new_storage = r > 0 ? USER_SUBVOLUME : USER_DIRECTORY;
+ } else
+ new_storage = USER_LUKS;
+
+ r = user_record_add_binding(
+ h,
+ new_storage,
+ NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ SD_ID128_NULL,
+ NULL,
+ NULL,
+ UINT64_MAX,
+ NULL,
+ NULL, 0,
+ NULL,
+ UID_INVALID,
+ GID_INVALID);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change storage type to LUKS: %m");
+
+ if (!h->image_path_auto) {
+ h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), new_storage == USER_LUKS ? ".home" : ".homedir");
+ if (!h->image_path_auto)
+ return log_oom();
+ }
+ }
+
+ r = user_record_test_image_path_and_warn(h);
+ if (r < 0)
+ return r;
+ 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));
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ r = home_create_luks(h, &new_home);
+ break;
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ r = home_create_directory_or_subvolume(h, &new_home);
+ break;
+
+ case USER_FSCRYPT:
+ r = home_create_fscrypt(h, &new_home);
+ break;
+
+ case USER_CIFS:
+ r = home_create_cifs(h, &new_home);
+ break;
+
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY),
+ "Creating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+ }
+ if (r < 0)
+ return r;
+
+ if (user_record_equal(h, new_home)) {
+ *ret_home = NULL;
+ return 0;
+ }
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int home_remove(UserRecord *h) {
+ bool deleted = false;
+ const char *ip, *hd;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+ if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Removing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ hd = user_record_home_directory(h);
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r == USER_TEST_MOUNTED)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Directory %s is still mounted, refusing.", hd);
+
+ assert(hd);
+
+ r = user_record_test_image_path_and_warn(h);
+ if (r < 0)
+ return r;
+
+ ip = user_record_image_path(h);
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS: {
+ struct stat st;
+
+ assert(ip);
+
+ if (stat(ip, &st) < 0) {
+ if (errno != -ENOENT)
+ return log_error_errno(errno, "Failed to stat %s: %m", ip);
+
+ } else {
+ if (S_ISREG(st.st_mode)) {
+ if (unlink(ip) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to remove %s: %m", ip);
+ } else
+ deleted = true;
+
+ } else if (S_ISBLK(st.st_mode))
+ log_info("Not removing file system on block device %s.", ip);
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Image file %s is neither block device, nor regular, refusing removal.", ip);
+ }
+
+ break;
+ }
+
+ case USER_SUBVOLUME:
+ case USER_DIRECTORY:
+ case USER_FSCRYPT:
+ assert(ip);
+
+ r = rm_rf(ip, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_warning_errno(r, "Failed to remove %s: %m", ip);
+ } else
+ deleted = true;
+
+ /* If the image path and the home directory are the same invalidate the home directory, so
+ * that we don't remove it anymore */
+ if (path_equal(ip, hd))
+ hd = NULL;
+
+ break;
+
+ case USER_CIFS:
+ /* Nothing else to do here: we won't remove remote stuff. */
+ log_info("Not removing home directory on remote server.");
+ break;
+
+ default:
+ assert_not_reached("unknown storage type");
+ }
+
+ if (hd) {
+ if (rmdir(hd) < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to remove %s, ignoring: %m", hd);
+ } else
+ deleted = true;
+ }
+
+ if (deleted)
+ log_info("Everything completed.");
+ else {
+ log_notice("Nothing to remove.");
+ return -EALREADY;
+ }
+
+ return 0;
+}
+
+static int home_validate_update(UserRecord *h, HomeSetup *setup) {
+ bool has_mount = false;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(!setup->dm_name);
+ assert(!setup->dm_node);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing.");
+ if (!uid_is_valid(h->uid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing.");
+ if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+
+ has_mount = r == USER_TEST_MOUNTED;
+
+ r = user_record_test_image_path_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r == USER_TEST_ABSENT)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s does not exist", user_record_image_path(h));
+
+ switch (user_record_storage(h)) {
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ case USER_CIFS:
+ break;
+
+ case USER_LUKS: {
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+ bool has_dm = false;
+
+ r = make_dm_names(h->user_name, &dm_name, &dm_node);
+ if (r < 0)
+ return r;
+
+ r = access(dm_node, F_OK);
+ if (r < 0 && errno != ENOENT)
+ return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node);
+
+ has_dm = r >= 0;
+
+ if (has_dm != has_mount)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Home mount incompletely set up.");
+
+ setup->dm_name = TAKE_PTR(dm_name);
+ setup->dm_node = TAKE_PTR(dm_node);
+ break;
+ }
+
+ default:
+ assert_not_reached("unexpected storage type");
+ }
+
+ return has_mount; /* return true if the home record is already active */
+}
+
+static int home_update(UserRecord *h, UserRecord **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL;
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = home_validate_update(h, &setup);
+ if (r < 0)
+ return r;
+
+ already_activated = r > 0;
+
+ r = home_prepare(h, already_activated, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home);
+ if (r < 0)
+ return r;
+
+ r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, &setup);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(setup.root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo(&setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret = TAKE_PTR(new_home);
+ return 0;
+}
+
+enum {
+ CAN_RESIZE_ONLINE,
+ CAN_RESIZE_OFFLINE,
+};
+
+static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) {
+ struct statfs sfs;
+
+ assert(fd >= 0);
+
+ /* Filter out bogus requests early */
+ if (old_size == 0 || old_size == UINT64_MAX ||
+ new_size == 0 || new_size == UINT64_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid resize parameters.");
+
+ if ((old_size & 511) != 0 || (new_size & 511) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Resize parameters not multiple of 512.");
+
+ if (fstatfs(fd, &sfs) < 0)
+ return log_error_errno(errno, "Failed to fstatfs() file system: %m");
+
+ if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) {
+
+ if (new_size < BTRFS_MINIMAL_SIZE)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for btrfs (needs to be 256M at least.");
+
+ /* btrfs can grow and shrink online */
+
+ } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) {
+
+ if (new_size < XFS_MINIMAL_SIZE)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for xfs (needs to be 14M at least).");
+
+ /* XFS can grow, but not shrink */
+ if (new_size < old_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Shrinking this type of file system is not supported.");
+
+ } else if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) {
+
+ if (new_size < EXT4_MINIMAL_SIZE)
+ return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for ext4 (needs to be 1M at least).");
+
+ /* ext4 can grow online, and shrink offline */
+ if (new_size < old_size)
+ return CAN_RESIZE_OFFLINE;
+
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(ESOCKTNOSUPPORT), "Resizing this type of file system is not supported.");
+
+ return CAN_RESIZE_ONLINE;
+}
+
+static int ext4_offline_resize_fs(HomeSetup *setup, uint64_t new_size, bool discard) {
+ _cleanup_free_ char *size_str = NULL;
+ bool re_open = false, re_mount = false;
+ pid_t resize_pid, fsck_pid;
+ int r, exit_status;
+
+ assert(setup);
+ assert(setup->dm_node);
+
+ /* First, unmount the file system */
+ if (setup->root_fd >= 0) {
+ setup->root_fd = safe_close(setup->root_fd);
+ re_open = true;
+ }
+
+ if (setup->undo_mount) {
+ r = umount_verbose("/run/systemd/user-home-mount");
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = false;
+ re_mount = true;
+ }
+
+ log_info("Temporarary unmounting of file system completed.");
+
+ /* resize2fs requires that the file system is force checked first, do so. */
+ r = safe_fork("(e2fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL);
+ log_error_errno(errno, "Failed to execute e2fsck: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ exit_status = wait_for_terminate_and_check("e2fsck", fsck_pid, WAIT_LOG_ABNORMAL);
+ if (exit_status < 0)
+ return exit_status;
+ if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) {
+ log_warning("e2fsck failed with exit status %i.", exit_status);
+
+ if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing.");
+
+ log_warning("Ignoring fsck error.");
+ }
+
+ log_info("Forced file system check completed.");
+
+ /* We use 512 sectors here, because resize2fs doesn't do byte sizes */
+ if (asprintf(&size_str, "%" PRIu64 "s", new_size / 512) < 0)
+ return log_oom();
+
+ /* Resize the thing */
+ r = safe_fork("(e2resize)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, &resize_pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+ execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL);
+ log_error_errno(errno, "Failed to execute resize2fs: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ log_info("Offline file system resize completed.");
+
+ /* Re-establish mounts and reopen the directory */
+ if (re_mount) {
+ r = mount_node(setup->dm_node, "ext4", discard);
+ if (r < 0)
+ return r;
+
+ setup->undo_mount = true;
+ }
+
+ if (re_open) {
+ setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (setup->root_fd < 0)
+ return log_error_errno(errno, "Failed to reopen file system: %m");
+ }
+
+ log_info("File system mounted again.");
+
+ return 0;
+}
+
+static int prepare_resize_partition(
+ int fd,
+ uint64_t partition_offset,
+ uint64_t old_partition_size,
+ uint64_t new_partition_size,
+ sd_id128_t *ret_disk_uuid,
+ struct fdisk_table **ret_table) {
+
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL;
+ _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL;
+ size_t n_partitions, i;
+ sd_id128_t disk_uuid;
+ bool found = false;
+ int r;
+
+ assert(fd >= 0);
+ assert(ret_disk_uuid);
+ assert(ret_table);
+
+ assert((partition_offset & 511) == 0);
+ assert((old_partition_size & 511) == 0);
+ assert((new_partition_size & 511) == 0);
+ assert(UINT64_MAX - old_partition_size >= partition_offset);
+ assert(UINT64_MAX - new_partition_size >= partition_offset);
+
+ if (partition_offset == 0) {
+ /* If the offset is at the beginning we assume no partition table, let's exit early. */
+ log_debug("Not rewriting partition table, operating on naked device.");
+ *ret_disk_uuid = SD_ID128_NULL;
+ *ret_table = NULL;
+ return 0;
+ }
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+ return log_oom();
+
+ r = fdisk_assign_device(c, path, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device: %m");
+
+ if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "Disk has no GPT partition table.");
+
+ r = fdisk_get_disklabel_id(c, &disk_uuid_as_string);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire disk UUID: %m");
+
+ r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid);
+ if (r < 0)
+ return log_error_errno(r, "Failed parse disk UUID: %m");
+
+ r = fdisk_get_partitions(c, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire partition table: %m");
+
+ n_partitions = fdisk_table_get_nents(t);
+ for (i = 0; i < n_partitions; i++) {
+ struct fdisk_partition *p;
+
+ p = fdisk_table_get_partition(t, i);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read partition metadata: %m");
+
+ if (fdisk_partition_is_used(p) <= 0)
+ continue;
+ if (fdisk_partition_has_start(p) <= 0 || fdisk_partition_has_size(p) <= 0 || fdisk_partition_has_end(p) <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found partition without a size.");
+
+ if (fdisk_partition_get_start(p) == partition_offset / 512U &&
+ fdisk_partition_get_size(p) == old_partition_size / 512U) {
+
+ if (found)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition found twice, refusing.");
+
+ /* Found our partition, now patch it */
+ r = fdisk_partition_size_explicit(p, 1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable explicit partition size: %m");
+
+ r = fdisk_partition_set_size(p, new_partition_size / 512U);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change partition size: %m");
+
+ found = true;
+ continue;
+
+ } else {
+ if (fdisk_partition_get_start(p) < partition_offset + new_partition_size / 512U &&
+ fdisk_partition_get_end(p) >= partition_offset / 512)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, conflicting partition found.");
+ }
+ }
+
+ if (!found)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize.");
+
+ *ret_table = TAKE_PTR(t);
+ *ret_disk_uuid = disk_uuid;
+
+ return 1;
+}
+
+static int ask_cb(struct fdisk_context *c, struct fdisk_ask *ask, void *userdata) {
+ char *result;
+
+ assert(c);
+
+ switch (fdisk_ask_get_type(ask)) {
+
+ case FDISK_ASKTYPE_STRING:
+ result = new(char, 37);
+ if (!result)
+ return log_oom();
+
+ fdisk_ask_string_set_result(ask, id128_to_uuid_string(*(sd_id128_t*) userdata, result));
+ break;
+
+ default:
+ log_debug("Unexpected question from libfdisk, ignoring.");
+ }
+
+ return 0;
+}
+
+static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_table *t) {
+ _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL;
+ _cleanup_free_ void *two_zero_lbas = NULL;
+ _cleanup_free_ char *path = NULL;
+ ssize_t n;
+ int r;
+
+ assert(fd >= 0);
+
+ if (!t) /* no partition table to apply, exit early */
+ return 0;
+
+ two_zero_lbas = malloc0(1024U);
+ if (!two_zero_lbas)
+ return log_oom();
+
+ /* libfdisk appears to get confused by the existing PMBR. Let's explicitly flush it out. */
+ n = pwrite(fd, two_zero_lbas, 1024U, 0);
+ if (n < 0)
+ return log_error_errno(r, "Failed to wipe partition table: %m");
+ if (n != 1024)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while whiping partition table.");
+
+ c = fdisk_new_context();
+ if (!c)
+ return log_oom();
+
+ if (asprintf(&path, "/proc/self/fd/%i", fd) < 0)
+ return log_oom();
+
+ r = fdisk_assign_device(c, path, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open device: %m");
+
+ r = fdisk_create_disklabel(c, "gpt");
+ if (r < 0)
+ return log_error_errno(r, "Failed to create GPT disk label: %m");
+
+ r = fdisk_apply_table(c, t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to apply partition table: %m");
+
+ r = fdisk_set_ask(c, ask_cb, &disk_uuids);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set libfdisk query function: %m");
+
+ r = fdisk_set_disklabel_id(c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change disklabel ID: %m");
+
+ r = fdisk_write_disklabel(c);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write disk label: %m");
+
+ return 1;
+}
+
+static int home_resize_luks(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) {
+ char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX],
+ buffer4[FORMAT_BYTES_MAX], buffer5[FORMAT_BYTES_MAX], buffer6[FORMAT_BYTES_MAX];
+ uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, new_partition_size;
+ _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+ _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL;
+ _cleanup_free_ char *whole_disk = NULL;
+ _cleanup_close_ int image_fd = -1;
+ sd_id128_t disk_uuid;
+ const char *ip, *ipo;
+ struct statfs sfs;
+ struct stat st;
+ int r, resize_type;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_LUKS);
+ assert(setup);
+ assert(ret_home);
+
+ assert_se(ipo = user_record_image_path(h));
+ ip = strdupa(ipo); /* copy out since original might change later in home record object */
+
+ image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (image_fd < 0)
+ return log_error_errno(errno, "Failed to open image file %s: %m", ip);
+
+ if (fstat(image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat image file %s: %m", ip);
+ if (S_ISBLK(st.st_mode)) {
+ dev_t parent;
+
+ r = block_get_whole_disk(st.st_rdev, &parent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire whole block device for %s: %m", ip);
+ if (r > 0) {
+ /* If we shall resize a file system on a partition device, then let's figure out the
+ * whole disk device and operate on that instead, since we need to rewrite the
+ * partition table to resize the partition. */
+
+ log_info("Operating on partition device %s, using parent device.", ip);
+
+ r = device_path_make_major_minor(st.st_mode, parent, &whole_disk);
+ if (r < 0)
+ return log_error_errno(r, "Failed to derive whole disk path for %s: %m", ip);
+
+ safe_close(image_fd);
+
+ image_fd = open(whole_disk, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
+ if (image_fd < 0)
+ return log_error_errno(errno, "Failed to open whole block device %s: %m", whole_disk);
+
+ if (fstat(image_fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat whole block device %s: %m", whole_disk);
+ if (!S_ISBLK(st.st_mode))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Whole block device %s is not actually a block device, refusing.", whole_disk);
+ } else
+ log_info("Operating on whole block device %s.", ip);
+
+ if (ioctl(image_fd, BLKGETSIZE64, &old_image_size) < 0)
+ return log_error_errno(errno, "Failed to determine size of original block device: %m");
+
+ if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */
+ return log_error_errno(errno, "Failed to lock block device %s: %m", ip);
+
+ new_image_size = old_image_size; /* we can't resize physical block devices */
+ } else {
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return log_error_errno(r, "Image file %s is not a block device nor regular: %m", ip);
+
+ old_image_size = st.st_size;
+
+ /* Note an asymetry here: when we operate on loopback files the specified disk size we get we
+ * apply onto the loopback file as a whole. When we operate on block devices we instead apply
+ * to the partition itself only. */
+
+ new_image_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+ if (new_image_size == old_image_size) {
+ log_info("Image size already matching, skipping operation.");
+ return 0;
+ }
+ }
+
+ r = home_prepare_luks(h, already_activated, whole_disk, setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ log_info("offset = %" PRIu64 ", size = %" PRIu64 ", image = %" PRIu64, setup->partition_offset, setup->partition_size, old_image_size);
+
+ if ((UINT64_MAX - setup->partition_offset) < setup->partition_size ||
+ setup->partition_offset + setup->partition_size > old_image_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing.");
+
+ if (S_ISREG(st.st_mode)) {
+ uint64_t partition_table_extra;
+
+ partition_table_extra = old_image_size - setup->partition_size;
+ if (new_image_size <= partition_table_extra)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than partition table metadata.");
+
+ new_partition_size = new_image_size - partition_table_extra;
+ } else {
+ assert(S_ISBLK(st.st_mode));
+
+ new_partition_size = DISK_SIZE_ROUND_DOWN(h->disk_size);
+ if (new_partition_size == setup->partition_size) {
+ log_info("Partition size already matching, skipping operation.");
+ return 0;
+ }
+ }
+
+ if ((UINT64_MAX - setup->partition_offset) < new_partition_size ||
+ setup->partition_offset + new_partition_size > new_image_size)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New partition doesn't fit into backing storage, refusing.");
+
+ crypto_offset = crypt_get_data_offset(setup->crypt_device);
+ if (setup->partition_size / 512U <= crypto_offset)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?");
+ if (new_partition_size / 512U <= crypto_offset)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than crypto payload offset?");
+
+ old_fs_size = (setup->partition_size / 512U - crypto_offset) * 512U;
+ new_fs_size = (new_partition_size / 512U - crypto_offset) * 512U;
+
+ /* Before we start doing anything, let's figure out if we actually can */
+ resize_type = can_resize_fs(setup->root_fd, old_fs_size, new_fs_size);
+ if (resize_type < 0)
+ return resize_type;
+ if (resize_type == CAN_RESIZE_OFFLINE && already_activated)
+ return log_error_errno(SYNTHETIC_ERRNO(ETXTBSY), "File systems of this type can only be resized offline, but is currently online.");
+
+ log_info("Ready to resize image size %s → %s, partition size %s → %s, file system size %s → %s.",
+ format_bytes(buffer1, sizeof(buffer1), old_image_size),
+ format_bytes(buffer2, sizeof(buffer2), new_image_size),
+ format_bytes(buffer3, sizeof(buffer3), setup->partition_size),
+ format_bytes(buffer4, sizeof(buffer4), new_partition_size),
+ format_bytes(buffer5, sizeof(buffer5), old_fs_size),
+ format_bytes(buffer6, sizeof(buffer6), new_fs_size));
+
+ r = prepare_resize_partition(
+ image_fd,
+ setup->partition_offset,
+ setup->partition_size,
+ new_partition_size,
+ &disk_uuid,
+ &table);
+ if (r < 0)
+ return r;
+
+ if (new_fs_size > old_fs_size) {
+
+ if (S_ISREG(st.st_mode)) {
+ /* Grow file size */
+
+ if (user_record_luks_discard(h))
+ r = ftruncate(image_fd, new_image_size);
+ else
+ r = fallocate(image_fd, 0, 0, new_image_size);
+ if (r < 0) {
+ if (ERRNO_IS_DISK_SPACE(errno)) {
+ log_debug_errno(errno, "Not enough disk space to grow home.");
+ return -ENOSPC; /* make recognizable */
+ }
+
+ return log_error_errno(errno, "Failed to grow image file %s: %m", ip);
+ }
+
+ log_info("Growing of image file completed.");
+ }
+
+ /* Make sure loopback device sees the new bigger size */
+ r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size);
+ if (r == -ENOTTY)
+ log_debug_errno(r, "Device is not a loopback device, not refreshing size.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to refresh loopback device size: %m");
+ else
+ log_info("Refreshing loop device size completed.");
+
+ r = apply_resize_partition(image_fd, disk_uuid, table);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ log_info("Growing of partition completed.");
+
+ if (ioctl(image_fd, BLKRRPART, 0) < 0)
+ log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m");
+
+ /* Tell LUKS about the new bigger size too */
+ r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512U);
+ if (r < 0)
+ return log_error_errno(r, "Failed to grow LUKS device: %m");
+
+ log_info("LUKS device growing completed.");
+ } else {
+ r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+
+ if (S_ISREG(st.st_mode)) {
+ if (user_record_luks_discard(h))
+ /* Before we shrink, let's trim the file system, so that we need less space on disk during the shrinking */
+ (void) run_fitrim(setup->root_fd);
+ else {
+ /* If discard is off, let's ensure all backing blocks are allocated, so that our resize operation doesn't fail half-way */
+ r = run_fallocate(image_fd, &st);
+ if (r < 0)
+ return r;
+ }
+ }
+ }
+
+ /* Now resize the file system */
+ if (resize_type == CAN_RESIZE_ONLINE)
+ r = resize_fs(setup->root_fd, new_fs_size);
+ else
+ r = ext4_offline_resize_fs(setup, new_fs_size, user_record_luks_discard(h));
+ if (r < 0)
+ return log_error_errno(r, "Failed to resize file system: %m");
+
+ log_info("File system resizing completed.");
+
+ /* Immediately sync afterwards */
+ r = sync_and_statfs(setup->root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ if (new_fs_size < old_fs_size) {
+
+ /* Shrink the LUKS device now, matching the new file system size */
+ r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512);
+ if (r < 0)
+ return log_error_errno(r, "Failed to shrink LUKS device: %m");
+
+ log_info("LUKS device shrinking completed.");
+
+ if (S_ISREG(st.st_mode)) {
+ /* Shrink the image file */
+ if (ftruncate(image_fd, new_image_size) < 0)
+ return log_error_errno(errno, "Failed to shrink image file %s: %m", ip);
+
+ log_info("Shrinking of image file completed.");
+ }
+
+ /* Refresh the loop devices size */
+ r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size);
+ if (r == -ENOTTY)
+ log_debug_errno(r, "Device is not a loopback device, not refreshing size.");
+ else if (r < 0)
+ return log_error_errno(r, "Failed to refresh loopback device size: %m");
+ else
+ log_info("Refreshing loop device size completed.");
+
+ r = apply_resize_partition(image_fd, disk_uuid, table);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ log_info("Shrinking of partition completed.");
+
+ if (ioctl(image_fd, BLKRRPART, 0) < 0)
+ log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m");
+ } else {
+ r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+ }
+
+ r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, setup);
+ if (r < 0)
+ return r;
+
+ if (user_record_luks_discard(h))
+ (void) run_fitrim(setup->root_fd);
+
+ r = sync_and_statfs(setup->root_fd, &sfs);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo(setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ print_size_summary(new_image_size, new_fs_size, &sfs);
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_resize_directory(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
+ int r;
+
+ assert(h);
+ assert(setup);
+ assert(ret_home);
+ assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT));
+
+ r = home_prepare(h, already_activated, setup, NULL);
+ if (r < 0)
+ return r;
+
+ r = load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ r = home_update_quota_auto(h, NULL);
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return -ESOCKTNOSUPPORT; /* make recognizable */
+ if (r < 0)
+ return r;
+
+ r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, setup);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(setup->root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo(setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 0;
+}
+
+static int home_resize(UserRecord *h, UserRecord **ret) {
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ if (h->disk_size == UINT64_MAX)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing.");
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = home_validate_update(h, &setup);
+ if (r < 0)
+ return r;
+
+ already_activated = r > 0;
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ return home_resize_luks(h, already_activated, &setup, ret);
+
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ return home_resize_directory(h, already_activated, &setup, ret);
+
+ default:
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+ }
+}
+
+static int luks_passwd(struct crypt_device *cd, char **passwords, char **hashed_passwords) {
+ _cleanup_free_ char **add = NULL; /* Note: do not use strv_free() here! */
+ size_t volume_key_size, added = 0, i, max_key_slots;
+ _cleanup_(erase_and_freep) void *volume_key = NULL;
+ bool volume_key_loaded = false;
+ const char *type;
+ char **pp;
+ int r;
+
+ if (!cd)
+ return 0;
+
+ type = crypt_get_type(cd);
+ if (!type)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type.");
+
+ r = crypt_keyslot_max(type);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine number of key slots.");
+ max_key_slots = r;
+
+ r = crypt_get_volume_key_size(cd);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine volume key size.");
+ volume_key_size = r;
+
+ volume_key = malloc(volume_key_size);
+ if (!volume_key)
+ return log_oom();
+
+ add = new(char*, strv_length(passwords));
+ if (!add)
+ return log_oom();
+
+ STRV_FOREACH(pp, passwords) {
+ /* Look if this password is among the hashed passwords. If so, it's a new password to set,
+ * otherwise an old password to use to unlock */
+
+ r = test_password(hashed_passwords, *pp);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ /* This is a new key to add */
+ add[added++] = *pp;
+
+ if (!volume_key_loaded) {
+ /* Try to load the volume key with this */
+ r = crypt_volume_key_get(cd, CRYPT_ANY_SLOT, volume_key, &volume_key_size, *pp, strlen(*pp));
+ if (r == -EPERM) {
+ log_debug_errno(r, "Password %zu didn't work.", (size_t) (pp - passwords));
+ continue;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to unlock LUKS superblock: %m");
+
+ log_info("Unlocked LUKS superblock with key %zu.", (size_t) (pp - passwords));
+ volume_key_loaded = true;
+ }
+ }
+
+ if (added == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Found no new key to set, refusing.");
+
+ if (!volume_key_loaded)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with all keys.");
+
+ for (i = 0; i < max_key_slots; i++) {
+ r = crypt_keyslot_destroy(cd, i);
+ if (r < 0 && !IN_SET(r, -ENOENT, -EINVAL)) /* Returns EINVAL or ENOENT if there's no key in this slot already */
+ return log_error_errno(r, "Failed to destroy LUKS password: %m");
+
+ if (i >= added) {
+ if (r >= 0)
+ log_info("Destroyed LUKS key slot %zu.", i);
+ } else {
+ r = crypt_keyslot_add_by_volume_key(
+ cd,
+ i,
+ volume_key,
+ volume_key_size,
+ add[i],
+ strlen(add[i]));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up LUKS password: %m");
+
+ log_info("Updated LUKS key slot %zu.", i);
+ }
+ }
+
+ return 1;
+}
+
+static int home_passwd(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret_home);
+
+ /* Yes that's right, we cannot change passwords for "fscrypt" directories right now, since ext4
+ * doesn't support re-encryption. We could add that by not deriving the master key directly from the
+ * password, but then we'd need some side storage (xattr?) and would be in the crypto business a
+ * bit too much for my taste (also home directories would not be self-contained anymore
+ * then). Alternatively we could do reencrypt in userspace, by setting up a new directory and copying
+ * things over. But that'd be terribly slow. */
+ if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = home_validate_update(h, &setup);
+ if (r < 0)
+ return r;
+
+ already_activated = r > 0;
+
+ r = home_prepare(h, already_activated, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home);
+ if (r < 0)
+ return r;
+
+ r = luks_passwd(setup.crypt_device, h->password, h->hashed_password);
+ if (r < 0)
+ return r;
+
+ r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home);
+ if (r < 0)
+ return r;
+
+ r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, &setup);
+ if (r < 0)
+ return r;
+
+ r = sync_and_statfs(setup.root_fd, NULL);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo(&setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int home_inspect(UserRecord *h, UserRecord **ret_home) {
+ _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT;
+ _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL;
+ bool already_activated = false;
+ int r;
+
+ assert(h);
+ assert(ret_home);
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ r = home_validate_update(h, &setup);
+ if (r < 0)
+ return r;
+
+ already_activated = r > 0;
+
+ r = home_prepare(h, already_activated, &setup, &header_home);
+ if (r < 0)
+ return r;
+
+ r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, NULL, &new_home);
+ if (r < 0)
+ return r;
+
+ r = extend_embedded_identity(new_home, h, &setup);
+ if (r < 0)
+ return r;
+
+ r = home_setup_undo(&setup);
+ if (r < 0)
+ return r;
+
+ log_info("Everything completed.");
+
+ *ret_home = TAKE_PTR(new_home);
+ return 1;
+}
+
+static int home_lock(UserRecord *h) {
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+ _cleanup_close_ int root_fd = -1;
+ const char *p;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+ if (user_record_storage(h) != USER_LUKS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Locking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_test_home_directory_and_warn(h);
+ if (r < 0)
+ return r;
+ if (r != USER_TEST_MOUNTED)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name);
+
+ assert_se(p = user_record_home_directory(h));
+ root_fd = open(p, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
+ if (root_fd < 0)
+ return log_error_errno(errno, "Failed to open home directory: %m");
+
+ r = make_dm_names(h->user_name, &dm_name, &dm_node);
+ if (r < 0)
+ return r;
+
+ r = crypt_init_by_name(&cd, dm_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+ log_info("Discovered used LUKS device %s.", dm_node);
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ if (syncfs(root_fd) < 0) /* Snake oil, but let's better be safe than sorry */
+ return log_error_errno(errno, "Failed to synchronize file system %s: %m", p);
+
+ root_fd = safe_close(root_fd);
+
+ log_info("File system synchronized.");
+
+ /* Note that we don't invoke FIFREEZE here, it appears libcryptsetup/device-mapper already does that on its own for us */
+
+ r = crypt_suspend(cd, dm_name);
+ if (r < 0) {
+ /* (void) ioctl(root_fd, FITHAW, 0); */
+ return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", dm_node);
+ }
+
+ log_info("LUKS device suspended.");
+
+ log_info("Everything completed.");
+ return 1;
+}
+
+static int home_unlock(UserRecord *h) {
+ _cleanup_(crypt_freep) struct crypt_device *cd = NULL;
+ _cleanup_free_ char *dm_name = NULL, *dm_node = NULL;
+ bool resumed = false;
+ char **pp;
+ int r;
+
+ assert(h);
+
+ if (!h->user_name)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing.");
+ if (user_record_storage(h) != USER_LUKS)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Unlocking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h)));
+
+ r = user_record_validate_hashed_password(h);
+ if (r < 0)
+ return r;
+
+ /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on that mount until we have resumed the device. */
+
+ r = make_dm_names(h->user_name, &dm_name, &dm_node);
+ if (r < 0)
+ return r;
+
+ r = crypt_init_by_name(&cd, dm_name);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name);
+
+ log_info("Discovered used LUKS device %s.", dm_node);
+ crypt_set_log_callback(cd, cryptsetup_log_glue, NULL);
+
+ STRV_FOREACH(pp, h->password) {
+ r = crypt_resume_by_passphrase(cd, dm_name, CRYPT_ANY_SLOT, *pp, strlen(*pp));
+ if (r < 0)
+ log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - h->password));
+ else {
+ log_info("Resumed LUKS device %s.", dm_node);
+ resumed = true;
+ break;
+ }
+ }
+
+ if (!resumed)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock.");
+
+ log_info("LUKS device resumed.");
+
+ log_info("Everything completed.");
+ return 1;
+}
+
+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_(fclosep) FILE *opened_file = NULL;
+ unsigned line = 0, column = 0;
+ const char *json_path = NULL;
+ FILE *json_file;
+ usec_t start;
+ int r;
+
+ start = now(CLOCK_MONOTONIC);
+
+ log_setup_service();
+
+ umask(0022);
+
+ if (argc < 2 || argc > 3)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes one or two arguments.");
+
+ if (argc > 2) {
+ json_path = argv[2];
+
+ opened_file = fopen(json_path, "re");
+ if (!opened_file)
+ return log_error_errno(errno, "Failed to open %s: %m", json_path);
+
+ json_file = opened_file;
+ } else {
+ json_path = "";
+ json_file = stdin;
+ }
+
+ r = json_parse_file(json_file, json_path, JSON_PARSE_SENSITIVE, &v, &line, &column);
+ if (r < 0)
+ return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column);
+
+ home = user_record_new();
+ if (!home)
+ return log_oom();
+
+ r = user_record_load(home, v, USER_RECORD_LOAD_FULL|USER_RECORD_LOG);
+ if (r < 0)
+ return r;
+
+ /* Well known return values of these operations, that systemd-homed knows and converts to proper D-Bus errors:
+ *
+ * EMSGSIZE → file systems of this type cannnot be shrinked
+ * ETXTBSY → file systems of this type can only be shrinked offline
+ * ERANGE → file system size too small
+ * ENOLINK → system does not support selected storage backend
+ * EPROTONOSUPPORT → system does not support selected file system
+ * ENOTTY → operation not support on this storage
+ * ESOCKTNOSUPPORT → operation not support on this file system
+ * ENOKEY → password incorrect (or not sufficient)
+ * EBUSY → file system is currently active
+ * ENOEXEC → file system is currently not active
+ * ENOSPC → not enough disk space for operation
+ */
+
+ if (streq(argv[1], "activate"))
+ r = home_activate(home, &new_home);
+ else if (streq(argv[1], "deactivate"))
+ r = home_deactivate(home, false);
+ else if (streq(argv[1], "deactivate-force"))
+ r = home_deactivate(home, true);
+ else if (streq(argv[1], "create"))
+ r = home_create(home, &new_home);
+ else if (streq(argv[1], "remove"))
+ r = home_remove(home);
+ else if (streq(argv[1], "update"))
+ r = home_update(home, &new_home);
+ else if (streq(argv[1], "resize"))
+ r = home_resize(home, &new_home);
+ else if (streq(argv[1], "passwd"))
+ r = home_passwd(home, &new_home);
+ else if (streq(argv[1], "inspect"))
+ r = home_inspect(home, &new_home);
+ else if (streq(argv[1], "lock"))
+ r = home_lock(home);
+ else if (streq(argv[1], "unlock"))
+ r = home_unlock(home);
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]);
+ if (r == -ENOKEY) {
+ usec_t end, n, d;
+
+ /* Make sure bad passwords replies always take at least 3s, and if longer multiples of 3s, so
+ * that it's not clear how long we actually needed for our calculations. */
+ n = now(CLOCK_MONOTONIC);
+ assert(n >= start);
+
+ d = usec_sub_unsigned(n, start);
+ if (d > BAD_PASSWORD_DELAY_USEC)
+ end = start + DIV_ROUND_UP(d, BAD_PASSWORD_DELAY_USEC) * BAD_PASSWORD_DELAY_USEC;
+ else
+ end = start + BAD_PASSWORD_DELAY_USEC;
+
+ if (n < end)
+ (void) usleep(usec_sub_unsigned(end, n));
+ }
+ if (r < 0)
+ return r;
+
+ /* We always pass the new record back, regardless if it changed or not. This allows our caller to
+ * prepare a fresh record, send to us, and only if it works use it without having to keep a local
+ * copy. */
+ if (new_home)
+ json_variant_dump(new_home->json, JSON_FORMAT_NEWLINE, stdout, NULL);
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/meson.build b/src/home/meson.build
new file mode 100644
index 0000000000000..33bf36345d4fb
--- /dev/null
+++ b/src/home/meson.build
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_homework_sources = files('''
+ home-util.c
+ home-util.h
+ homework.c
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+systemd_homed_sources = files('''
+ home-util.c
+ home-util.h
+ homed-bus.c
+ homed-bus.h
+ homed-home-bus.c
+ homed-home-bus.h
+ homed-home.c
+ homed-home.h
+ homed-manager-bus.c
+ homed-manager-bus.h
+ homed-manager.c
+ homed-manager.h
+ homed-operation.c
+ homed-operation.h
+ homed-varlink.c
+ homed-varlink.h
+ homed.c
+ pwquality-util.c
+ pwquality-util.h
+ user-record-sign.c
+ user-record-sign.h
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+homectl_sources = files('''
+ home-util.c
+ home-util.h
+ homectl.c
+ pwquality-util.c
+ pwquality-util.h
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+pam_systemd_home_sym = 'src/home/pam_systemd_home.sym'
+pam_systemd_home_c = files('''
+ home-util.c
+ home-util.h
+ pam_systemd_home.c
+ user-record-util.c
+ user-record-util.h
+'''.split())
+
+if conf.get('ENABLE_HOMED') == 1
+ install_data('org.freedesktop.home1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.home1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.home1.policy',
+ install_dir : polkitpolicydir)
+endif
diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf
new file mode 100644
index 0000000000000..d615501054d74
--- /dev/null
+++ b/src/home/org.freedesktop.home1.conf
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy
new file mode 100644
index 0000000000000..66ef8e0e9d45c
--- /dev/null
+++ b/src/home/org.freedesktop.home1.policy
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+ The systemd Project
+ http://www.freedesktop.org/wiki/Software/systemd
+
+
+ Create a home
+ Authentication is required for creating a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Remove a home
+ Authentication is required for removing a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Check credentials of a home
+ Authentication is required for checking credentials against a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Update a home
+ Authentication is required for updating a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Resize a home
+ Authentication is required for resizing a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Change password of a home
+ Authentication is required for changing the password of a user's home.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
diff --git a/src/home/org.freedesktop.home1.service b/src/home/org.freedesktop.home1.service
new file mode 100644
index 0000000000000..cff19b38617df
--- /dev/null
+++ b/src/home/org.freedesktop.home1.service
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+[D-BUS Service]
+Name=org.freedesktop.home1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.home1.service
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
new file mode 100644
index 0000000000000..6f795ecb96103
--- /dev/null
+++ b/src/home/pam_systemd_home.c
@@ -0,0 +1,836 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "sd-bus.h"
+
+#include "bus-common-errors.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "home-util.h"
+#include "memory-util.h"
+#include "pam-util.h"
+#include "parse-util.h"
+#include "strv.h"
+#include "user-record-util.h"
+#include "user-record.h"
+
+/* Used for the "systemd-user-record-is-homed" PAM data field, to indicate whether we know whether this user
+ * record is managed by homed or by something else. */
+#define USER_RECORD_IS_HOMED INT_TO_PTR(1)
+#define USER_RECORD_IS_OTHER INT_TO_PTR(2)
+
+static int parse_argv(
+ pam_handle_t *handle,
+ int argc, const char **argv,
+ bool *please_suspend,
+ bool *debug) {
+
+ int i;
+
+ assert(argc >= 0);
+ assert(argc == 0 || argv);
+
+ for (i = 0; i < argc; i++) {
+ const char *v;
+
+ if ((v = startswith(argv[1], "suspend="))) {
+ int k;
+
+ k = parse_boolean(v);
+ if (k < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse suspend-please= argument, ignoring: %s", v);
+ else if (please_suspend)
+ *please_suspend = k;
+
+ } else if ((v = startswith(argv[i], "debug="))) {
+ int k;
+
+ k = parse_boolean(v);
+ if (k < 0)
+ pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v);
+ else if (debug)
+ *debug = k;
+
+ } else
+ pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]);
+ }
+
+ return 0;
+}
+
+static int acquire_user_record(
+ pam_handle_t *handle,
+ UserRecord **ret_record) {
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ const char *username = NULL, *json = NULL;
+ const void *b = NULL;
+ int r;
+
+ assert(handle);
+
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(username)) {
+ pam_syslog(handle, LOG_ERR, "User name not valid.");
+ return PAM_SERVICE_ERR;
+ }
+
+ /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage. */
+ if (STR_IN_SET(username, "root", NOBODY_USER_NAME))
+ return PAM_USER_UNKNOWN;
+
+ /* Let's check if a previous run determined that this user is not managed by homed. If so, let's exit early */
+ r = pam_get_data(handle, "systemd-user-record-is-homed", &b);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ /* Failure */
+ pam_syslog(handle, LOG_ERR, "Failed to get PAM user record is homed flag: %s", pam_strerror(handle, r));
+ return r;
+ } else if (b == NULL)
+ /* Nothing cached yet, need to acquire fresh */
+ json = NULL;
+ else if (b != USER_RECORD_IS_HOMED)
+ /* Definitely not a homed record */
+ return PAM_USER_UNKNOWN;
+ else {
+ /* It's a homed record, let's use the cache, so that we can share it between the session and
+ * the authentication hooks */
+ r = pam_get_data(handle, "systemd-user-record", (const void**) &json);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r));
+ return r;
+ }
+ }
+
+ if (!json) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *json_copy = NULL;
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "GetUserRecordByName",
+ &error,
+ &reply,
+ "s",
+ username);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) ||
+ sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) {
+ pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r));
+ goto user_unknown;
+ }
+
+ if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) {
+ pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r));
+ goto user_unknown;
+ }
+
+ pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+
+ r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL);
+ if (r < 0)
+ return pam_bus_log_parse_error(handle, r);
+
+ json_copy = strdup(json);
+ if (!json_copy)
+ return pam_log_oom(handle);
+
+ r = pam_set_data(handle, "systemd-user-record", json_copy, pam_cleanup_free);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ TAKE_PTR(json_copy);
+
+ r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_HOMED, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag: %s", pam_strerror(handle, r));
+ return r;
+ }
+ }
+
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ ur = user_record_new();
+ if (!ur)
+ return pam_log_oom(handle);
+
+ r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ if (!streq_ptr(username, ur->user_name)) {
+ pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name.");
+ return PAM_SERVICE_ERR;
+ }
+
+ if (ret_record)
+ *ret_record = TAKE_PTR(ur);
+
+ return PAM_SUCCESS;
+
+user_unknown:
+ /* Cache this, so that we don't check again */
+ r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_OTHER, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag, ignoring: %s", pam_strerror(handle, r));
+
+ return PAM_USER_UNKNOWN;
+}
+
+static int release_user_record(pam_handle_t *handle) {
+ int r, k;
+
+ r = pam_set_data(handle, "systemd-user-record", NULL, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r));
+
+ k = pam_set_data(handle, "systemd-user-record-is-homed", NULL, NULL);
+ if (k != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM user record is homed flag: %s", pam_strerror(handle, k));
+
+ return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r;
+}
+
+static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) {
+ safe_close(PTR_TO_FD(data));
+}
+
+static int acquire_home(
+ pam_handle_t *handle,
+ bool please_authenticate,
+ bool please_suspend,
+ bool debug) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ _cleanup_close_ int acquired_fd = -1;
+ const void *home_fd_ptr = NULL;
+ const char *password = NULL;
+ unsigned n_attempts = 0;
+ int r;
+
+ assert(handle);
+
+ /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true,
+ * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call
+ * RefHome() and if that fails queries the user for a password and uses AcquireHome().
+ *
+ * The idea is that the PAM authentication hook sets please_authenticate and thus always
+ * authenticates, while the other PAM hooks unset it so that they can a ref of their own without
+ * authentication if possible, but with authentication if necessary. */
+
+ /* If we already have acquired the fd, let's shortcut this */
+ r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr);
+ if (r == PAM_SUCCESS && PTR_TO_INT(home_fd_ptr) >= 0)
+ return PAM_SUCCESS;
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Eventually, we should analyze the record we got here, and if there's some other authentication
+ * data configured in it query for that. For now we don't care and just unconditionally ask for a
+ * password. */
+
+ if (please_authenticate) {
+ r = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: ");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (isempty(password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ secret = user_record_new();
+ if (!secret)
+ return pam_log_oom(handle);
+
+ r = user_record_set_password(secret, STRV_MAKE(password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it
+ * might happen that the the record we stored on the host does not match the encryption password of
+ * the LUKS image in case the image was used in a different system where the password was
+ * changed. In that case it will happen that the LUKS password and the host password are
+ * different, and we handle that by collecting and passing multiple passwords in that case. Hence we
+ * treat bad passwords as a request to collect one more password and pass the new all all previously
+ * used passwords again. */
+
+ for (;;) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(erase_and_freep) char *newp = NULL;
+
+ password = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ secret ? "AcquireHome" : "RefHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", ur->user_name);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ if (secret) {
+ r = bus_message_append_secret(m, secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+ }
+
+ r = sd_bus_message_append(m, "b", please_suspend);
+ if (r < 0)
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ return PAM_PERM_DENIED;
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", ur->user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ return PAM_MAXTRIES;
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) {
+
+ if (++n_attempts >= 5) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name);
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ return PAM_MAXTRIES;
+ }
+
+ /* This didn't work? Ask for an additional password */
+
+ r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient for authentication of user %s, please try again: ", ur->user_name);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to prompt for additional password: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ password = newp;
+
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE) ||
+ sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) {
+
+ int q;
+
+ /* Only on RefHome(): We can't access the home directory currently, unless
+ * it's unlocked with a password. Try to do so hence, ask for a password. */
+
+ q = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: ");
+ if (q != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ pam_syslog(handle, LOG_DEBUG, "Failed to get password: %s", pam_strerror(handle, q));
+
+ /* This will fail in certain environments, for example when we are
+ * called from OpenSSH's account or session hooks, or in systemd's
+ * per-service PAM logic. In that case, print a friendly message and
+ * accept failure. */
+
+ if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE))
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name);
+ else {
+ assert(sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED));
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name);
+ }
+
+ return PAM_PERM_DENIED;
+ }
+ } else {
+ pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+
+ if (isempty(password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+ } else {
+ int fd;
+
+ r = sd_bus_message_read(reply, "h", &fd);
+ if (r < 0)
+ return pam_bus_log_parse_error(handle, r);
+
+ acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (acquired_fd < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+
+ break;
+ }
+
+ if (password) {
+ /* We acquire a new password? Then add it to our set of secrets */
+ if (!secret) {
+ secret = user_record_new();
+ if (!secret)
+ return pam_log_oom(handle);
+ }
+
+ /* Add the password to the list of passwords we already have, in case the
+ * record and LUKS passwords differ and we need multiple to unlock things */
+ r = user_record_set_password(secret, STRV_MAKE(password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+ }
+
+ /* Try again */
+ }
+
+ r = pam_set_data(handle, "systemd-home-fd", FD_TO_PTR(acquired_fd), cleanup_home_fd);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r));
+ return r;
+ }
+ TAKE_FD(acquired_fd);
+
+ if (secret) {
+ /* We likely just activated the home directory, let's flush out the user record, since a
+ * newer embedded user record might have been acquired from the activation. */
+
+ r = release_user_record(handle);
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA))
+ return r;
+ }
+
+ pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
+
+ return PAM_SUCCESS;
+}
+
+static int release_home_fd(pam_handle_t *handle) {
+ const void *home_fd_ptr = NULL;
+ int r;
+
+ r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr);
+ if (r == PAM_NO_MODULE_DATA || PTR_TO_FD(home_fd_ptr) < 0)
+ return PAM_NO_MODULE_DATA;
+
+ r = pam_set_data(handle, "systemd-home-fd", NULL, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r));
+
+ return r;
+}
+
+_public_ PAM_EXTERN int pam_sm_authenticate(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ bool debug = true, suspend_please = false;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &suspend_please,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating");
+
+ return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug);
+}
+
+_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_open_session(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ bool debug = true, suspend_please = false;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &suspend_please,
+ &debug) < 0)
+ return PAM_SESSION_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug);
+ if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */
+ return PAM_SUCCESS;
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = pam_putenv(handle, "SYSTEMD_HOME=1");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are
+ * not going to process the bus connection in that time, so let's better close before the daemon
+ * kicks us off because we are not processing anything. */
+ (void) pam_release_bus_connection(handle);
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_close_session(
+ pam_handle_t *handle,
+ int flags,
+ int argc, const char **argv) {
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ const char *username = NULL;
+ bool debug = true;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ NULL,
+ &debug) < 0)
+ return PAM_SESSION_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end");
+
+ /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome()
+ * call will be able to do its thing. */
+ r = release_home_fd(handle);
+ if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */
+ return PAM_SUCCESS;
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = pam_get_user(handle, &username, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ReleaseHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", username);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, 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_HOME_BUSY))
+ pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username);
+ else {
+ pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r));
+ return PAM_SESSION_ERR;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_acct_mgmt(
+ pam_handle_t *handle,
+ int flags,
+ int argc,
+ const char **argv) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ bool debug = true, please_suspend = false;
+ usec_t t;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ &please_suspend,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+ r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug);
+ if (r == PAM_USER_UNKNOWN)
+ return PAM_SUCCESS; /* we don't have anything to say about users we don't manage */
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = user_record_test_blocked(ur);
+ switch (r) {
+
+ case -ESTALE:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is newer than current system time, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ case -ENOLCK:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ case -EL2HLT:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ case -EL3HLT:
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+
+ default:
+ if (r < 0) {
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access.");
+ return PAM_ACCT_EXPIRED;
+ }
+
+ break;
+ }
+
+ t = user_record_ratelimit_next_try(ur);
+ if (t != USEC_INFINITY) {
+ usec_t n = now(CLOCK_REALTIME);
+
+ if (t > n) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.",
+ format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC));
+
+ return PAM_MAXTRIES;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+_public_ PAM_EXTERN int pam_sm_chauthtok(
+ pam_handle_t *handle,
+ int flags,
+ int argc,
+ const char **argv) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL;
+ const char *old_password = NULL, *new_password = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ unsigned n_attempts = 0;
+ bool debug = true;
+ int r;
+
+ if (parse_argv(handle,
+ argc, argv,
+ NULL,
+ &debug) < 0)
+ return PAM_AUTH_ERR;
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management");
+
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_user_record(handle, &ur);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Regardless if in the PAM_PRELIM_CHECK phase or not let's ask for the passwords with the right
+ * prompts, so that the password is definitely cached. */
+ r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password, "Old password: ");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (isempty(old_password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ /* Is the new password already cached? */
+ r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password);
+ if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (isempty(new_password)) {
+ /* No, it's not cached, then let's ask for the password and its verification, and cache
+ * it. */
+
+ r = pam_get_authtok_noverify(handle, &new_password, "New password: ");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (isempty(new_password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do
+ // that, and instead assume the password was already verified once when it is found to be
+ // cached already. needs to be fixed in pam_pwquality
+ }
+
+ /* Now everything is cached and checked, let's exit from the preliminary check */
+ if (FLAGS_SET(flags, PAM_PRELIM_CHECK))
+ return PAM_SUCCESS;
+
+ old_secret = user_record_new();
+ if (!old_secret)
+ return pam_log_oom(handle);
+
+ r = user_record_set_password(old_secret, STRV_MAKE(old_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ new_secret = user_record_new();
+ if (!new_secret)
+ return pam_log_oom(handle);
+
+ r = user_record_set_password(new_secret, STRV_MAKE(new_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "ChangePasswordHome");
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = sd_bus_message_append(m, "s", ur->user_name);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = bus_message_append_secret(m, new_secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ r = bus_message_append_secret(m, old_secret);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, 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_HOME_BUSY)) {
+ pam_error(handle, "Home of user %s is currently being modified, please come back later.", ur->user_name);
+ return PAM_PERM_DENIED;
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) {
+ pam_error(handle, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name);
+ return PAM_PERM_DENIED;
+ } else if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) {
+ pam_syslog(handle, LOG_ERR, "Failed to change password for home: %s", bus_error_message(&error, r));
+ return PAM_SERVICE_ERR;
+ }
+ } else {
+ pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name);
+ return PAM_SUCCESS;
+ }
+
+ /* Invalidate old token */
+ r = pam_set_item(handle, PAM_OLDAUTHTOK, NULL);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to reset cached password: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (++n_attempts >= 5)
+ break;
+
+ /* Acquire new old token */
+ r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password,
+ "Old password incorrect or not sufficient for authentication, please try again: ");
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r));
+ return r;
+ }
+ if (isempty(old_password)) {
+ pam_syslog(handle, LOG_DEBUG, "Password request aborted.");
+ return PAM_AUTHTOK_ERR;
+ }
+
+ r = user_record_set_password(old_secret, STRV_MAKE(old_password), true);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set old password: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ /* Try again */
+ };
+
+ pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name);
+ return PAM_MAXTRIES;
+}
diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym
new file mode 100644
index 0000000000000..daec0499e1148
--- /dev/null
+++ b/src/home/pam_systemd_home.sym
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+{
+global:
+ pam_sm_authenticate;
+ pam_sm_setcred;
+ pam_sm_open_session;
+ pam_sm_close_session;
+ pam_sm_acct_mgmt;
+ pam_sm_chauthtok;
+local: *;
+};
diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c
new file mode 100644
index 0000000000000..be4fb1cb4a071
--- /dev/null
+++ b/src/home/pwquality-util.c
@@ -0,0 +1,182 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#if HAVE_LIBPWQUALITY
+/* pwquality.h uses size_t but doesn't include sys/types.h on its own */
+#include
+#include
+#endif
+
+#include "bus-common-errors.h"
+#include "home-util.h"
+#include "memory-util.h"
+#include "pwquality-util.h"
+#include "strv.h"
+
+#if HAVE_LIBPWQUALITY
+DEFINE_TRIVIAL_CLEANUP_FUNC(pwquality_settings_t*, pwquality_free_settings);
+
+static void pwquality_maybe_disable_dictionary(
+ pwquality_settings_t *pwq) {
+
+ char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
+ const char *path;
+ int r;
+
+ r = pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path);
+ if (r < 0) {
+ log_warning("Failed to read libpwquality dictionary path, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL));
+ return;
+ }
+
+ // REMOVE THIS AS SOON AS https://github.com/libpwquality/libpwquality/pull/21 IS MERGED AND RELEASED
+ if (isempty(path))
+ path = "/usr/share/cracklib/pw_dict.pwd.gz";
+
+ if (isempty(path)) {
+ log_warning("Weird, no dictionary file configured, ignoring.");
+ return;
+ }
+
+ if (access(path, F_OK) >= 0)
+ return;
+
+ if (errno != ENOENT) {
+ log_warning_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path);
+ return;
+ }
+
+ r = pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0);
+ if (r < 0) {
+ log_warning("Failed to disable libpwquality dictionary check, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL));
+ return;
+ }
+}
+
+int quality_check_password(
+ UserRecord *hr,
+ UserRecord *secret,
+ sd_bus_error *error) {
+
+ _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
+ char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp;
+ void *auxerror;
+ int r;
+
+ assert(hr);
+ assert(secret);
+
+ pwq = pwquality_default_settings();
+ if (!pwq)
+ return log_oom();
+
+ r = pwquality_read_config(pwq, NULL, &auxerror);
+ if (r < 0)
+ log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s",
+ pwquality_strerror(buf, sizeof(buf), r, auxerror));
+
+ pwquality_maybe_disable_dictionary(pwq);
+
+ /* This is a bit more complex than one might think at first. pwquality_check() would like to know the
+ * old password to make security checks. We support arbitrary numbers of passwords however, hence we
+ * call the function once for each combination of old and new password. */
+
+ /* Iterate through all new passwords */
+ STRV_FOREACH(pp, secret->password) {
+ bool called = false;
+ char **old;
+
+ r = test_password(hr->hashed_password, *pp);
+ if (r < 0)
+ return r;
+ if (r == 0) /* This is an old password as it isn't listed in the hashedPassword field, skip it */
+ continue;
+
+ /* Check this password against all old passwords */
+ STRV_FOREACH(old, secret->password) {
+
+ if (streq(*pp, *old))
+ continue;
+
+ r = test_password(hr->hashed_password, *old);
+ if (r < 0)
+ return r;
+ if (r > 0) /* This is a new password, not suitable as old password */
+ continue;
+
+ r = pwquality_check(pwq, *pp, *old, hr->user_name, &auxerror);
+ if (r < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s",
+ pwquality_strerror(buf, sizeof(buf), r, auxerror));
+
+ called = true;
+ }
+
+ if (called)
+ continue;
+
+ /* If there are no old passwords, let's call pwquality_check() without any. */
+ r = pwquality_check(pwq, *pp, NULL, hr->user_name, &auxerror);
+ if (r < 0)
+ return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s",
+ pwquality_strerror(buf, sizeof(buf), r, auxerror));
+ }
+
+ return 0;
+}
+
+#define N_SUGGESTIONS 6
+
+int suggest_passwords(void) {
+ _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
+ _cleanup_strv_free_erase_ char **suggestions = NULL;
+ _cleanup_(erase_and_freep) char *joined = NULL;
+ char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
+ void *auxerror;
+ size_t i;
+ int r;
+
+ pwq = pwquality_default_settings();
+ if (!pwq)
+ return log_oom();
+
+ r = pwquality_read_config(pwq, NULL, &auxerror);
+ if (r < 0)
+ log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s",
+ pwquality_strerror(buf, sizeof(buf), r, auxerror));
+
+ pwquality_maybe_disable_dictionary(pwq);
+
+ suggestions = new0(char*, N_SUGGESTIONS);
+ if (!suggestions)
+ return log_oom();
+
+ for (i = 0; i < N_SUGGESTIONS; i++) {
+ _cleanup_free_ char *suggestion = NULL;
+
+ r = pwquality_generate(pwq, 64, suggestions + i);
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate password, ignoring: %s",
+ pwquality_strerror(buf, sizeof(buf), r, NULL));
+ }
+
+ joined = strv_join(suggestions, " ");
+ if (!joined)
+ return log_oom();
+
+ log_info("Password suggestions: %s", joined);
+ return 0;
+}
+
+#else
+
+int quality_check_password(UserRecord *hr, sd_bus_error *error) {
+ assert(hr);
+ return 0;
+}
+
+int suggest_passwords(void) {
+ return 0;
+}
+#endif
diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h
new file mode 100644
index 0000000000000..d61c04c34216f
--- /dev/null
+++ b/src/home/pwquality-util.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+#include "user-record.h"
+
+int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error);
+
+int suggest_passwords(void);
diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c
new file mode 100644
index 0000000000000..d02c4750b37c4
--- /dev/null
+++ b/src/home/user-record-sign.c
@@ -0,0 +1,174 @@
+#include
+
+#include "fd-util.h"
+#include "user-record-sign.h"
+#include "fileio.h"
+
+static int user_record_signable_json(UserRecord *ur, char **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *reduced = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *j = NULL;
+ int r;
+
+ assert(ur);
+ assert(ret);
+
+ r = user_record_clone(ur, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_STRIP_SECRET|USER_RECORD_STRIP_BINDING|USER_RECORD_STRIP_STATUS|USER_RECORD_STRIP_SIGNATURE, &reduced);
+ if (r < 0)
+ return r;
+
+ j = json_variant_ref(reduced->json);
+
+ r = json_variant_normalize(&j);
+ if (r < 0)
+ return r;
+
+ return json_variant_format(j, 0, ret);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_MD_CTX*, EVP_MD_CTX_free);
+
+int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *encoded = NULL, *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL;
+ _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL;
+ _cleanup_free_ char *text = NULL, *key = NULL;
+ size_t signature_size = 0, key_size = 0;
+ _cleanup_free_ void *signature = NULL;
+ _cleanup_fclose_ FILE *mf = NULL;
+ int r;
+
+ assert(ur);
+ assert(private_key);
+ assert(ret);
+
+ r = user_record_signable_json(ur, &text);
+ if (r < 0)
+ return r;
+
+ md_ctx = EVP_MD_CTX_new();
+ if (!md_ctx)
+ return -ENOMEM;
+
+ if (EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, private_key) <= 0)
+ return -EIO;
+
+ /* Request signature size */
+ if (EVP_DigestSign(md_ctx, NULL, &signature_size, (uint8_t*) text, strlen(text)) <= 0)
+ return -EIO;
+
+ signature = malloc(signature_size);
+ if (!signature)
+ return -ENOMEM;
+
+ if (EVP_DigestSign(md_ctx, signature, &signature_size, (uint8_t*) text, strlen(text)) <= 0)
+ return -EIO;
+
+ mf = open_memstream_unlocked(&key, &key_size);
+ if (!mf)
+ return -ENOMEM;
+
+ if (PEM_write_PUBKEY(mf, private_key) <= 0)
+ return -EIO;
+
+ r = fflush_and_check(mf);
+ if (r < 0)
+ return r;
+
+ r = json_build(&encoded, JSON_BUILD_ARRAY(
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(signature, signature_size)),
+ JSON_BUILD_PAIR("key", JSON_BUILD_STRING(key)))));
+ if (r < 0)
+ return r;
+
+ v = json_variant_ref(ur->json);
+
+ r = json_variant_set_field(&v, "signature", encoded);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING)
+ json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
+
+ signed_ur = user_record_new();
+ if (!signed_ur)
+ return log_oom();
+
+ r = user_record_load(signed_ur, v, USER_RECORD_LOAD_FULL);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(signed_ur);
+ return 0;
+}
+
+int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) {
+ _cleanup_free_ char *text = NULL, *key = NULL;
+ unsigned n_good = 0, n_bad = 0;
+ JsonVariant *array, *e;
+ int r;
+
+ assert(ur);
+ assert(public_key);
+
+ array = json_variant_by_key(ur->json, "signature");
+ if (!array)
+ return USER_RECORD_UNSIGNED;
+
+ if (!json_variant_is_array(array))
+ return -EINVAL;
+
+ if (json_variant_elements(array) == 0)
+ return USER_RECORD_UNSIGNED;
+
+ r = user_record_signable_json(ur, &text);
+ if (r < 0)
+ return r;
+
+ JSON_VARIANT_ARRAY_FOREACH(e, array) {
+ _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL;
+ _cleanup_free_ void *signature = NULL;
+ size_t signature_size = 0;
+ JsonVariant *data;
+
+ if (!json_variant_is_object(e))
+ return -EINVAL;
+
+ data = json_variant_by_key(e, "data");
+ if (!data)
+ return -EINVAL;
+
+ r = json_variant_unbase64(data, &signature, &signature_size);
+ if (r < 0)
+ return r;
+
+ md_ctx = EVP_MD_CTX_new();
+ if (!md_ctx)
+ return -ENOMEM;
+
+ if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, public_key) <= 0)
+ return -EIO;
+
+ if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) {
+ n_bad ++;
+ continue;
+ }
+
+ n_good ++;
+ }
+
+ return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) :
+ (n_bad == 0 ? USER_RECORD_UNSIGNED : USER_RECORD_FOREIGN);
+}
+
+int user_record_has_signature(UserRecord *ur) {
+ JsonVariant *array;
+
+ array = json_variant_by_key(ur->json, "signature");
+ if (!array)
+ return false;
+
+ if (!json_variant_is_array(array))
+ return -EINVAL;
+
+ return json_variant_elements(array) > 0;
+}
diff --git a/src/home/user-record-sign.h b/src/home/user-record-sign.h
new file mode 100644
index 0000000000000..f045c8837bced
--- /dev/null
+++ b/src/home/user-record-sign.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "user-record.h"
+
+int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret);
+
+enum {
+ USER_RECORD_UNSIGNED, /* user record has no signature */
+ USER_RECORD_SIGNED_EXCLUSIVE, /* user record has only a signature by our own key */
+ USER_RECORD_SIGNED, /* user record is signed by us, but by others too */
+ USER_RECORD_FOREIGN, /* user record is not signed by us, but by others */
+};
+
+int user_record_verify(UserRecord *ur, EVP_PKEY *public_key);
+
+int user_record_has_signature(UserRecord *ur);
diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c
new file mode 100644
index 0000000000000..10adee848f6b9
--- /dev/null
+++ b/src/home/user-record-util.c
@@ -0,0 +1,1007 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#if HAVE_CRYPT_H
+#include
+#endif
+
+#include "errno-util.h"
+#include "home-util.h"
+#include "id128-util.h"
+#include "mountpoint-util.h"
+#include "path-util.h"
+#include "stat-util.h"
+#include "user-record-util.h"
+#include "user-util.h"
+
+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) {
+
+ _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL;
+ char smid[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+ assert(user_name);
+ assert(image_path);
+ assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY));
+ assert(uid_is_valid(uid));
+ assert(gid_is_valid(gid));
+
+ /* Fill in a home record from just a username and an image path. */
+
+ if (h->json)
+ return -EBUSY;
+
+ if (!suitable_user_name(user_name))
+ return -EINVAL;
+
+ if (realm) {
+ r = suitable_realm(realm);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EINVAL;
+ }
+
+ if (!suitable_image_path(image_path))
+ return -EINVAL;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ un = strdup(user_name);
+ if (!un)
+ return -ENOMEM;
+
+ if (realm) {
+ rr = strdup(realm);
+ if (!rr)
+ return -ENOMEM;
+
+ user_name_and_realm = strjoin(user_name, "@", realm);
+ if (!user_name_and_realm)
+ return -ENOMEM;
+ }
+
+ ip = strdup(image_path);
+ if (!ip)
+ return -ENOMEM;
+
+ hd = path_join("/home/", user_name);
+ if (!hd)
+ return -ENOMEM;
+
+ r = json_build(&h->json,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)),
+ JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")),
+ JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)),
+ JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)),
+ JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid))))))));
+ if (r < 0)
+ return r;
+
+ free_and_replace(h->user_name, un);
+ free_and_replace(h->realm, rr);
+ free_and_replace(h->user_name_and_realm_auto, user_name_and_realm);
+ free_and_replace(h->image_path, ip);
+ free_and_replace(h->home_directory, hd);
+ h->storage = storage;
+ h->uid = uid;
+
+ h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
+ return 0;
+}
+
+int group_record_synthesize(GroupRecord *g, UserRecord *h) {
+ _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL;
+ char smid[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+ int r;
+
+ assert(g);
+ assert(h);
+
+ if (g->json)
+ return -EBUSY;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ un = strdup(h->user_name);
+ if (!un)
+ return -ENOMEM;
+
+ if (h->realm) {
+ rr = strdup(h->realm);
+ if (!rr)
+ return -ENOMEM;
+
+ group_name_and_realm = strjoin(un, "@", rr);
+ if (!group_name_and_realm)
+ return -ENOMEM;
+ }
+
+ r = json_build(&g->json,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)),
+ JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)),
+ JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))),
+ JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))),
+ JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home"))))))));
+ if (r < 0)
+ return r;
+
+ free_and_replace(g->group_name, un);
+ free_and_replace(g->realm, rr);
+ free_and_replace(g->group_name_and_realm_auto, group_name_and_realm);
+ g->gid = user_record_gid(h);
+ g->disposition = h->disposition;
+
+ g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING;
+ return 0;
+}
+
+int user_record_reconcile(
+ UserRecord *host,
+ UserRecord *embedded,
+ UserReconcileMode mode,
+ UserRecord **ret) {
+
+ int r, result;
+
+ /* Reconciles the identity record stored on the host with the one embedded in a $HOME
+ * directory. Returns the following error codes:
+ *
+ * -EINVAL: one of the records not valid
+ * -REMCHG: identity records are not about the same user
+ * -ESTALE: embedded identity record is equally new or newer than supplied record
+ *
+ * Return the new record to use, which is either the the embedded record updated with the host
+ * binding or the host record. In both cases the secret data is stripped. */
+
+ assert(host);
+ assert(embedded);
+
+ /* Make sure both records are initialized */
+ if (!host->json || !embedded->json)
+ return -EINVAL;
+
+ /* Ensure these records actually contain user data */
+ if (!(embedded->mask & host->mask & USER_RECORD_REGULAR))
+ return -EINVAL;
+
+ /* Make sure the user name and realm matches */
+ if (!user_record_compatible(host, embedded))
+ return -EREMCHG;
+
+ /* Embedded identities may not contain secrets or binding info*/
+ if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0)
+ return -EINVAL;
+
+ /* The embedded record checked out, let's now figure out which of the two identities we'll consider
+ * in effect from now on. We do this by checking the last change timestamp, and in doubt always let
+ * the embedded data win. */
+ if (host->last_change_usec != UINT64_MAX &&
+ (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec))
+
+ /* The host version is definitely newer, either because it has a version at all and the
+ * embedded version doesn't or because it is numerically newer. */
+ result = USER_RECONCILE_HOST_WON;
+
+ else if (host->last_change_usec == embedded->last_change_usec) {
+
+ /* The nominal version number of the host and the embedded identity is the same. If so, let's
+ * verify that, and tell the caller if we are ignoring embedded data. */
+
+ r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ if (mode == USER_RECONCILE_REQUIRE_NEWER)
+ return -ESTALE;
+
+ result = USER_RECONCILE_IDENTICAL;
+ } else
+ result = USER_RECONCILE_HOST_WON;
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *merged = NULL;
+ JsonVariant *e;
+
+ /* The embedded version is newer */
+
+ if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL)
+ return -ESTALE;
+
+ /* Copy in the binding data */
+ extended = json_variant_ref(embedded->json);
+
+ e = json_variant_by_key(host->json, "binding");
+ if (e) {
+ r = json_variant_set_field(&extended, "binding", e);
+ if (r < 0)
+ return r;
+ }
+
+ merged = user_record_new();
+ if (!merged)
+ return -ENOMEM;
+
+ r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(merged);
+ return USER_RECONCILE_EMBEDDED_WON; /* update */
+ }
+
+ /* Strip out secrets */
+ r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret);
+ if (r < 0)
+ return r;
+
+ return result;
+}
+
+int user_record_add_binding(
+ UserRecord *h,
+ UserStorage storage,
+ const char *image_path,
+ sd_id128_t partition_uuid,
+ sd_id128_t luks_uuid,
+ sd_id128_t fs_uuid,
+ const char *luks_cipher,
+ const char *luks_cipher_mode,
+ uint64_t luks_volume_key_size,
+ const char *file_system_type,
+ const void *fscrypt_salt,
+ size_t fscrypt_salt_size,
+ const char *home_directory,
+ uid_t uid,
+ gid_t gid) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL;
+ char smid[SD_ID128_STRING_MAX], partition_uuids[37], luks_uuids[37], fs_uuids[37];
+ _cleanup_free_ char *ip = NULL, *hd = NULL;
+ _cleanup_free_ void *fs = NULL;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+ assert(fscrypt_salt_size == 0 || fscrypt_salt);
+
+ if (!h->json)
+ return -EUNATCH;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+ sd_id128_to_string(mid, smid);
+
+ if (image_path) {
+ ip = strdup(image_path);
+ if (!ip)
+ return -ENOMEM;
+ }
+
+ if (home_directory) {
+ hd = strdup(home_directory);
+ if (!hd)
+ return -ENOMEM;
+ }
+
+ if (fscrypt_salt_size > 0) {
+ fs = memdup(fscrypt_salt, fscrypt_salt_size);
+ if (!fs)
+ return -ENOMEM;
+ }
+
+ r = json_build(&new_binding_entry,
+ JSON_BUILD_OBJECT(
+ 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(id128_to_uuid_string(partition_uuid, partition_uuids))),
+ JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUUID", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))),
+ JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUUID", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))),
+ JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)),
+ JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)),
+ JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)),
+ JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)),
+ JSON_BUILD_PAIR_CONDITION(fscrypt_salt_size > 0, "fscryptSalt", JSON_BUILD_BASE64(fscrypt_salt, fscrypt_salt_size)),
+ JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)),
+ JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)),
+ JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)),
+ JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage)))));
+ if (r < 0)
+ return r;
+
+ binding = json_variant_ref(json_variant_by_key(h->json, "binding"));
+ if (binding) {
+ _cleanup_(json_variant_unrefp) JsonVariant *be = NULL;
+
+ /* Merge the new entry with an old one, if that exists */
+ be = json_variant_ref(json_variant_by_key(binding, smid));
+ if (be) {
+ r = json_variant_merge(&be, new_binding_entry);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(new_binding_entry);
+ new_binding_entry = TAKE_PTR(be);
+ }
+ }
+
+ r = json_variant_set_field(&binding, smid, new_binding_entry);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&h->json, "binding", binding);
+ if (r < 0)
+ return r;
+
+ if (storage >= 0)
+ h->storage = storage;
+
+ if (ip)
+ free_and_replace(h->image_path, ip);
+
+ if (!sd_id128_is_null(partition_uuid))
+ h->partition_uuid = partition_uuid;
+
+ if (!sd_id128_is_null(luks_uuid))
+ h->luks_uuid = luks_uuid;
+
+ if (!sd_id128_is_null(fs_uuid))
+ h->file_system_uuid = fs_uuid;
+
+ if (fscrypt_salt_size > 0) {
+ free_and_replace(h->fscrypt_salt, fs);
+ h->fscrypt_salt_size = fscrypt_salt_size;
+ }
+
+ if (hd)
+ free_and_replace(h->home_directory, hd);
+
+ if (uid_is_valid(uid))
+ h->uid = uid;
+
+ h->mask |= USER_RECORD_BINDING;
+ return 1;
+}
+
+int user_record_test_home_directory(UserRecord *h) {
+ const char *hd;
+ int r;
+
+ assert(h);
+
+ /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */
+
+ hd = user_record_home_directory(h);
+ if (!hd)
+ return -ENXIO;
+
+ r = is_dir(hd, false);
+ if (r == -ENOENT)
+ return USER_TEST_ABSENT;
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -ENOTDIR;
+
+ r = path_is_mount_point(hd, NULL, 0);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return USER_TEST_MOUNTED;
+
+ /* If the image path and the home directory are identical, then it's OK if the directory is
+ * populated. */
+ if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) {
+ const char *ip;
+
+ ip = user_record_image_path(h);
+ if (ip && path_equal(ip, hd))
+ return USER_TEST_EXISTS;
+ }
+
+ /* Otherwise it's not OK */
+ r = dir_is_empty(hd);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EBUSY;
+
+ return USER_TEST_EXISTS;
+}
+
+int user_record_test_home_directory_and_warn(UserRecord *h) {
+ int r;
+
+ assert(h);
+
+ r = user_record_test_home_directory(h);
+ if (r == -ENXIO)
+ return log_error_errno(r, "User record lacks home directory, refusing.");
+ if (r == -ENOTDIR)
+ return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h));
+ if (r == -EBUSY)
+ return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h));
+ if (r < 0)
+ return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h));
+
+ return r;
+}
+
+int user_record_test_image_path(UserRecord *h) {
+ const char *ip;
+ struct stat st;
+
+ assert(h);
+
+ if (user_record_storage(h) == USER_CIFS)
+ return USER_TEST_UNDEFINED;
+
+ ip = user_record_image_path(h);
+ if (!ip)
+ return -ENXIO;
+
+ if (stat(ip, &st) < 0) {
+ if (errno == ENOENT)
+ return USER_TEST_ABSENT;
+
+ return -errno;
+ }
+
+ switch (user_record_storage(h)) {
+
+ case USER_LUKS:
+ if (S_ISREG(st.st_mode))
+ return USER_TEST_EXISTS;
+ if (S_ISBLK(st.st_mode)) {
+ /* For block devices we can't really be sure if the device referenced actually is the
+ * fs we look for or some other file system (think: what does /dev/sdb1 refer
+ * to?). Hence, let's return USER_TEST_MAYBE as an ambigious return value for these
+ * case, except if the device path used is one of the paths that is based on a
+ * filesystem or partition UUID or label, because in those cases we can be sure we
+ * are referring to the right device. */
+
+ if (PATH_STARTSWITH_SET(ip,
+ "/dev/disk/by-uuid/",
+ "/dev/disk/by-partuuid/",
+ "/dev/disk/by-partlabel/",
+ "/dev/disk/by-label/"))
+ return USER_TEST_EXISTS;
+
+ return USER_TEST_MAYBE;
+ }
+
+ return -EBADFD;
+
+ case USER_CLASSIC:
+ case USER_DIRECTORY:
+ case USER_SUBVOLUME:
+ case USER_FSCRYPT:
+ if (S_ISDIR(st.st_mode))
+ return USER_TEST_EXISTS;
+
+ return -ENOTDIR;
+
+ default:
+ assert_not_reached("Unexpected record type");
+ }
+}
+
+int user_record_test_image_path_and_warn(UserRecord *h) {
+ int r;
+
+ assert(h);
+
+ r = user_record_test_image_path(h);
+ if (r == -ENXIO)
+ return log_error_errno(r, "User record lacks image path, refusing.");
+ if (r == -EBADFD)
+ return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h));
+ if (r == -ENOTDIR)
+ return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h));
+ if (r < 0)
+ return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h));
+
+ return r;
+}
+
+int user_record_test_secret(UserRecord *h, UserRecord *secret) {
+ char **i;
+ int r;
+
+ assert(h);
+
+ /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */
+
+ if (strv_isempty(h->hashed_password))
+ return -ENXIO;
+
+ STRV_FOREACH(i, secret->password) {
+ r = test_password(h->hashed_password, *i);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+ }
+
+ return -ENOKEY;
+}
+
+int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) {
+ _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ char smid[SD_ID128_STRING_MAX];
+ size_t idx = SIZE_MAX, n;
+ JsonVariant *per_machine;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ if (!h->json)
+ return -EUNATCH;
+
+ if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX)
+ return -ERANGE;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ sd_id128_to_string(mid, smid);
+
+ r = json_variant_new_string(&midv, smid);
+ if (r < 0)
+ return r;
+
+ r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1);
+ if (r < 0)
+ return r;
+
+ per_machine = json_variant_by_key(h->json, "perMachine");
+ if (per_machine) {
+ size_t i;
+
+ if (!json_variant_is_array(per_machine))
+ return -EINVAL;
+
+ n = json_variant_elements(per_machine);
+
+ array = new(JsonVariant*, n + 1);
+ if (!array)
+ return -ENOMEM;
+
+ for (i = 0; i < n; i++) {
+ JsonVariant *m;
+
+ array[i] = json_variant_by_index(per_machine, i);
+
+ if (!json_variant_is_object(array[i]))
+ return -EINVAL;
+
+ m = json_variant_by_key(array[i], "matchMachineId");
+ if (!m) {
+ /* No machineId field? Let's ignore this, but invalidate what we found so far */
+ idx = SIZE_MAX;
+ continue;
+ }
+
+ if (json_variant_equal(m, midv) ||
+ json_variant_equal(m, midav)) {
+ /* Matches exactly what we are looking for. Let's use this */
+ idx = i;
+ continue;
+ }
+
+ r = per_machine_id_match(m, JSON_PERMISSIVE);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ /* Also matches what we are looking for, but with a broader match. In this
+ * case let's ignore this entry, and add a new specific one to the end. */
+ idx = SIZE_MAX;
+ }
+
+ if (idx == SIZE_MAX)
+ idx = n++; /* Nothing suitable found, place new entry at end */
+ else
+ ne = json_variant_ref(array[idx]);
+
+ } else {
+ array = new(JsonVariant*, 1);
+ if (!array)
+ return -ENOMEM;
+
+ idx = 0;
+ n = 1;
+ }
+
+ if (!ne) {
+ r = json_variant_set_field(&ne, "matchMachineId", midav);
+ if (r < 0)
+ return r;
+ }
+
+ r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size);
+ if (r < 0)
+ return r;
+
+ assert(idx < n);
+ array[idx] = ne;
+
+ r = json_variant_new_array(&new_per_machine, array, n);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&h->json, "perMachine", new_per_machine);
+ if (r < 0)
+ return r;
+
+ h->disk_size = disk_size;
+ h->mask |= USER_RECORD_PER_MACHINE;
+ return 0;
+}
+
+int user_record_update_last_changed(UserRecord *h, bool with_password) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ usec_t n;
+ int r;
+
+ assert(h);
+
+ if (!h->json)
+ return -EUNATCH;
+
+ n = now(CLOCK_REALTIME);
+
+ /* refuse downgrading */
+ if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n)
+ return -ECHRNG;
+ if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n)
+ return -ECHRNG;
+
+ v = json_variant_ref(h->json);
+
+ r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n);
+ if (r < 0)
+ return r;
+
+ if (with_password) {
+ r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n);
+ if (r < 0)
+ return r;
+
+ h->last_password_change_usec = n;
+ }
+
+ h->last_change_usec = n;
+
+ json_variant_unref(h->json);
+ h->json = TAKE_PTR(v);
+
+ h->mask |= USER_RECORD_REGULAR;
+ return 0;
+}
+
+int user_record_make_hashed_password(UserRecord *h, UserRecord *secret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL, *priv = NULL;
+ _cleanup_strv_free_ char **np = NULL;
+ char **i;
+ int r;
+
+ assert(h);
+ assert(secret);
+
+ /* Initializes the hashed password list from the specified plaintext passwords */
+
+ STRV_FOREACH(i, secret->password) {
+ _cleanup_free_ char *salt = NULL;
+ struct crypt_data cd = {};
+ char *k;
+
+ r = make_salt(&salt);
+ if (r < 0)
+ return r;
+
+ errno = 0;
+ k = crypt_r(*i, salt, &cd);
+ if (!k)
+ return errno_or_else(EINVAL);
+
+ r = strv_extend(&np, k);
+ if (r < 0)
+ return r;
+ }
+
+ r = json_variant_new_array_strv(&new_array, np);
+ if (r < 0)
+ return r;
+
+ priv = json_variant_ref(json_variant_by_key(h->json, "privileged"));
+ r = json_variant_set_field(&priv, "hashedPassword", new_array);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&h->json, "privileged", priv);
+ if (r < 0)
+ return r;
+
+ strv_free(h->hashed_password);
+ h->hashed_password = TAKE_PTR(np);
+
+ h->mask |= USER_RECORD_PRIVILEGED;
+ return 0;
+}
+
+int user_record_set_password(UserRecord *h, char **password, bool prepend) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
+ _cleanup_(strv_free_erasep) char **e = NULL;
+ int r;
+
+ assert(h);
+
+ if (prepend) {
+ e = strv_copy(password);
+ if (!e)
+ return -ENOMEM;
+
+ r = strv_extend_strv(&e, h->password, true);
+ if (r < 0)
+ return r;
+
+ if (strv_equal(h->password, e))
+ return 0;
+
+ } else {
+ if (strv_equal(h->password, password))
+ return 0;
+
+ e = strv_copy(password);
+ if (!e)
+ return -ENOMEM;
+ }
+
+ r = json_variant_new_array_strv(&l, e);
+ if (r < 0)
+ return r;
+
+ json_variant_sensitive(l);
+
+ w = json_variant_ref(json_variant_by_key(h->json, "secret"));
+ r = json_variant_set_field(&w, "password", l);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&h->json, "secret", w);
+ if (r < 0)
+ return r;
+
+ strv_free_and_replace(h->password, e);
+
+ h->mask |= USER_RECORD_SECRET;
+ return 0;
+}
+
+int user_record_merge_secret(UserRecord *h, UserRecord *secret) {
+ assert(h);
+
+ /* Merges the secrets from 'secret' into 'h'. */
+
+ return user_record_set_password(h, secret->password, true);
+}
+
+int user_record_good_authentication(UserRecord *h) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+ char buf[SD_ID128_STRING_MAX];
+ uint64_t counter, usec;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ switch (h->good_authentication_counter) {
+ case UINT64_MAX:
+ counter = 1;
+ break;
+ case UINT64_MAX-1:
+ counter = h->good_authentication_counter; /* saturate */
+ break;
+ default:
+ counter = h->good_authentication_counter + 1;
+ break;
+ }
+
+ usec = now(CLOCK_REALTIME);
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ v = json_variant_ref(h->json);
+ w = json_variant_ref(json_variant_by_key(v, "status"));
+ z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+ r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&w, buf, z);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&v, "status", w);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(h->json);
+ h->json = TAKE_PTR(v);
+
+ h->good_authentication_counter = counter;
+ h->last_good_authentication_usec = usec;
+
+ h->mask |= USER_RECORD_STATUS;
+ return 0;
+}
+
+int user_record_bad_authentication(UserRecord *h) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+ char buf[SD_ID128_STRING_MAX];
+ uint64_t counter, usec;
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ switch (h->bad_authentication_counter) {
+ case UINT64_MAX:
+ counter = 1;
+ break;
+ case UINT64_MAX-1:
+ counter = h->bad_authentication_counter; /* saturate */
+ break;
+ default:
+ counter = h->bad_authentication_counter + 1;
+ break;
+ }
+
+ usec = now(CLOCK_REALTIME);
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ v = json_variant_ref(h->json);
+ w = json_variant_ref(json_variant_by_key(v, "status"));
+ z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+ r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&w, buf, z);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&v, "status", w);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(h->json);
+ h->json = TAKE_PTR(v);
+
+ h->bad_authentication_counter = counter;
+ h->last_bad_authentication_usec = usec;
+
+ h->mask |= USER_RECORD_STATUS;
+ return 0;
+}
+
+int user_record_ratelimit(UserRecord *h) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL;
+ usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count;
+ char buf[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+ int r;
+
+ assert(h);
+
+ usec = now(CLOCK_REALTIME);
+
+ if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec)
+ /* Hmm, time is running backwards? Say no! */
+ return 0;
+ else if (h->ratelimit_begin_usec == UINT64_MAX ||
+ usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) {
+ /* Fresh start */
+ new_ratelimit_begin_usec = usec;
+ new_ratelimit_count = 1;
+ } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) {
+ /* Count up */
+ new_ratelimit_begin_usec = h->ratelimit_begin_usec;
+ new_ratelimit_count = h->ratelimit_count + 1;
+ } else
+ /* Limit hit */
+ return 0;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ v = json_variant_ref(h->json);
+ w = json_variant_ref(json_variant_by_key(v, "status"));
+ z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf)));
+
+ r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&w, buf, z);
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&v, "status", w);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(h->json);
+ h->json = TAKE_PTR(v);
+
+ h->ratelimit_begin_usec = new_ratelimit_begin_usec;
+ h->ratelimit_count = new_ratelimit_count;
+
+ h->mask |= USER_RECORD_STATUS;
+ return 1;
+}
+
+int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
+ assert(hr);
+
+ if (hr->disposition >= 0 && hr->disposition != USER_REGULAR)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users.");
+
+ if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage.");
+
+ if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields.");
+
+ if (hr->service && !streq(hr->service, "io.systemd.Home"))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");
+
+ return 0;
+}
diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h
new file mode 100644
index 0000000000000..ce70b0938d461
--- /dev/null
+++ b/src/home/user-record-util.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "user-record.h"
+#include "group-record.h"
+
+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);
+
+typedef enum UserReconcileMode {
+ USER_RECONCILE_ANY,
+ USER_RECONCILE_REQUIRE_NEWER, /* host version must be newer than embedded version */
+ USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, /* similar, but may also be equal */
+ _USER_RECONCILE_MODE_MAX,
+ _USER_RECONCILE_MODE_INVALID = -1,
+} UserReconcileMode;
+
+enum { /* return values */
+ USER_RECONCILE_HOST_WON,
+ USER_RECONCILE_EMBEDDED_WON,
+ USER_RECONCILE_IDENTICAL,
+};
+
+int user_record_reconcile(UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret);
+int user_record_add_binding(UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const void *fscrypt_salt, size_t fscrypt_salt_size, const char *home_directory, uid_t uid, gid_t gid);
+
+/* Results of the two test functions below. */
+enum {
+ USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */
+ USER_TEST_ABSENT,
+ USER_TEST_EXISTS,
+ USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */
+ USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */
+};
+
+int user_record_test_home_directory(UserRecord *h);
+int user_record_test_home_directory_and_warn(UserRecord *h);
+int user_record_test_image_path(UserRecord *h);
+int user_record_test_image_path_and_warn(UserRecord *h);
+
+int user_record_test_secret(UserRecord *h, UserRecord *secret);
+
+int user_record_update_last_changed(UserRecord *h, bool with_password);
+int user_record_set_disk_size(UserRecord *h, uint64_t disk_size);
+int user_record_set_password(UserRecord *h, char **password, bool prepend);
+int user_record_make_hashed_password(UserRecord *h, UserRecord *secret);
+int user_record_merge_secret(UserRecord *h, UserRecord *secret);
+
+int user_record_good_authentication(UserRecord *h);
+int user_record_bad_authentication(UserRecord *h);
+int user_record_ratelimit(UserRecord *h);
+
+int user_record_is_supported(UserRecord *hr, sd_bus_error *error);
diff --git a/src/libsystemd/libsystemd.sym b/src/libsystemd/libsystemd.sym
index 5ec42e0f1f826..13cca2db6c46e 100644
--- a/src/libsystemd/libsystemd.sym
+++ b/src/libsystemd/libsystemd.sym
@@ -682,3 +682,8 @@ global:
sd_bus_object_vtable_format;
sd_event_source_disable_unref;
} LIBSYSTEMD_241;
+
+LIBSYSTEMD_244 {
+global:
+ sd_bus_message_sensitive;
+} LIBSYSTEMD_243;
diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c
index edd30bf84d8ad..8f7a79fb39b70 100644
--- a/src/libsystemd/sd-bus/bus-common-errors.c
+++ b/src/libsystemd/sd-bus/bus-common-errors.c
@@ -104,5 +104,28 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = {
SD_BUS_ERROR_MAP(BUS_ERROR_SPEED_METER_INACTIVE, EOPNOTSUPP),
+ SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_HOME, EEXIST),
+ SD_BUS_ERROR_MAP(BUS_ERROR_UID_IN_USE, EEXIST),
+ SD_BUS_ERROR_MAP(BUS_ERROR_USER_NAME_EXISTS, EEXIST),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_EXISTS, EEXIST),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_ACTIVE, EALREADY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_FIXATED, EALREADY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_UNFIXATED, EADDRNOTAVAIL),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_ACTIVE, EALREADY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ABSENT, EREMOTE),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_BUSY, EBUSY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD, ENOKEY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_LOW_PASSWORD_QUALITY, EUCLEAN),
+ SD_BUS_ERROR_MAP(BUS_ERROR_BAD_SIGNATURE, EKEYREJECTED),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_MISMATCH, EUCLEAN),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_DOWNGRADE, ESTALE),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_SIGNED, EROFS),
+ SD_BUS_ERROR_MAP(BUS_ERROR_BAD_HOME_SIZE, ERANGE),
+ SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRIVATE_KEY, ENOPKG),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_LOCKED, ENOEXEC),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_LOCKED, ENOEXEC),
+ SD_BUS_ERROR_MAP(BUS_ERROR_TOO_MANY_OPERATIONS, ENOBUFS),
+ SD_BUS_ERROR_MAP(BUS_ERROR_AUTHENTICATION_LIMIT_HIT, ETOOMANYREFS),
+
SD_BUS_ERROR_MAP_END
};
diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h
index 4a29b3bea8ecb..d3f002d75a7d5 100644
--- a/src/libsystemd/sd-bus/bus-common-errors.h
+++ b/src/libsystemd/sd-bus/bus-common-errors.h
@@ -83,4 +83,28 @@
#define BUS_ERROR_SPEED_METER_INACTIVE "org.freedesktop.network1.SpeedMeterInactive"
+#define BUS_ERROR_NO_SUCH_HOME "org.freedesktop.home1.NoSuchHome"
+#define BUS_ERROR_UID_IN_USE "org.freedesktop.home1.UIDInUse"
+#define BUS_ERROR_USER_NAME_EXISTS "org.freedesktop.home1.UserNameExists"
+#define BUS_ERROR_HOME_EXISTS "org.freedesktop.home1.HomeExists"
+#define BUS_ERROR_HOME_ALREADY_ACTIVE "org.freedesktop.home1.HomeAlreadyActive"
+#define BUS_ERROR_HOME_ALREADY_FIXATED "org.freedesktop.home1.HomeAlreadyFixated"
+#define BUS_ERROR_HOME_UNFIXATED "org.freedesktop.home1.HomeUnfixated"
+#define BUS_ERROR_HOME_NOT_ACTIVE "org.freedesktop.home1.HomeNotActive"
+#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent"
+#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy"
+#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword"
+#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality"
+#define BUS_ERROR_BAD_SIGNATURE "org.freedesktop.home1.BadSignature"
+#define BUS_ERROR_HOME_RECORD_MISMATCH "org.freedesktop.home1.RecordMismatch"
+#define BUS_ERROR_HOME_RECORD_DOWNGRADE "org.freedesktop.home1.RecordDowngrade"
+#define BUS_ERROR_HOME_RECORD_SIGNED "org.freedesktop.home1.RecordSigned"
+#define BUS_ERROR_BAD_HOME_SIZE "org.freedesktop.home1.BadHomeSize"
+#define BUS_ERROR_NO_PRIVATE_KEY "org.freedesktop.home1.NoPrivateKey"
+#define BUS_ERROR_HOME_LOCKED "org.freedesktop.home1.HomeLocked"
+#define BUS_ERROR_HOME_NOT_LOCKED "org.freedesktop.home1.HomeNotLocked"
+#define BUS_ERROR_NO_DISK_SPACE "org.freedesktop.home1.NoDiskSpace"
+#define BUS_ERROR_TOO_MANY_OPERATIONS "org.freedesktop.home1.TooManyOperations"
+#define BUS_ERROR_AUTHENTICATION_LIMIT_HIT "org.freedesktop.home1.AuthenticationLimitHit"
+
BUS_ERROR_MAP_ELF_USE(bus_common_errors);
diff --git a/src/libsystemd/sd-bus/bus-message.c b/src/libsystemd/sd-bus/bus-message.c
index eb029e4453268..0515064236a5d 100644
--- a/src/libsystemd/sd-bus/bus-message.c
+++ b/src/libsystemd/sd-bus/bus-message.c
@@ -45,12 +45,24 @@ static void message_free_part(sd_bus_message *m, struct bus_body_part *part) {
assert(m);
assert(part);
- if (part->memfd >= 0)
+ if (part->memfd >= 0) {
+ /* erase this requested, but ony if the memfd is not sealed yet, i.e. is writable */
+ if (m->sensitive && !m->sealed)
+ explicit_bzero_safe(part->data, part->size);
+
close_and_munmap(part->memfd, part->mmap_begin, part->mapped);
- else if (part->munmap_this)
+ } else if (part->munmap_this)
+ /* We don't erase sensitive data here, since the data is memory mapped from someone else, and
+ * we just don't know if it's OK to write to it */
munmap(part->mmap_begin, part->mapped);
- else if (part->free_this)
- free(part->data);
+ else {
+ /* Erase this if that is requested. Since this is regular memory we know we can write it. */
+ if (m->sensitive)
+ explicit_bzero_safe(part->data, part->size);
+
+ if (part->free_this)
+ free(part->data);
+ }
if (part != &m->body)
free(part);
@@ -113,11 +125,11 @@ static void message_reset_containers(sd_bus_message *m) {
static sd_bus_message* message_free(sd_bus_message *m) {
assert(m);
+ message_reset_parts(m);
+
if (m->free_header)
free(m->header);
- message_reset_parts(m);
-
/* Note that we don't unref m->bus here. That's already done by sd_bus_message_unref() as each user
* reference to the bus message also is considered a reference to the bus connection itself. */
@@ -727,6 +739,12 @@ static int message_new_reply(
t->dont_send = !!(call->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED);
t->enforced_reply_signature = call->enforced_reply_signature;
+ /* let's copy the sensitive flag over. Let's do that as a safety precaution to keep a transaction
+ * wholly sensitive if already the incoming message was sensitive. This is particularly useful when a
+ * vtable record sets the SD_BUS_VTABLE_SENSITIVE flag on a method call, since this means it applies
+ * to both the message call and the reply. */
+ t->sensitive = call->sensitive;
+
*m = TAKE_PTR(t);
return 0;
}
@@ -5919,3 +5937,10 @@ _public_ int sd_bus_message_set_priority(sd_bus_message *m, int64_t priority) {
m->priority = priority;
return 0;
}
+
+_public_ int sd_bus_message_sensitive(sd_bus_message *m) {
+ assert_return(m, -EINVAL);
+
+ m->sensitive = true;
+ return 0;
+}
diff --git a/src/libsystemd/sd-bus/bus-message.h b/src/libsystemd/sd-bus/bus-message.h
index ced0bb3d34a91..a88a531e15b82 100644
--- a/src/libsystemd/sd-bus/bus-message.h
+++ b/src/libsystemd/sd-bus/bus-message.h
@@ -85,6 +85,7 @@ struct sd_bus_message {
bool free_header:1;
bool free_fds:1;
bool poisoned:1;
+ bool sensitive:1;
/* The first and last bytes of the message */
struct bus_header *header;
diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c
index ae643cacc740f..474f8a6a1695c 100644
--- a/src/libsystemd/sd-bus/bus-objects.c
+++ b/src/libsystemd/sd-bus/bus-objects.c
@@ -353,6 +353,12 @@ static int method_callbacks_run(
if (require_fallback && !c->parent->is_fallback)
return 0;
+ if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) {
+ r = sd_bus_message_sensitive(m);
+ if (r < 0)
+ return r;
+ }
+
r = check_access(bus, m, c, &error);
if (r < 0)
return bus_maybe_reply_error(m, r, &error);
@@ -577,6 +583,12 @@ static int property_get_set_callbacks_run(
if (require_fallback && !c->parent->is_fallback)
return 0;
+ if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) {
+ r = sd_bus_message_sensitive(m);
+ if (r < 0)
+ return r;
+ }
+
r = vtable_property_get_userdata(bus, m->path, c, &u, &error);
if (r <= 0)
return bus_maybe_reply_error(m, r, &error);
@@ -591,6 +603,12 @@ static int property_get_set_callbacks_run(
if (r < 0)
return r;
+ if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) {
+ r = sd_bus_message_sensitive(reply);
+ if (r < 0)
+ return r;
+ }
+
if (is_get) {
/* Note that we do not protect against reexecution
* here (using the last_iteration check, see below),
@@ -692,6 +710,12 @@ static int vtable_append_one_property(
assert(c);
assert(v);
+ if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) {
+ r = sd_bus_message_sensitive(reply);
+ if (r < 0)
+ return r;
+ }
+
r = sd_bus_message_open_container(reply, 'e', "sv");
if (r < 0)
return r;
@@ -750,9 +774,18 @@ static int vtable_append_all_properties(
if (v->flags & SD_BUS_VTABLE_HIDDEN)
continue;
+ /* Let's not include properties marked as "explicit" in any message that contians a generic
+ * dump of properties, but only in those genrated as a response to an explicit request. */
if (v->flags & SD_BUS_VTABLE_PROPERTY_EXPLICIT)
continue;
+ /* Let's not include properties marked only for invalidation on change (i.e. in contrast to
+ * those whose new values are included in PropertiesChanges message) in any signals. This is
+ * useful to ensure they aren't included in InterfacesAdded messages. */
+ if (reply->header->type != SD_BUS_MESSAGE_METHOD_RETURN &&
+ FLAGS_SET(v->flags, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION))
+ continue;
+
r = vtable_append_one_property(bus, reply, path, c, v, userdata, error);
if (r < 0)
return r;
diff --git a/src/login/inhibit.c b/src/login/inhibit.c
index 25ff4d0be6428..b3b37fb595906 100644
--- a/src/login/inhibit.c
+++ b/src/login/inhibit.c
@@ -113,9 +113,9 @@ static int print_inhibitors(sd_bus *bus) {
r = table_add_many(table,
TABLE_STRING, who,
- TABLE_UINT32, uid,
+ TABLE_UID, (uid_t) uid,
TABLE_STRING, strna(u),
- TABLE_UINT32, pid,
+ TABLE_PID, (pid_t) pid,
TABLE_STRING, strna(comm),
TABLE_STRING, what,
TABLE_STRING, why,
diff --git a/src/login/loginctl.c b/src/login/loginctl.c
index 2ad9887066f6c..6e7d6600132f4 100644
--- a/src/login/loginctl.c
+++ b/src/login/loginctl.c
@@ -185,7 +185,7 @@ static int list_sessions(int argc, char *argv[], void *userdata) {
r = table_add_many(table,
TABLE_STRING, id,
- TABLE_UINT32, uid,
+ TABLE_UID, (uid_t) uid,
TABLE_STRING, user,
TABLE_STRING, seat,
TABLE_STRING, strna(tty));
@@ -244,7 +244,7 @@ static int list_users(int argc, char *argv[], void *userdata) {
break;
r = table_add_many(table,
- TABLE_UINT32, uid,
+ TABLE_UID, (uid_t) uid,
TABLE_STRING, user);
if (r < 0)
return log_error_errno(r, "Failed to add row to table: %m");
diff --git a/src/login/logind-core.c b/src/login/logind-core.c
index 1d21e90a2eaa9..7a29ccdf91ebd 100644
--- a/src/login/logind-core.c
+++ b/src/login/logind-core.c
@@ -28,6 +28,7 @@
#include "terminal-util.h"
#include "udev-util.h"
#include "user-util.h"
+#include "userdb.h"
void manager_reset_config(Manager *m) {
assert(m);
@@ -139,21 +140,18 @@ int manager_add_session(Manager *m, const char *id, Session **_session) {
int manager_add_user(
Manager *m,
- uid_t uid,
- gid_t gid,
- const char *name,
- const char *home,
+ UserRecord *ur,
User **_user) {
User *u;
int r;
assert(m);
- assert(name);
+ assert(ur);
- u = hashmap_get(m->users, UID_TO_PTR(uid));
+ u = hashmap_get(m->users, UID_TO_PTR(ur->uid));
if (!u) {
- r = user_new(&u, m, uid, gid, name, home);
+ r = user_new(&u, m, ur);
if (r < 0)
return r;
}
@@ -169,32 +167,35 @@ int manager_add_user_by_name(
const char *name,
User **_user) {
- const char *home = NULL;
- uid_t uid;
- gid_t gid;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int r;
assert(m);
assert(name);
- r = get_user_creds(&name, &uid, &gid, &home, NULL, 0);
+ r = userdb_by_name(name, 0, &ur);
if (r < 0)
return r;
- return manager_add_user(m, uid, gid, name, home, _user);
+ return manager_add_user(m, ur, _user);
}
-int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user) {
- struct passwd *p;
+int manager_add_user_by_uid(
+ Manager *m,
+ uid_t uid,
+ User **_user) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ int r;
assert(m);
+ assert(uid_is_valid(uid));
- errno = 0;
- p = getpwuid(uid);
- if (!p)
- return errno_or_else(ENOENT);
+ r = userdb_by_uid(uid, 0, &ur);
+ if (r < 0)
+ return r;
- return manager_add_user(m, uid, p->pw_gid, p->pw_name, p->pw_dir, _user);
+ return manager_add_user(m, ur, _user);
}
int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret) {
diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c
index b36616e55a03b..695e6dfbe21ea 100644
--- a/src/login/logind-dbus.c
+++ b/src/login/logind-dbus.c
@@ -540,8 +540,8 @@ static int method_list_sessions(sd_bus_message *message, void *userdata, sd_bus_
r = sd_bus_message_append(reply, "(susso)",
session->id,
- (uint32_t) session->user->uid,
- session->user->name,
+ (uint32_t) session->user->user_record->uid,
+ session->user->user_record->user_name,
session->seat ? session->seat->id : "",
p);
if (r < 0)
@@ -581,8 +581,8 @@ static int method_list_users(sd_bus_message *message, void *userdata, sd_bus_err
return -ENOMEM;
r = sd_bus_message_append(reply, "(uso)",
- (uint32_t) user->uid,
- user->name,
+ (uint32_t) user->user_record->uid,
+ user->user_record->user_name,
p);
if (r < 0)
return r;
@@ -1491,7 +1491,7 @@ static int have_multiple_sessions(
* count, and non-login sessions do not count either. */
HASHMAP_FOREACH(session, m->sessions, i)
if (session->class == SESSION_USER &&
- session->user->uid != uid)
+ session->user->user_record->uid != uid)
return true;
return false;
diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c
index 094fcd668dcb1..a2b4cac976d03 100644
--- a/src/login/logind-seat.c
+++ b/src/login/logind-seat.c
@@ -120,7 +120,7 @@ int seat_save(Seat *s) {
"ACTIVE=%s\n"
"ACTIVE_UID="UID_FMT"\n",
s->active->id,
- s->active->user->uid);
+ s->active->user->user_record->uid);
}
if (s->sessions) {
@@ -138,7 +138,7 @@ int seat_save(Seat *s) {
LIST_FOREACH(sessions_by_seat, i, s->sessions)
fprintf(f,
UID_FMT"%c",
- i->user->uid,
+ i->user->user_record->uid,
i->sessions_by_seat_next ? ' ' : '\n');
}
@@ -217,8 +217,8 @@ int seat_apply_acls(Seat *s, Session *old_active) {
r = devnode_acl_all(s->id,
false,
- !!old_active, old_active ? old_active->user->uid : 0,
- !!s->active, s->active ? s->active->user->uid : 0);
+ !!old_active, old_active ? old_active->user->user_record->uid : 0,
+ !!s->active, s->active ? s->active->user->user_record->uid : 0);
if (r < 0)
return log_error_errno(r, "Failed to apply ACLs: %m");
diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c
index a37bbf56b75ad..59e6fe40091fd 100644
--- a/src/login/logind-session-dbus.c
+++ b/src/login/logind-session-dbus.c
@@ -44,7 +44,7 @@ static int property_get_user(
if (!p)
return -ENOMEM;
- return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->uid, p);
+ return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->user_record->uid, p);
}
static int property_get_name(
@@ -62,7 +62,7 @@ static int property_get_name(
assert(reply);
assert(s);
- return sd_bus_message_append(reply, "s", s->user->name);
+ return sd_bus_message_append(reply, "s", s->user->user_record->user_name);
}
static int property_get_seat(
@@ -169,7 +169,7 @@ int bus_session_method_terminate(sd_bus_message *message, void *userdata, sd_bus
"org.freedesktop.login1.manage",
NULL,
false,
- s->user->uid,
+ s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@@ -211,7 +211,7 @@ int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_erro
"org.freedesktop.login1.lock-sessions",
NULL,
false,
- s->user->uid,
+ s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@@ -247,7 +247,7 @@ static int method_set_idle_hint(sd_bus_message *message, void *userdata, sd_bus_
if (r < 0)
return r;
- if (uid != 0 && uid != s->user->uid)
+ if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set idle hint");
session_set_idle_hint(s, b);
@@ -276,7 +276,7 @@ static int method_set_locked_hint(sd_bus_message *message, void *userdata, sd_bu
if (r < 0)
return r;
- if (uid != 0 && uid != s->user->uid)
+ if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set locked hint");
session_set_locked_hint(s, b);
@@ -315,7 +315,7 @@ int bus_session_method_kill(sd_bus_message *message, void *userdata, sd_bus_erro
"org.freedesktop.login1.manage",
NULL,
false,
- s->user->uid,
+ s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@@ -351,7 +351,7 @@ static int method_take_control(sd_bus_message *message, void *userdata, sd_bus_e
if (r < 0)
return r;
- if (uid != 0 && (force || uid != s->user->uid))
+ if (uid != 0 && (force || uid != s->user->user_record->uid))
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may take control");
r = session_set_controller(s, sd_bus_message_get_sender(message), force, true);
@@ -520,7 +520,7 @@ static int method_set_brightness(sd_bus_message *message, void *userdata, sd_bus
if (r < 0)
return r;
- if (uid != 0 && uid != s->user->uid)
+ if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change brightness.");
r = sd_device_new_from_subsystem_sysname(&d, subsystem, name);
@@ -821,7 +821,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) {
"session_fd=%d seat=%s vtnr=%u",
s->id,
p,
- (uint32_t) s->user->uid,
+ (uint32_t) s->user->user_record->uid,
s->user->runtime_path,
fifo_fd,
s->seat ? s->seat->id : "",
@@ -833,7 +833,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) {
p,
s->user->runtime_path,
fifo_fd,
- (uint32_t) s->user->uid,
+ (uint32_t) s->user->user_record->uid,
s->seat ? s->seat->id : "",
(uint32_t) s->vtnr,
false);
diff --git a/src/login/logind-session.c b/src/login/logind-session.c
index 7e8025a0ea22b..e962fe60e2410 100644
--- a/src/login/logind-session.c
+++ b/src/login/logind-session.c
@@ -231,8 +231,8 @@ int session_save(Session *s) {
"IS_DISPLAY=%i\n"
"STATE=%s\n"
"REMOTE=%i\n",
- s->user->uid,
- s->user->name,
+ s->user->user_record->uid,
+ s->user->user_record->user_name,
session_is_active(s),
s->user->display == s,
session_state_to_string(session_get_state(s)),
@@ -642,7 +642,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
if (!scope)
return log_oom();
- description = strjoina("Session ", s->id, " of user ", s->user->name);
+ description = strjoina("Session ", s->id, " of user ", s->user->user_record->user_name);
r = manager_start_scope(
s->manager,
@@ -658,7 +658,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
"systemd-user-sessions.service",
s->user->runtime_dir_service,
s->user->service),
- s->user->home,
+ user_record_home_directory(s->user->user_record),
properties,
error,
&s->scope_job);
@@ -699,9 +699,9 @@ int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) {
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR,
"SESSION_ID=%s", s->id,
- "USER_ID=%s", s->user->name,
+ "USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
- LOG_MESSAGE("New session %s of user %s.", s->id, s->user->name));
+ LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name));
if (!dual_timestamp_is_set(&s->timestamp))
dual_timestamp_get(&s->timestamp);
@@ -751,7 +751,10 @@ static int session_stop_scope(Session *s, bool force) {
s->scope_job = mfree(s->scope_job);
/* Optionally, let's kill everything that's left now. */
- if (force || manager_shall_kill(s->manager, s->user->name)) {
+ if (force ||
+ (s->user->user_record->kill_processes != 0 &&
+ (s->user->user_record->kill_processes > 0 ||
+ manager_shall_kill(s->manager, s->user->user_record->user_name)))) {
r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job);
if (r < 0) {
@@ -767,7 +770,7 @@ static int session_stop_scope(Session *s, bool force) {
* Session stop is quite significant on its own, let's log it. */
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"SESSION_ID=%s", s->id,
- "USER_ID=%s", s->user->name,
+ "USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id));
}
@@ -825,7 +828,7 @@ int session_finalize(Session *s) {
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR,
"SESSION_ID=%s", s->id,
- "USER_ID=%s", s->user->name,
+ "USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
LOG_MESSAGE("Removed session %s.", s->id));
@@ -1194,7 +1197,7 @@ int session_prepare_vt(Session *s) {
if (vt < 0)
return vt;
- r = fchown(vt, s->user->uid, -1);
+ r = fchown(vt, s->user->user_record->uid, -1);
if (r < 0) {
r = log_error_errno(errno,
"Cannot change owner of /dev/tty%u: %m",
diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c
index beb97362e7302..6af226d006e7e 100644
--- a/src/login/logind-user-dbus.c
+++ b/src/login/logind-user-dbus.c
@@ -16,6 +16,60 @@
#include "strv.h"
#include "user-util.h"
+static int property_get_uid(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ User *u = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(u);
+
+ return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->uid);
+}
+
+static int property_get_gid(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ User *u = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(u);
+
+ return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->gid);
+}
+
+static int property_get_name(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ User *u = userdata;
+
+ assert(bus);
+ assert(reply);
+ assert(u);
+
+ return sd_bus_message_append(reply, "s", u->user_record->user_name);
+}
+
static BUS_DEFINE_PROPERTY_GET2(property_get_state, "s", User, user_get_state, user_state_to_string);
static int property_get_display(
@@ -153,7 +207,7 @@ int bus_user_method_terminate(sd_bus_message *message, void *userdata, sd_bus_er
"org.freedesktop.login1.manage",
NULL,
false,
- u->uid,
+ u->user_record->uid,
&u->manager->polkit_registry,
error);
if (r < 0)
@@ -182,7 +236,7 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *
"org.freedesktop.login1.manage",
NULL,
false,
- u->uid,
+ u->user_record->uid,
&u->manager->polkit_registry,
error);
if (r < 0)
@@ -207,9 +261,9 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *
const sd_bus_vtable user_vtable[] = {
SD_BUS_VTABLE_START(0),
- SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(User, uid), SD_BUS_VTABLE_PROPERTY_CONST),
- SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(User, gid), SD_BUS_VTABLE_PROPERTY_CONST),
- SD_BUS_PROPERTY("Name", "s", NULL, offsetof(User, name), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("UID", "u", property_get_uid, 0, SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("GID", "u", property_get_gid, 0, SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Name", "s", property_get_name, 0, SD_BUS_VTABLE_PROPERTY_CONST),
BUS_PROPERTY_DUAL_TIMESTAMP("Timestamp", offsetof(User, timestamp), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RuntimePath", "s", NULL, offsetof(User, runtime_path), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Service", "s", NULL, offsetof(User, service), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -279,7 +333,7 @@ char *user_bus_path(User *u) {
assert(u);
- if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->uid) < 0)
+ if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->user_record->uid) < 0)
return NULL;
return s;
@@ -348,7 +402,7 @@ int user_send_signal(User *u, bool new_user) {
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
new_user ? "UserNew" : "UserRemoved",
- "uo", (uint32_t) u->uid, p);
+ "uo", (uint32_t) u->user_record->uid, p);
}
int user_send_changed(User *u, const char *properties, ...) {
diff --git a/src/login/logind-user.c b/src/login/logind-user.c
index b17fb2e3225e6..903b9f0cb84b8 100644
--- a/src/login/logind-user.c
+++ b/src/login/logind-user.c
@@ -38,10 +38,7 @@
int user_new(User **ret,
Manager *m,
- uid_t uid,
- gid_t gid,
- const char *name,
- const char *home) {
+ UserRecord *ur) {
_cleanup_(user_freep) User *u = NULL;
char lu[DECIMAL_STR_MAX(uid_t) + 1];
@@ -49,7 +46,13 @@ int user_new(User **ret,
assert(ret);
assert(m);
- assert(name);
+ assert(ur);
+
+ if (!ur->user_name)
+ return -EINVAL;
+
+ if (!uid_is_valid(ur->uid))
+ return -EINVAL;
u = new(User, 1);
if (!u)
@@ -57,28 +60,17 @@ int user_new(User **ret,
*u = (User) {
.manager = m,
- .uid = uid,
- .gid = gid,
+ .user_record = user_record_ref(ur),
.last_session_timestamp = USEC_INFINITY,
};
- u->name = strdup(name);
- if (!u->name)
- return -ENOMEM;
-
- u->home = strdup(home);
- if (!u->home)
+ if (asprintf(&u->state_file, "/run/systemd/users/" UID_FMT, ur->uid) < 0)
return -ENOMEM;
- path_simplify(u->home, true);
-
- if (asprintf(&u->state_file, "/run/systemd/users/"UID_FMT, uid) < 0)
- return -ENOMEM;
-
- if (asprintf(&u->runtime_path, "/run/user/"UID_FMT, uid) < 0)
+ if (asprintf(&u->runtime_path, "/run/user/" UID_FMT, ur->uid) < 0)
return -ENOMEM;
- xsprintf(lu, UID_FMT, uid);
+ xsprintf(lu, UID_FMT, ur->uid);
r = slice_build_subslice(SPECIAL_USER_SLICE, lu, &u->slice);
if (r < 0)
return r;
@@ -91,7 +83,7 @@ int user_new(User **ret,
if (r < 0)
return r;
- r = hashmap_put(m->users, UID_TO_PTR(uid), u);
+ r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u);
if (r < 0)
return r;
@@ -130,9 +122,9 @@ User *user_free(User *u) {
if (u->slice)
hashmap_remove_value(u->manager->user_units, u->slice, u);
- hashmap_remove_value(u->manager->users, UID_TO_PTR(u->uid), u);
+ hashmap_remove_value(u->manager->users, UID_TO_PTR(u->user_record->uid), u);
- (void) sd_event_source_unref(u->timer_event_source);
+ sd_event_source_unref(u->timer_event_source);
u->service_job = mfree(u->service_job);
@@ -141,8 +133,8 @@ User *user_free(User *u) {
u->slice = mfree(u->slice);
u->runtime_path = mfree(u->runtime_path);
u->state_file = mfree(u->state_file);
- u->name = mfree(u->name);
- u->home = mfree(u->home);
+
+ user_record_unref(u->user_record);
return mfree(u);
}
@@ -170,7 +162,7 @@ static int user_save_internal(User *u) {
"NAME=%s\n"
"STATE=%s\n" /* friendly user-facing state */
"STOPPING=%s\n", /* low-level state */
- u->name,
+ u->user_record->user_name,
user_state_to_string(user_get_state(u)),
yes_no(u->stopping));
@@ -364,6 +356,100 @@ static void user_start_service(User *u) {
"Failed to start user service '%s', ignoring: %s", u->service, bus_error_message(&error, r));
}
+static int update_slice_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ _cleanup_(user_record_unrefp) UserRecord *ur = userdata;
+
+ assert(m);
+ assert(ur);
+
+ if (sd_bus_message_is_method_error(m, NULL)) {
+ log_error_errno(sd_bus_message_get_errno(m),
+ "Failed to update slice of %s: %s",
+ ur->user_name,
+ sd_bus_message_get_error(m)->message);
+
+ return 0;
+ }
+
+ log_debug("Successfully set slice parameters of %s.", ur->user_name);
+ return 0;
+}
+
+static int user_update_slice(User *u) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ int r;
+
+ assert(u);
+
+ if (u->user_record->tasks_max == UINT64_MAX &&
+ u->user_record->memory_high == UINT64_MAX &&
+ u->user_record->memory_max == UINT64_MAX &&
+ u->user_record->cpu_weight == UINT64_MAX &&
+ u->user_record->io_weight == UINT64_MAX)
+ return 0;
+
+ r = sd_bus_message_new_method_call(
+ u->manager->bus,
+ &m,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "SetUnitProperties");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "sb", u->slice, true);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_open_container(m, 'a', "(sv)");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ if (u->user_record->tasks_max != UINT64_MAX) {
+ r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", u->user_record->tasks_max);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ if (u->user_record->memory_high != UINT64_MAX) {
+ r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", u->user_record->memory_max);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ if (u->user_record->memory_high != UINT64_MAX) {
+ r = sd_bus_message_append(m, "(sv)", "MemoryHigh", "t", u->user_record->memory_high);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ if (u->user_record->cpu_weight != UINT64_MAX) {
+ r = sd_bus_message_append(m, "(sv)", "CPUWeight", "t", u->user_record->cpu_weight);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ if (u->user_record->io_weight != UINT64_MAX) {
+ r = sd_bus_message_append(m, "(sv)", "IOWeight", "t", u->user_record->io_weight);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call_async(u->manager->bus, NULL, m, update_slice_callback, u->user_record, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change user slice properties: %m");
+
+ /* Ref the user record pointer, so that the slot keeps it pinned */
+ user_record_ref(u->user_record);
+
+ return 0;
+}
+
int user_start(User *u) {
assert(u);
@@ -377,12 +463,15 @@ int user_start(User *u) {
u->stopping = false;
if (!u->started)
- log_debug("Starting services for new user %s.", u->name);
+ log_debug("Starting services for new user %s.", u->user_record->user_name);
/* Save the user data so far, because pam_systemd will read the XDG_RUNTIME_DIR out of it while starting up
* systemd --user. We need to do user_save_internal() because we have not "officially" started yet. */
user_save_internal(u);
+ /* Set slice parameters */
+ (void) user_update_slice(u);
+
/* Start user@UID.service */
user_start_service(u);
@@ -461,7 +550,7 @@ int user_finalize(User *u) {
* done. This is called as a result of an earlier user_done() when all jobs are completed. */
if (u->started)
- log_debug("User %s logged out.", u->name);
+ log_debug("User %s logged out.", u->user_record->user_name);
LIST_FOREACH(sessions_by_user, s, u->sessions) {
k = session_finalize(s);
@@ -475,8 +564,8 @@ int user_finalize(User *u) {
* cases, as we shouldn't accidentally remove a system service's IPC objects while it is running, just because
* a cronjob running as the same user just finished. Hence: exclude system users generally from IPC clean-up,
* and do it only for normal users. */
- if (u->manager->remove_ipc && !uid_is_system(u->uid)) {
- k = clean_ipc_by_uid(u->uid);
+ if (u->manager->remove_ipc && !uid_is_system(u->user_record->uid)) {
+ k = clean_ipc_by_uid(u->user_record->uid);
if (k < 0)
r = k;
}
@@ -532,7 +621,7 @@ int user_check_linger_file(User *u) {
_cleanup_free_ char *cc = NULL;
char *p = NULL;
- cc = cescape(u->name);
+ cc = cescape(u->user_record->user_name);
if (!cc)
return -ENOMEM;
@@ -568,6 +657,18 @@ static bool user_unit_active(User *u) {
return false;
}
+static usec_t user_get_stop_delay(User *u) {
+ assert(u);
+
+ if (u->user_record->stop_delay_usec != UINT64_MAX)
+ return u->user_record->stop_delay_usec;
+
+ if (user_record_removable(u->user_record) > 0)
+ return 0; /* For removable users lower the stop delay to zero */
+
+ return u->manager->user_stop_delay;
+}
+
bool user_may_gc(User *u, bool drop_not_started) {
int r;
@@ -580,12 +681,16 @@ bool user_may_gc(User *u, bool drop_not_started) {
return false;
if (u->last_session_timestamp != USEC_INFINITY) {
+ usec_t user_stop_delay;
+
/* All sessions have been closed. Let's see if we shall leave the user record around for a bit */
- if (u->manager->user_stop_delay == USEC_INFINITY)
+ user_stop_delay = user_get_stop_delay(u);
+
+ if (user_stop_delay == USEC_INFINITY)
return false; /* Leave it around forever! */
- if (u->manager->user_stop_delay > 0 &&
- now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, u->manager->user_stop_delay))
+ if (user_stop_delay > 0 &&
+ now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, user_stop_delay))
return false; /* Leave it around for a bit longer. */
}
@@ -713,7 +818,7 @@ void user_elect_display(User *u) {
/* This elects a primary session for each user, which we call the "display". We try to keep the assignment
* stable, but we "upgrade" to better choices. */
- log_debug("Electing new display for user %s", u->name);
+ log_debug("Electing new display for user %s", u->user_record->user_name);
LIST_FOREACH(sessions_by_user, s, u->sessions) {
if (!elect_display_filter(s)) {
@@ -738,6 +843,7 @@ static int user_stop_timeout_callback(sd_event_source *es, uint64_t usec, void *
}
void user_update_last_session_timer(User *u) {
+ usec_t user_stop_delay;
int r;
assert(u);
@@ -756,7 +862,8 @@ void user_update_last_session_timer(User *u) {
assert(!u->timer_event_source);
- if (IN_SET(u->manager->user_stop_delay, 0, USEC_INFINITY))
+ user_stop_delay = user_get_stop_delay(u);
+ if (IN_SET(user_stop_delay, 0, USEC_INFINITY))
return;
if (sd_event_get_state(u->manager->event) == SD_EVENT_FINISHED) {
@@ -767,7 +874,7 @@ void user_update_last_session_timer(User *u) {
r = sd_event_add_time(u->manager->event,
&u->timer_event_source,
CLOCK_MONOTONIC,
- usec_add(u->last_session_timestamp, u->manager->user_stop_delay), 0,
+ usec_add(u->last_session_timestamp, user_stop_delay), 0,
user_stop_timeout_callback, u);
if (r < 0)
log_warning_errno(r, "Failed to enqueue user stop event source, ignoring: %m");
@@ -776,8 +883,8 @@ void user_update_last_session_timer(User *u) {
char s[FORMAT_TIMESPAN_MAX];
log_debug("Last session of user '%s' logged out, terminating user context in %s.",
- u->name,
- format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC));
+ u->user_record->user_name,
+ format_timespan(s, sizeof(s), user_stop_delay, USEC_PER_MSEC));
}
}
diff --git a/src/login/logind-user.h b/src/login/logind-user.h
index 4bd65d8373414..f8f172cb0f6d5 100644
--- a/src/login/logind-user.h
+++ b/src/login/logind-user.h
@@ -6,6 +6,7 @@ typedef struct User User;
#include "conf-parser.h"
#include "list.h"
#include "logind.h"
+#include "user-record.h"
typedef enum UserState {
USER_OFFLINE, /* Not logged in at all */
@@ -20,10 +21,9 @@ typedef enum UserState {
struct User {
Manager *manager;
- uid_t uid;
- gid_t gid;
- char *name;
- char *home;
+
+ UserRecord *user_record;
+
char *state_file;
char *runtime_path;
@@ -50,7 +50,7 @@ struct User {
LIST_FIELDS(User, gc_queue);
};
-int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name, const char *home);
+int user_new(User **out, Manager *m, UserRecord *ur);
User *user_free(User *u);
DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free);
diff --git a/src/login/logind.h b/src/login/logind.h
index f260f2dc96de8..65bcf7c5fbc9f 100644
--- a/src/login/logind.h
+++ b/src/login/logind.h
@@ -12,6 +12,7 @@
#include "list.h"
#include "set.h"
#include "time-util.h"
+#include "user-record.h"
typedef struct Manager Manager;
@@ -28,7 +29,7 @@ struct Manager {
Hashmap *seats;
Hashmap *sessions;
Hashmap *sessions_by_leader;
- Hashmap *users;
+ Hashmap *users; /* indexed by UID */
Hashmap *inhibitors;
Hashmap *buttons;
Hashmap *brightness_writers;
@@ -131,7 +132,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **_dev
int manager_add_button(Manager *m, const char *name, Button **_button);
int manager_add_seat(Manager *m, const char *id, Seat **_seat);
int manager_add_session(Manager *m, const char *id, Session **_session);
-int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **_user);
+int manager_add_user(Manager *m, UserRecord *ur, User **_user);
int manager_add_user_by_name(Manager *m, const char *name, User **_user);
int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user);
int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **_inhibitor);
diff --git a/src/login/org.freedesktop.login1.policy b/src/login/org.freedesktop.login1.policy
index 6dc79aa32aa4d..b9a0b68dfbf2c 100644
--- a/src/login/org.freedesktop.login1.policy
+++ b/src/login/org.freedesktop.login1.policy
@@ -2,16 +2,7 @@
-
+
diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c
index 3f762cbbc30b4..261b738564163 100644
--- a/src/login/pam_systemd.c
+++ b/src/login/pam_systemd.c
@@ -25,16 +25,24 @@
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
+#include "fs-util.h"
#include "hostname-util.h"
+#include "locale-util.h"
#include "login-util.h"
#include "macro.h"
+#include "pam-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "process-util.h"
+#include "rlimit-util.h"
#include "socket-util.h"
#include "stdio-util.h"
#include "strv.h"
#include "terminal-util.h"
+#include "user-util.h"
+#include "userdb.h"
+
+#define LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
static int parse_argv(
pam_handle_t *handle,
@@ -50,28 +58,30 @@ static int parse_argv(
assert(argc == 0 || argv);
for (i = 0; i < (unsigned) argc; i++) {
- if (startswith(argv[i], "class=")) {
+ const char *p;
+
+ if ((p = startswith(argv[i], "class="))) {
if (class)
- *class = argv[i] + 6;
+ *class = p;
- } else if (startswith(argv[i], "type=")) {
+ } else if ((p = startswith(argv[i], "type="))) {
if (type)
- *type = argv[i] + 5;
+ *type = p;
- } else if (startswith(argv[i], "desktop=")) {
+ } else if ((p = startswith(argv[i], "desktop="))) {
if (desktop)
- *desktop = argv[i] + 8;
+ *desktop = p;
} else if (streq(argv[i], "debug")) {
if (debug)
*debug = true;
- } else if (startswith(argv[i], "debug=")) {
+ } else if ((p = startswith(argv[i], "debug="))) {
int k;
- k = parse_boolean(argv[i] + 6);
+ k = parse_boolean(p);
if (k < 0)
- pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring.");
+ pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", p);
else if (debug)
*debug = k;
@@ -82,38 +92,93 @@ static int parse_argv(
return 0;
}
-static int get_user_data(
+static int acquire_user_record(
pam_handle_t *handle,
- const char **ret_username,
- struct passwd **ret_pw) {
+ UserRecord **ret_record) {
- const char *username = NULL;
- struct passwd *pw = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ const char *username = NULL, *json = NULL;
int r;
assert(handle);
- assert(ret_username);
- assert(ret_pw);
r = pam_get_user(handle, &username, NULL);
if (r != PAM_SUCCESS) {
- pam_syslog(handle, LOG_ERR, "Failed to get user name.");
+ pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r));
return r;
}
if (isempty(username)) {
pam_syslog(handle, LOG_ERR, "User name not valid.");
- return PAM_AUTH_ERR;
+ return PAM_SERVICE_ERR;
}
- pw = pam_modutil_getpwnam(handle, username);
- if (!pw) {
- pam_syslog(handle, LOG_ERR, "Failed to get user data.");
- return PAM_USER_UNKNOWN;
+ /* If pam_systemd_homed (or some other module) already acqired the user record we can reuse it
+ * here. */
+ r = pam_get_data(handle, "systemd-user-record", (const void**) &json);
+ if (r != PAM_SUCCESS || !json) {
+ _cleanup_free_ char *formatted = NULL;
+
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ /* Request the record ourselves */
+ r = userdb_by_name(username, 0, &ur);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to get user record: %s", strerror_safe(r));
+ return PAM_USER_UNKNOWN;
+ }
+
+ r = json_variant_format(ur->json, 0, &formatted);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to format user JSON: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ /* And cache it for everyone else */
+ r = pam_set_data(handle, "systemd-user-record", formatted, pam_cleanup_free);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ TAKE_PTR(formatted);
+ } else {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ /* Parse cached record */
+ r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ ur = user_record_new();
+ if (!ur)
+ return pam_log_oom(handle);
+
+ r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ /* Safety check if cached record actually matches what we are looking for */
+ if (!streq_ptr(username, ur->user_name)) {
+ pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name.");
+ return PAM_SERVICE_ERR;
+ }
}
- *ret_pw = pw;
- *ret_username = username;
+ if (!uid_is_valid(ur->uid)) {
+ pam_syslog(handle, LOG_ERR, "Acquired user record does not have a UID.");
+ return PAM_SERVICE_ERR;
+ }
+
+ if (ret_record)
+ *ret_record = TAKE_PTR(ur);
return PAM_SUCCESS;
}
@@ -229,17 +294,15 @@ static int export_legacy_dbus_address(
return PAM_SUCCESS;
if (asprintf(&t, DEFAULT_USER_BUS_ADDRESS_FMT, runtime) < 0)
- goto error;
+ return pam_log_oom(handle);
r = pam_misc_setenv(handle, "DBUS_SESSION_BUS_ADDRESS", t, 0);
- if (r != PAM_SUCCESS)
- goto error;
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set bus variable: %s", pam_strerror(handle, r));
+ return r;
+ }
return PAM_SUCCESS;
-
-error:
- pam_syslog(handle, LOG_ERR, "Failed to set bus variable.");
- return r;
}
static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) {
@@ -247,36 +310,36 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co
int r;
if (isempty(limit))
- return 0;
+ return PAM_SUCCESS;
if (streq(limit, "infinity")) {
r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", (uint64_t)-1);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return r;
- }
- } else {
- r = parse_permille(limit);
- if (r >= 0) {
- r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U));
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return r;
- }
- } else {
- r = parse_size(limit, 1024, &val);
- if (r >= 0) {
- r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return r;
- }
- } else
- pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max: %s, ignoring.", limit);
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ return PAM_SUCCESS;
}
- return 0;
+ r = parse_permille(limit);
+ if (r >= 0) {
+ r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U));
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ return PAM_SUCCESS;
+ }
+
+ r = parse_size(limit, 1024, &val);
+ if (r >= 0) {
+ r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val);
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
+
+ return PAM_SUCCESS;
+ }
+
+ pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit);
+ return PAM_SUCCESS;
}
static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) {
@@ -285,19 +348,17 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con
/* No need to parse "infinity" here, it will be set unconditionally later in manager_start_scope() */
if (isempty(limit) || streq(limit, "infinity"))
- return 0;
+ return PAM_SUCCESS;
r = safe_atou64(limit, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", val);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return r;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
} else
- pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max: %s, ignoring.", limit);
+ pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max, ignoring: %s", limit);
- return 0;
+ return PAM_SUCCESS;
}
static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, const char *limit, const char *field) {
@@ -305,21 +366,19 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con
int r;
if (isempty(limit))
- return 0;
+ return PAM_SUCCESS;
r = cg_weight_parse(limit, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", field, "t", val);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return r;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
} else if (streq(field, "CPUWeight"))
- pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight: %s, ignoring.", limit);
+ pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight, ignoring: %s", limit);
else
- pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight: %s, ignoring.", limit);
+ pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight, ignoring: %s", limit);
- return 0;
+ return PAM_SUCCESS;
}
static const char* getenv_harder(pam_handle_t *handle, const char *key, const char *fallback) {
@@ -362,7 +421,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char
r = pam_misc_setenv(handle, key, value, 0);
if (r != PAM_SUCCESS)
- pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s.", key);
+ pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s: %s", key, pam_strerror(handle, r));
return r;
}
@@ -370,6 +429,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char
static bool validate_runtime_directory(pam_handle_t *handle, const char *path, uid_t uid) {
struct stat st;
+ assert(handle);
assert(path);
/* Just some extra paranoia: let's not set $XDG_RUNTIME_DIR if the directory we'd set it to isn't actually set
@@ -397,6 +457,140 @@ static bool validate_runtime_directory(pam_handle_t *handle, const char *path, u
return false;
}
+static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) {
+ char **i;
+ int r;
+
+ assert(handle);
+ assert(ur);
+
+ if (ur->umask != MODE_INVALID) {
+ umask(ur->umask);
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "Set user umask to %04o, based on user record.", ur->umask);
+ }
+
+ STRV_FOREACH(i, ur->environment) {
+ _cleanup_free_ char *n = NULL;
+ const char *e;
+
+ assert_se(e = strchr(*i, '=')); /* environment was already validated while parsing JSON record, this thus must hold */
+
+ n = strndup(*i, e - *i);
+ if (!n)
+ return pam_log_oom(handle);
+
+ if (pam_getenv(handle, n)) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $%s already set, not changing based on record.", *i);
+ continue;
+ }
+
+ r = pam_putenv(handle, *i);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable %s set, based on user record.", *i);
+ }
+
+ if (ur->email_address) {
+ if (pam_getenv(handle, "EMAIL")) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL already set, not changing based on user record.");
+ } else {
+ _cleanup_free_ char *joined = NULL;
+
+ joined = strjoin("EMAIL=", ur->email_address);
+ if (!joined)
+ return pam_log_oom(handle);
+
+ r = pam_putenv(handle, joined);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL set, based on user record.");
+ }
+ }
+
+ if (ur->time_zone) {
+ if (pam_getenv(handle, "TZ")) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ already set, not changing based on user record.");
+ } else if (!timezone_is_valid(ur->time_zone, LOG_DEBUG)) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "Time zone specified in user record is not valid locally, not setting $TZ.");
+ } else {
+ _cleanup_free_ char *joined = NULL;
+
+ joined = strjoin("TZ=", ur->time_zone);
+ if (!joined)
+ return pam_log_oom(handle);
+
+ r = pam_putenv(handle, joined);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $TZ: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ set, based on user record.");
+ }
+ }
+
+ if (ur->preferred_language) {
+ if (pam_getenv(handle, "LANG")) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG already set, not changing based on user record.");
+ } else if (!locale_is_valid(ur->preferred_language)) {
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "Preferred language specified in user record is not valid locally, not setting $LANG.");
+ } else {
+ _cleanup_free_ char *joined = NULL;
+
+ joined = strjoin("LANG=", ur->preferred_language);
+ if (!joined)
+ return pam_log_oom(handle);
+
+ r = pam_putenv(handle, joined);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $LANG: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ if (debug)
+ pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG set, based on user record.");
+ }
+ }
+
+ if (nice_is_valid(ur->nice_level)) {
+ if (nice(ur->nice_level) < 0)
+ pam_syslog(handle, LOG_ERR, "Failed to set nice level to %i, ignoring: %s", ur->nice_level, strerror(errno));
+ else if (debug)
+ pam_syslog(handle, LOG_DEBUG, "Nice level set, based on user record.");
+ }
+
+ for (int rl = 0; rl < _RLIMIT_MAX; rl++) {
+
+ if (!ur->rlimits[rl])
+ continue;
+
+ r = setrlimit_closest(rl, ur->rlimits[rl]);
+ if (r < 0)
+ pam_syslog(handle, LOG_ERR, "Failed to set resource limit %s, ignoring: %s", rlimit_to_string(rl), strerror(errno));
+ else if (debug)
+ pam_syslog(handle, LOG_DEBUG, "Resource limit %s set, based on user record.", rlimit_to_string(rl));
+ }
+
+ return PAM_SUCCESS;
+}
+
_public_ PAM_EXTERN int pam_sm_open_session(
pam_handle_t *handle,
int flags,
@@ -405,7 +599,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
const char
- *username, *id, *object_path, *runtime_path,
+ *id, *object_path, *runtime_path,
*service = NULL,
*tty = NULL, *display = NULL,
*remote_user = NULL, *remote_host = NULL,
@@ -414,9 +608,9 @@ _public_ PAM_EXTERN int pam_sm_open_session(
*class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL,
*memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int session_fd = -1, existing, r;
bool debug = false, remote;
- struct passwd *pw;
uint32_t vtnr = 0;
uid_t original_uid;
@@ -437,11 +631,9 @@ _public_ PAM_EXTERN int pam_sm_open_session(
if (debug)
pam_syslog(handle, LOG_DEBUG, "pam-systemd initializing");
- r = get_user_data(handle, &username, &pw);
- if (r != PAM_SUCCESS) {
- pam_syslog(handle, LOG_ERR, "Failed to get user data.");
+ r = acquire_user_record(handle, &ur);
+ if (r != PAM_SUCCESS)
return r;
- }
/* Make sure we don't enter a loop by talking to
* systemd-logind when it is actually waiting for the
@@ -449,20 +641,24 @@ _public_ PAM_EXTERN int pam_sm_open_session(
* "systemd-user" we simply set XDG_RUNTIME_DIR and
* leave. */
- pam_get_item(handle, PAM_SERVICE, (const void**) &service);
+ (void) pam_get_item(handle, PAM_SERVICE, (const void**) &service);
if (streq_ptr(service, "systemd-user")) {
char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)];
- xsprintf(rt, "/run/user/"UID_FMT, pw->pw_uid);
- if (validate_runtime_directory(handle, rt, pw->pw_uid)) {
+ xsprintf(rt, "/run/user/"UID_FMT, ur->uid);
+ if (validate_runtime_directory(handle, rt, ur->uid)) {
r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0);
if (r != PAM_SUCCESS) {
- pam_syslog(handle, LOG_ERR, "Failed to set runtime dir.");
+ pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r));
return r;
}
}
- r = export_legacy_dbus_address(handle, pw->pw_uid, rt);
+ r = export_legacy_dbus_address(handle, ur->uid, rt);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = apply_user_record_settings(handle, ur, debug);
if (r != PAM_SUCCESS)
return r;
@@ -471,10 +667,10 @@ _public_ PAM_EXTERN int pam_sm_open_session(
/* Otherwise, we ask logind to create a session for us */
- pam_get_item(handle, PAM_XDISPLAY, (const void**) &display);
- pam_get_item(handle, PAM_TTY, (const void**) &tty);
- pam_get_item(handle, PAM_RUSER, (const void**) &remote_user);
- pam_get_item(handle, PAM_RHOST, (const void**) &remote_host);
+ (void) pam_get_item(handle, PAM_XDISPLAY, (const void**) &display);
+ (void) pam_get_item(handle, PAM_TTY, (const void**) &tty);
+ (void) pam_get_item(handle, PAM_RUSER, (const void**) &remote_user);
+ (void) pam_get_item(handle, PAM_RHOST, (const void**) &remote_host);
seat = getenv_harder(handle, "XDG_SEAT", NULL);
cvtnr = getenv_harder(handle, "XDG_VTNR", NULL);
@@ -548,16 +744,14 @@ _public_ PAM_EXTERN int pam_sm_open_session(
/* Talk to logind over the message bus */
- r = sd_bus_open_system(&bus);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
- return PAM_SESSION_ERR;
- }
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
if (debug) {
pam_syslog(handle, LOG_DEBUG, "Asking logind to create session: "
"uid="UID_FMT" pid="PID_FMT" service=%s type=%s class=%s desktop=%s seat=%s vtnr=%"PRIu32" tty=%s display=%s remote=%s remote_user=%s remote_host=%s",
- pw->pw_uid, getpid_cached(),
+ ur->uid, getpid_cached(),
strempty(service),
type, class, strempty(desktop),
strempty(seat), vtnr, strempty(tty), strempty(display),
@@ -574,13 +768,11 @@ _public_ PAM_EXTERN int pam_sm_open_session(
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"CreateSession");
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to create CreateSession method call: %s", strerror_safe(r));
- return PAM_SESSION_ERR;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
r = sd_bus_message_append(m, "uusssssussbss",
- (uint32_t) pw->pw_uid,
+ (uint32_t) ur->uid,
0,
service,
type,
@@ -593,40 +785,34 @@ _public_ PAM_EXTERN int pam_sm_open_session(
remote,
remote_user,
remote_host);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
- return PAM_SESSION_ERR;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
r = sd_bus_message_open_container(m, 'a', "(sv)");
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to open message container: %s", strerror_safe(r));
- return PAM_SYSTEM_ERR;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
r = append_session_memory_max(handle, m, memory_max);
if (r < 0)
- return PAM_SESSION_ERR;
+ return r;
r = append_session_tasks_max(handle, m, tasks_max);
if (r < 0)
- return PAM_SESSION_ERR;
+ return r;
r = append_session_cg_weight(handle, m, cpu_weight, "CPUWeight");
if (r < 0)
- return PAM_SESSION_ERR;
+ return r;
r = append_session_cg_weight(handle, m, io_weight, "IOWeight");
if (r < 0)
- return PAM_SESSION_ERR;
+ return r;
r = sd_bus_message_close_container(m);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to close message container: %s", strerror_safe(r));
- return PAM_SYSTEM_ERR;
- }
+ if (r < 0)
+ return pam_bus_log_create_error(handle, r);
- r = sd_bus_call(bus, m, 0, &error, &reply);
+ r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
if (r < 0) {
if (sd_bus_error_has_name(&error, BUS_ERROR_SESSION_BUSY)) {
if (debug)
@@ -634,7 +820,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
return PAM_SUCCESS;
} else {
pam_syslog(handle, LOG_ERR, "Failed to create session: %s", bus_error_message(&error, r));
- return PAM_SYSTEM_ERR;
+ return PAM_SESSION_ERR;
}
}
@@ -648,10 +834,8 @@ _public_ PAM_EXTERN int pam_sm_open_session(
&seat,
&vtnr,
&existing);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to parse message: %s", strerror_safe(r));
- return PAM_SESSION_ERR;
- }
+ if (r < 0)
+ return pam_bus_log_parse_error(handle, r);
if (debug)
pam_syslog(handle, LOG_DEBUG, "Reply from logind: "
@@ -662,20 +846,20 @@ _public_ PAM_EXTERN int pam_sm_open_session(
if (r != PAM_SUCCESS)
return r;
- if (original_uid == pw->pw_uid) {
+ if (original_uid == ur->uid) {
/* Don't set $XDG_RUNTIME_DIR if the user we now
* authenticated for does not match the original user
* of the session. We do this in order not to result
* in privileged apps clobbering the runtime directory
* unnecessarily. */
- if (validate_runtime_directory(handle, runtime_path, pw->pw_uid)) {
+ if (validate_runtime_directory(handle, runtime_path, ur->uid)) {
r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_path);
if (r != PAM_SUCCESS)
return r;
}
- r = export_legacy_dbus_address(handle, pw->pw_uid, runtime_path);
+ r = export_legacy_dbus_address(handle, ur->uid, runtime_path);
if (r != PAM_SUCCESS)
return r;
}
@@ -711,7 +895,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
r = pam_set_data(handle, "systemd.existing", INT_TO_PTR(!!existing), NULL);
if (r != PAM_SUCCESS) {
- pam_syslog(handle, LOG_ERR, "Failed to install existing flag.");
+ pam_syslog(handle, LOG_ERR, "Failed to install existing flag: %s", pam_strerror(handle, r));
return r;
}
@@ -724,12 +908,20 @@ _public_ PAM_EXTERN int pam_sm_open_session(
r = pam_set_data(handle, "systemd.session-fd", FD_TO_PTR(session_fd), NULL);
if (r != PAM_SUCCESS) {
- pam_syslog(handle, LOG_ERR, "Failed to install session fd.");
+ pam_syslog(handle, LOG_ERR, "Failed to install session fd: %s", pam_strerror(handle, r));
safe_close(session_fd);
return r;
}
}
+ r = apply_user_record_settings(handle, ur, debug);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are
+ * not going to process the bus connection in that time, so let's better close before the daemon
+ * kicks us off because we are not processing anything. */
+ (void) pam_release_bus_connection(handle);
return PAM_SUCCESS;
}
@@ -738,8 +930,6 @@ _public_ PAM_EXTERN int pam_sm_close_session(
int flags,
int argc, const char **argv) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
const void *existing = NULL;
const char *id;
int r;
@@ -752,17 +942,15 @@ _public_ PAM_EXTERN int pam_sm_close_session(
id = pam_getenv(handle, "XDG_SESSION_ID");
if (id && !existing) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- /* Before we go and close the FIFO we need to tell
- * logind that this is a clean session shutdown, so
- * that it doesn't just go and slaughter us
- * immediately after closing the fd */
+ /* Before we go and close the FIFO we need to tell logind that this is a clean session
+ * shutdown, so that it doesn't just go and slaughter us immediately after closing the fd */
- r = sd_bus_open_system(&bus);
- if (r < 0) {
- pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
- return PAM_SESSION_ERR;
- }
+ r = pam_acquire_bus_connection(handle, &bus);
+ if (r != PAM_SUCCESS)
+ return r;
r = sd_bus_call_method(bus,
"org.freedesktop.login1",
@@ -779,11 +967,9 @@ _public_ PAM_EXTERN int pam_sm_close_session(
}
}
- /* Note that we are knowingly leaking the FIFO fd here. This
- * way, logind can watch us die. If we closed it here it would
- * not have any clue when that is completed. Given that one
- * cannot really have multiple PAM sessions open from the same
- * process this means we will leak one FD at max. */
+ /* Note that we are knowingly leaking the FIFO fd here. This way, logind can watch us die. If we
+ * closed it here it would not have any clue when that is completed. Given that one cannot really
+ * have multiple PAM sessions open from the same process this means we will leak one FD at max. */
return PAM_SUCCESS;
}
diff --git a/src/network/netdev/macsec.c b/src/network/netdev/macsec.c
index cf281e75a6d45..56240b9586a22 100644
--- a/src/network/netdev/macsec.c
+++ b/src/network/netdev/macsec.c
@@ -981,7 +981,7 @@ static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) {
if (!sa->key_file)
return 0;
- r = read_full_file_full(sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len);
+ r = read_full_file_full(AT_FDCWD, sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len);
if (r < 0)
return log_netdev_error_errno(netdev, r,
"Failed to read key from '%s', ignoring: %m",
diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c
index 913ee2a058977..38f47a36f8ea1 100644
--- a/src/network/netdev/wireguard.c
+++ b/src/network/netdev/wireguard.c
@@ -901,7 +901,7 @@ static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_
assert(dest);
- r = read_full_file_full(filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len);
+ r = read_full_file_full(AT_FDCWD, filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len);
if (r < 0)
return r;
diff --git a/src/nspawn/nspawn-oci.c b/src/nspawn/nspawn-oci.c
index 4519c74b95b35..782c03c539bc6 100644
--- a/src/nspawn/nspawn-oci.c
+++ b/src/nspawn/nspawn-oci.c
@@ -172,24 +172,13 @@ static int oci_env(const char *name, JsonVariant *v, JsonDispatchFlags flags, vo
static int oci_args(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) {
_cleanup_strv_free_ char **l = NULL;
char ***value = userdata;
- JsonVariant *e;
int r;
assert(value);
- JSON_VARIANT_ARRAY_FOREACH(e, v) {
- const char *n;
-
- if (!json_variant_is_string(e))
- return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL),
- "Argument is not a string.");
-
- assert_se(n = json_variant_string(e));
-
- r = strv_extend(&l, n);
- if (r < 0)
- return log_oom();
- }
+ r = json_variant_strv(v, &l);
+ if (r < 0)
+ return json_log(v, flags, r, "Cannot parse arguments as list of strings: %m");
if (strv_isempty(l))
return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL),
@@ -2214,7 +2203,7 @@ int oci_load(FILE *f, const char *bundle, Settings **ret) {
path = strjoina(bundle, "/config.json");
- r = json_parse_file(f, path, &oci, &line, &column);
+ r = json_parse_file(f, path, 0, &oci, &line, &column);
if (r < 0) {
if (line != 0 && column != 0)
return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", path, line, column);
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 2aec8041f0079..a96ee1ccc2af7 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -4967,7 +4967,7 @@ static int run(int argc, char *argv[]) {
goto finish;
}
- r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, &loop);
+ r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &loop);
if (r < 0) {
log_error_errno(r, "Failed to set up loopback block device: %m");
goto finish;
diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c
index 8ef1cd5ea9e74..ae95bba78318b 100644
--- a/src/nss-systemd/nss-systemd.c
+++ b/src/nss-systemd/nss-systemd.c
@@ -3,28 +3,17 @@
#include
#include
-#include "sd-bus.h"
-
-#include "alloc-util.h"
-#include "bus-common-errors.h"
-#include "dirent-util.h"
#include "env-util.h"
+#include "errno-util.h"
#include "fd-util.h"
-#include "format-util.h"
-#include "fs-util.h"
-#include "list.h"
+#include "group-record-nss.h"
#include "macro.h"
#include "nss-util.h"
#include "signal-util.h"
-#include "stdio-util.h"
-#include "string-util.h"
+#include "strv.h"
#include "user-util.h"
-#include "util.h"
-
-#define DYNAMIC_USER_GECOS "Dynamic User"
-#define DYNAMIC_USER_PASSWD "*" /* locked */
-#define DYNAMIC_USER_DIR "/"
-#define DYNAMIC_USER_SHELL NOLOGIN
+#include "userdb-glue.h"
+#include "userdb.h"
static const struct passwd root_passwd = {
.pw_name = (char*) "root",
@@ -60,78 +49,33 @@ static const struct group nobody_group = {
.gr_mem = (char*[]) { NULL },
};
-typedef struct UserEntry UserEntry;
-typedef struct GetentData GetentData;
+typedef struct GetentData {
+ /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really reentrant since it
+ * shares the reading position in the stream with all other threads', we need to protect the data in
+ * UserDBIterator from multithreaded programs which may call setpwent(), getpwent_r(), or endpwent()
+ * simultaneously. So, each function locks the data by using the mutex below. */
+ pthread_mutex_t mutex;
+ UserDBIterator *iterator;
-struct UserEntry {
- uid_t id;
- char *name;
+ /* Applies to group iterations only: this is false while we iterate through groups natviely
+ * defined. It's true when we iterate through all memberships that extend groups that are
+ * non-natively (i.e. in NSS defined). */
+ bool by_membership;
+} GetentData;
- GetentData *data;
- LIST_FIELDS(UserEntry, entries);
+static GetentData getpwent_data = {
+ .mutex = PTHREAD_MUTEX_INITIALIZER
};
-struct GetentData {
- /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really
- * reentrant since it shares the reading position in the stream with all other threads',
- * we need to protect the data in UserEntry from multithreaded programs which may call
- * setpwent(), getpwent_r(), or endpwent() simultaneously. So, each function locks the
- * data by using the mutex below. */
- pthread_mutex_t mutex;
-
- UserEntry *position;
- LIST_HEAD(UserEntry, entries);
+static GetentData getgrent_data = {
+ .mutex = PTHREAD_MUTEX_INITIALIZER
};
-static GetentData getpwent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL };
-static GetentData getgrent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL };
-
NSS_GETPW_PROTOTYPES(systemd);
NSS_GETGR_PROTOTYPES(systemd);
-enum nss_status _nss_systemd_endpwent(void) _public_;
-enum nss_status _nss_systemd_setpwent(int stayopen) _public_;
-enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) _public_;
-enum nss_status _nss_systemd_endgrent(void) _public_;
-enum nss_status _nss_systemd_setgrent(int stayopen) _public_;
-enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) _public_;
-
-static int direct_lookup_name(const char *name, uid_t *ret) {
- _cleanup_free_ char *s = NULL;
- const char *path;
- int r;
-
- assert(name);
-
- /* Normally, we go via the bus to resolve names. That has the benefit that it is available from any mount
- * namespace and subject to proper authentication. However, there's one problem: if our module is called from
- * dbus-daemon itself we really can't use D-Bus to communicate. In this case, resort to a client-side hack,
- * and look for the dynamic names directly. This is pretty ugly, but breaks the cyclic dependency. */
-
- path = strjoina("/run/systemd/dynamic-uid/direct:", name);
- r = readlink_malloc(path, &s);
- if (r < 0)
- return r;
-
- return parse_uid(s, ret);
-}
-
-static int direct_lookup_uid(uid_t uid, char **ret) {
- char path[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t) + 1], *s;
- int r;
-
- xsprintf(path, "/run/systemd/dynamic-uid/direct:" UID_FMT, uid);
-
- r = readlink_malloc(path, &s);
- if (r < 0)
- return r;
- if (!valid_user_group_name(s)) { /* extra safety check */
- free(s);
- return -EINVAL;
- }
-
- *ret = s;
- return 0;
-}
+NSS_PWENT_PROTOTYPES(systemd);
+NSS_GRENT_PROTOTYPES(systemd);
+NSS_INITGROUPS_PROTOTYPE(systemd);
enum nss_status _nss_systemd_getpwnam_r(
const char *name,
@@ -139,99 +83,49 @@ enum nss_status _nss_systemd_getpwnam_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- uint32_t translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(name);
assert(pwd);
+ assert(errnop);
- /* If the username is not valid, then we don't know it. Ideally libc would filter these for us anyway. We don't
- * generate EINVAL here, because it isn't really out business to complain about invalid user names. */
+ /* If the username is not valid, then we don't know it. Ideally libc would filter these for us
+ * anyway. We don't generate EINVAL here, because it isn't really out business to complain about
+ * invalid user names. */
if (!valid_user_group_name(name))
return NSS_STATUS_NOTFOUND;
/* Synthesize entries for the root and nobody users, in case they are missing in /etc/passwd */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (streq(name, root_passwd.pw_name)) {
*pwd = root_passwd;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- streq(name, nobody_passwd.pw_name)) {
- *pwd = nobody_passwd;
- return NSS_STATUS_SUCCESS;
- }
- }
-
- /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_name(name, (uid_t*) &translated);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByName",
- &error,
- &reply,
- "s",
- name);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
+ if (streq(name, nobody_passwd.pw_name)) {
+ if (!synthesize_nobody())
return NSS_STATUS_NOTFOUND;
- goto fail;
+ *pwd = nobody_passwd;
+ return NSS_STATUS_SUCCESS;
}
- r = sd_bus_message_read(reply, "u", &translated);
- if (r < 0)
- goto fail;
- }
+ } else if (STR_IN_SET(name, root_passwd.pw_name, nobody_passwd.pw_name))
+ return NSS_STATUS_NOTFOUND;
- l = strlen(name);
- if (buflen < l+1) {
+ status = userdb_getpwnam(name, pwd, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = -e;
+ return status;
}
- memcpy(buffer, name, l+1);
-
- pwd->pw_name = buffer;
- pwd->pw_uid = (uid_t) translated;
- pwd->pw_gid = (uid_t) translated;
- pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- pwd->pw_dir = (char*) DYNAMIC_USER_DIR;
- pwd->pw_shell = (char*) DYNAMIC_USER_SHELL;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
enum nss_status _nss_systemd_getpwuid_r(
@@ -240,100 +134,45 @@ enum nss_status _nss_systemd_getpwuid_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_free_ char *direct = NULL;
- const char *translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(pwd);
+ assert(errnop);
+
if (!uid_is_valid(uid))
return NSS_STATUS_NOTFOUND;
/* Synthesize data for the root user and for nobody in case they are missing from /etc/passwd */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (uid == root_passwd.pw_uid) {
*pwd = root_passwd;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- uid == nobody_passwd.pw_uid) {
- *pwd = nobody_passwd;
- return NSS_STATUS_SUCCESS;
- }
- }
- if (!uid_is_dynamic(uid))
- return NSS_STATUS_NOTFOUND;
-
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_uid(uid, &direct);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
-
- translated = direct;
-
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByUID",
- &error,
- &reply,
- "u",
- (uint32_t) uid);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
+ if (uid == nobody_passwd.pw_uid) {
+ if (!synthesize_nobody())
return NSS_STATUS_NOTFOUND;
- goto fail;
+ *pwd = nobody_passwd;
+ return NSS_STATUS_SUCCESS;
}
- r = sd_bus_message_read(reply, "s", &translated);
- if (r < 0)
- goto fail;
- }
+ } else if (uid == root_passwd.pw_uid || uid == nobody_passwd.pw_uid)
+ return NSS_STATUS_NOTFOUND;
- l = strlen(translated) + 1;
- if (buflen < l) {
+ status = userdb_getpwuid(uid, pwd, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = -e;
+ return status;
}
- memcpy(buffer, translated, l);
-
- pwd->pw_name = buffer;
- pwd->pw_uid = uid;
- pwd->pw_gid = uid;
- pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- pwd->pw_dir = (char*) DYNAMIC_USER_DIR;
- pwd->pw_shell = (char*) DYNAMIC_USER_SHELL;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
#pragma GCC diagnostic ignored "-Wsizeof-pointer-memaccess"
@@ -344,94 +183,46 @@ enum nss_status _nss_systemd_getgrnam_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- uint32_t translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(name);
assert(gr);
+ assert(errnop);
if (!valid_user_group_name(name))
return NSS_STATUS_NOTFOUND;
/* Synthesize records for root and nobody, in case they are missing form /etc/group */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (streq(name, root_group.gr_name)) {
*gr = root_group;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- streq(name, nobody_group.gr_name)) {
- *gr = nobody_group;
- return NSS_STATUS_SUCCESS;
- }
- }
-
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
- if (bypass > 0) {
- r = direct_lookup_name(name, (uid_t*) &translated);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByName",
- &error,
- &reply,
- "s",
- name);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
+ if (streq(name, nobody_group.gr_name)) {
+ if (!synthesize_nobody())
return NSS_STATUS_NOTFOUND;
- goto fail;
+ *gr = nobody_group;
+ return NSS_STATUS_SUCCESS;
}
- r = sd_bus_message_read(reply, "u", &translated);
- if (r < 0)
- goto fail;
- }
+ } else if (STR_IN_SET(name, root_group.gr_name, nobody_group.gr_name))
+ return NSS_STATUS_NOTFOUND;
- l = sizeof(char*) + strlen(name) + 1;
- if (buflen < l) {
+ status = userdb_getgrnam(name, gr, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = -e;
+ return status;
}
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), name);
-
- gr->gr_name = buffer + sizeof(char*);
- gr->gr_gid = (gid_t) translated;
- gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- gr->gr_mem = (char**) buffer;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
enum nss_status _nss_systemd_getgrgid_r(
@@ -440,154 +231,56 @@ enum nss_status _nss_systemd_getgrgid_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_free_ char *direct = NULL;
- const char *translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(gr);
+ assert(errnop);
+
if (!gid_is_valid(gid))
return NSS_STATUS_NOTFOUND;
/* Synthesize records for root and nobody, in case they are missing from /etc/group */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (gid == root_group.gr_gid) {
*gr = root_group;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- gid == nobody_group.gr_gid) {
- *gr = nobody_group;
- return NSS_STATUS_SUCCESS;
- }
- }
-
- if (!gid_is_dynamic(gid))
- return NSS_STATUS_NOTFOUND;
-
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
- if (bypass > 0) {
- r = direct_lookup_uid(gid, &direct);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
-
- translated = direct;
-
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByUID",
- &error,
- &reply,
- "u",
- (uint32_t) gid);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
+ if (gid == nobody_group.gr_gid) {
+ if (!synthesize_nobody())
return NSS_STATUS_NOTFOUND;
- goto fail;
+ *gr = nobody_group;
+ return NSS_STATUS_SUCCESS;
}
- r = sd_bus_message_read(reply, "s", &translated);
- if (r < 0)
- goto fail;
- }
+ } else if (gid == root_group.gr_gid || gid == nobody_group.gr_gid)
+ return NSS_STATUS_NOTFOUND;
- l = sizeof(char*) + strlen(translated) + 1;
- if (buflen < l) {
+ status = userdb_getgrgid(gid, gr, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = -e;
+ return status;
}
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), translated);
-
- gr->gr_name = buffer + sizeof(char*);
- gr->gr_gid = gid;
- gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- gr->gr_mem = (char**) buffer;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
-}
-
-static void user_entry_free(UserEntry *p) {
- if (!p)
- return;
-
- if (p->data)
- LIST_REMOVE(entries, p->data->entries, p);
-
- free(p->name);
- free(p);
-}
-
-static int user_entry_add(GetentData *data, const char *name, uid_t id) {
- UserEntry *p;
-
- assert(data);
-
- /* This happens when User= or Group= already exists statically. */
- if (!uid_is_dynamic(id))
- return -EINVAL;
-
- p = new0(UserEntry, 1);
- if (!p)
- return -ENOMEM;
-
- p->name = strdup(name);
- if (!p->name) {
- free(p);
- return -ENOMEM;
- }
- p->id = id;
- p->data = data;
-
- LIST_PREPEND(entries, data->entries, p);
-
- return 0;
-}
-
-static void systemd_endent(GetentData *data) {
- UserEntry *p;
-
- assert(data);
-
- while ((p = data->entries))
- user_entry_free(p);
-
- data->position = NULL;
+ return status;
}
static enum nss_status nss_systemd_endent(GetentData *p) {
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(p);
+
assert_se(pthread_mutex_lock(&p->mutex) == 0);
- systemd_endent(p);
+ p->iterator = userdb_iterator_free(p->iterator);
+ p->by_membership = false;
assert_se(pthread_mutex_unlock(&p->mutex) == 0);
return NSS_STATUS_SUCCESS;
@@ -601,235 +294,362 @@ enum nss_status _nss_systemd_endgrent(void) {
return nss_systemd_endent(&getgrent_data);
}
-static int direct_enumeration(GetentData *p) {
- _cleanup_closedir_ DIR *d = NULL;
- struct dirent *de;
- int r;
+enum nss_status _nss_systemd_setpwent(int stayopen) {
+ enum nss_status ret;
- assert(p);
+ PROTECT_ERRNO;
+ BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
- d = opendir("/run/systemd/dynamic-uid/");
- if (!d)
- return -errno;
+ if (userdb_nss_compat_is_enabled() <= 0)
+ return NSS_STATUS_UNAVAIL;
- FOREACH_DIRENT(de, d, return -errno) {
- _cleanup_free_ char *name = NULL;
- uid_t uid, verified;
+ assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0);
- if (!dirent_is_file(de))
- continue;
+ getpwent_data.iterator = userdb_iterator_free(getpwent_data.iterator);
+ getpwent_data.by_membership = false;
- r = parse_uid(de->d_name, &uid);
- if (r < 0)
- continue;
+ ret = userdb_all(nss_glue_userdb_flags(), &getpwent_data.iterator) < 0 ?
+ NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS;
- r = direct_lookup_uid(uid, &name);
- if (r == -ENOMEM)
- return r;
- if (r < 0)
- continue;
+ assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0);
+ return ret;
+}
- r = direct_lookup_name(name, &verified);
- if (r < 0)
- continue;
+enum nss_status _nss_systemd_setgrent(int stayopen) {
+ enum nss_status ret;
- if (uid != verified)
- continue;
+ PROTECT_ERRNO;
+ BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
- r = user_entry_add(p, name, uid);
- if (r == -ENOMEM)
- return r;
- if (r < 0)
- continue;
- }
+ if (userdb_nss_compat_is_enabled() <= 0)
+ return NSS_STATUS_UNAVAIL;
+
+ assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0);
+
+ getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator);
+ getpwent_data.by_membership = false;
+
+ ret = groupdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator) < 0 ?
+ NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS;
- return 0;
+ assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0);
+ return ret;
}
-static enum nss_status systemd_setent(GetentData *p) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- const char *name;
- uid_t id;
- int bypass, r;
+enum nss_status _nss_systemd_getpwent_r(
+ struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ enum nss_status ret;
+ int r;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
- assert(p);
+ assert(result);
+ assert(errnop);
- assert_se(pthread_mutex_lock(&p->mutex) == 0);
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
- systemd_endent(p);
+ assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0);
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
+ if (!getpwent_data.iterator) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ ret = NSS_STATUS_UNAVAIL;
goto finish;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
-
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
}
- if (bypass > 0) {
- r = direct_enumeration(p);
- if (r < 0)
- goto fail;
-
+ r = userdb_iterator_get(getpwent_data.iterator, &ur);
+ if (r == -ESRCH) {
+ ret = NSS_STATUS_NOTFOUND;
+ goto finish;
+ }
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
goto finish;
}
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "GetDynamicUsers",
- &error,
- &reply,
- NULL);
- if (r < 0)
- goto fail;
-
- r = sd_bus_message_enter_container(reply, 'a', "(us)");
- if (r < 0)
- goto fail;
-
- while ((r = sd_bus_message_read(reply, "(us)", &id, &name)) > 0) {
- r = user_entry_add(p, name, id);
- if (r == -ENOMEM)
- goto fail;
- if (r < 0)
- continue;
+ r = nss_pack_user_record(ur, result, buffer, buflen);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_TRYAGAIN;
+ goto finish;
}
- if (r < 0)
- goto fail;
- r = sd_bus_message_exit_container(reply);
- if (r < 0)
- goto fail;
+ ret = NSS_STATUS_SUCCESS;
finish:
- p->position = p->entries;
- assert_se(pthread_mutex_unlock(&p->mutex) == 0);
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- systemd_endent(p);
- assert_se(pthread_mutex_unlock(&p->mutex) == 0);
-
- return NSS_STATUS_UNAVAIL;
-}
-
-enum nss_status _nss_systemd_setpwent(int stayopen) {
- return systemd_setent(&getpwent_data);
+ assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0);
+ return ret;
}
-enum nss_status _nss_systemd_setgrent(int stayopen) {
- return systemd_setent(&getgrent_data);
-}
+enum nss_status _nss_systemd_getgrent_r(
+ struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
-enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) {
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+ _cleanup_free_ char **members = NULL;
enum nss_status ret;
- UserEntry *p;
- size_t len;
+ int r;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(result);
- assert(buffer);
assert(errnop);
- assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0);
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0);
- LIST_FOREACH(entries, p, getpwent_data.position) {
- len = strlen(p->name) + 1;
- if (buflen < len) {
+ if (!getgrent_data.iterator) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ if (!getgrent_data.by_membership) {
+ r = groupdb_iterator_get(getgrent_data.iterator, &gr);
+ if (r == -ESRCH) {
+ /* So we finished iterating native groups now. let's now continue with iterating
+ * native memberships, and generate additional group entries for any groups
+ * referenced there that are defined in NSS only. This means for those groups there
+ * will be two or more entries generated during iteration, but this is apparently how
+ * this is supposed to work, and what other implementations do too. Clients are
+ * supposed to merge the group records found during iteration automatically. */
+ getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator);
+
+ r = membershipdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ getgrent_data.by_membership = true;
+ } else if (r < 0) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- ret = NSS_STATUS_TRYAGAIN;
- goto finalize;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ } else if (!STR_IN_SET(gr->group_name, root_group.gr_name, nobody_group.gr_name)) {
+ r = membershipdb_by_group_strv(gr->group_name, nss_glue_userdb_flags(), &members);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
}
+ }
- memcpy(buffer, p->name, len);
-
- result->pw_name = buffer;
- result->pw_uid = p->id;
- result->pw_gid = p->id;
- result->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- result->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- result->pw_dir = (char*) DYNAMIC_USER_DIR;
- result->pw_shell = (char*) DYNAMIC_USER_SHELL;
- break;
+ if (getgrent_data.by_membership) {
+ _cleanup_close_ int lock_fd = -1;
+
+ for (;;) {
+ _cleanup_free_ char *user_name = NULL, *group_name = NULL;
+
+ r = membershipdb_iterator_get(getgrent_data.iterator, &user_name, &group_name);
+ if (r == -ESRCH) {
+ ret = NSS_STATUS_NOTFOUND;
+ goto finish;
+ }
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name))
+ continue;
+ if (STR_IN_SET(group_name, root_group.gr_name, nobody_group.gr_name))
+ continue;
+
+ /* We are about to recursively call into NSS, let's make sure we disable recursion into our own code. */
+ if (lock_fd < 0) {
+ lock_fd = userdb_nss_compat_disable();
+ if (lock_fd < 0 && lock_fd != -EBUSY) {
+ UNPROTECT_ERRNO;
+ *errnop = -lock_fd;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+ }
+
+ r = nss_group_record_by_name(group_name, &gr);
+ if (r == -ESRCH)
+ continue;
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do NSS check for group '%s', ignoring: %m", group_name);
+ continue;
+ }
+
+ members = strv_new(user_name);
+ if (!members) {
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ /* Note that we currently generate one group entry per user that is part of a
+ * group. It's a bit ugly, but equivalent to generating a single entry with a set of
+ * members in them. */
+ break;
+ }
}
- if (!p) {
- ret = NSS_STATUS_NOTFOUND;
- goto finalize;
+
+ r = nss_pack_group_record(gr, members, result, buffer, buflen);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_TRYAGAIN;
+ goto finish;
}
- /* On success, step to the next entry. */
- p = p->entries_next;
ret = NSS_STATUS_SUCCESS;
-finalize:
- /* Save position for the next call. */
- getpwent_data.position = p;
-
- assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0);
-
+finish:
+ assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0);
return ret;
}
-enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) {
- enum nss_status ret;
- UserEntry *p;
- size_t len;
+enum nss_status _nss_systemd_initgroups_dyn(
+ const char *user_name,
+ gid_t gid,
+ long *start,
+ long *size,
+ gid_t **groupsp,
+ long int limit,
+ int *errnop) {
+
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ bool any = false;
+ int r;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
- assert(result);
- assert(buffer);
+ assert(user_name);
+ assert(start);
+ assert(size);
+ assert(groupsp);
assert(errnop);
- assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0);
+ if (!valid_user_group_name(user_name))
+ return NSS_STATUS_NOTFOUND;
+
+ /* Don't allow extending these two special users, the same as we won't resolve them via getpwnam() */
+ if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name))
+ return NSS_STATUS_NOTFOUND;
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = membershipdb_by_user(user_name, nss_glue_userdb_flags(), &iterator);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
- LIST_FOREACH(entries, p, getgrent_data.position) {
- len = sizeof(char*) + strlen(p->name) + 1;
- if (buflen < len) {
+ for (;;) {
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ _cleanup_free_ char *group_name = NULL;
+
+ r = membershipdb_iterator_get(iterator, NULL, &group_name);
+ if (r == -ESRCH)
+ break;
+ if (r < 0) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- ret = NSS_STATUS_TRYAGAIN;
- goto finalize;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
}
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), p->name);
+ /* The group might be defined via traditional NSS only, hence let's do a full look-up without
+ * disabling NSS. This means we are operating recursively here. */
- result->gr_name = buffer + sizeof(char*);
- result->gr_gid = p->id;
- result->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- result->gr_mem = (char**) buffer;
- break;
- }
- if (!p) {
- ret = NSS_STATUS_NOTFOUND;
- goto finalize;
- }
+ r = groupdb_by_name(group_name, nss_glue_userdb_flags() & ~USERDB_AVOID_NSS, &g);
+ if (r == -ESRCH)
+ continue;
+ if (r < 0) {
+ log_debug_errno(r, "Failed to resolve group '%s', ignoring: %m", group_name);
+ continue;
+ }
- /* On success, step to the next entry. */
- p = p->entries_next;
- ret = NSS_STATUS_SUCCESS;
+ if (g->gid == gid)
+ continue;
-finalize:
- /* Save position for the next call. */
- getgrent_data.position = p;
+ if (*start >= *size) {
+ gid_t *new_groups;
+ long new_size;
+
+ if (limit > 0 && *size >= limit) /* Reached the limit.? */
+ break;
+
+ if (*size > LONG_MAX/2) { /* Check for overflow */
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ new_size = *start * 2;
+ if (limit > 0 && new_size > limit)
+ new_size = limit;
+
+ /* Enlarge buffer */
+ new_groups = realloc(*groupsp, new_size * sizeof(**groupsp));
+ if (!new_groups) {
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ *groupsp = new_groups;
+ *size = new_size;
+ }
- assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0);
+ (*groupsp)[(*start)++] = g->gid;
+ any = true;
+ }
- return ret;
+ return any ? NSS_STATUS_SUCCESS : NSS_STATUS_NOTFOUND;
}
diff --git a/src/nss-systemd/nss-systemd.sym b/src/nss-systemd/nss-systemd.sym
index ff63382b152c9..77e1fbe93f227 100644
--- a/src/nss-systemd/nss-systemd.sym
+++ b/src/nss-systemd/nss-systemd.sym
@@ -19,5 +19,6 @@ global:
_nss_systemd_endgrent;
_nss_systemd_setgrent;
_nss_systemd_getgrent_r;
+ _nss_systemd_initgroups_dyn;
local: *;
};
diff --git a/src/nss-systemd/userdb-glue.c b/src/nss-systemd/userdb-glue.c
new file mode 100644
index 0000000000000..810ac56286d18
--- /dev/null
+++ b/src/nss-systemd/userdb-glue.c
@@ -0,0 +1,352 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "env-util.h"
+#include "fd-util.h"
+#include "group-record-nss.h"
+#include "strv.h"
+#include "user-record.h"
+#include "userdb-glue.h"
+#include "userdb.h"
+
+UserDBFlags nss_glue_userdb_flags(void) {
+ UserDBFlags flags = USERDB_AVOID_NSS;
+
+ /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */
+ if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
+ flags |= USERDB_AVOID_DYNAMIC_USER;
+
+ return flags;
+}
+
+int nss_pack_user_record(
+ UserRecord *hr,
+ struct passwd *pwd,
+ char *buffer,
+ size_t buflen) {
+
+ const char *rn, *hd, *shell;
+ size_t required;
+
+ assert(hr);
+ assert(pwd);
+
+ assert_se(hr->user_name);
+ required = strlen(hr->user_name) + 1;
+
+ assert_se(rn = user_record_real_name(hr));
+ required += strlen(rn) + 1;
+
+ assert_se(hd = user_record_home_directory(hr));
+ required += strlen(hd) + 1;
+
+ assert_se(shell = user_record_shell(hr));
+ required += strlen(shell) + 1;
+
+ if (buflen < required)
+ return -ERANGE;
+
+ *pwd = (struct passwd) {
+ .pw_name = buffer,
+ .pw_uid = hr->uid,
+ .pw_gid = user_record_gid(hr),
+ .pw_passwd = (char*) "x", /* means: see shadow file */
+ };
+
+ assert(buffer);
+
+ pwd->pw_gecos = stpcpy(pwd->pw_name, hr->user_name) + 1;
+ pwd->pw_dir = stpcpy(pwd->pw_gecos, rn) + 1;
+ pwd->pw_shell = stpcpy(pwd->pw_dir, hd) + 1;
+ strcpy(pwd->pw_shell, shell);
+
+ return 0;
+}
+
+enum nss_status userdb_getpwnam(
+ const char *name,
+ struct passwd *pwd,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ assert(pwd);
+ assert(errnop);
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = userdb_by_name(name, nss_glue_userdb_flags(), &hr);
+ if (r == -ESRCH)
+ return NSS_STATUS_NOTFOUND;
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = nss_pack_user_record(hr, pwd, buffer, buflen);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ return NSS_STATUS_SUCCESS;
+}
+
+enum nss_status userdb_getpwuid(
+ uid_t uid,
+ struct passwd *pwd,
+ char *buffer,
+ size_t buflen,
+ int *errnop) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ assert(pwd);
+ assert(errnop);
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = userdb_by_uid(uid, nss_glue_userdb_flags(), &hr);
+ if (r == -ESRCH)
+ return NSS_STATUS_NOTFOUND;
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = nss_pack_user_record(hr, pwd, buffer, buflen);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ return NSS_STATUS_SUCCESS;
+}
+
+int nss_pack_group_record(
+ GroupRecord *g,
+ char **extra_members,
+ struct group *gr,
+ char *buffer,
+ size_t buflen) {
+
+ char **array = NULL, *p, **m;
+ size_t required, n = 0, i = 0;
+
+ assert(g);
+ assert(gr);
+
+ assert_se(g->group_name);
+ required = strlen(g->group_name) + 1;
+
+ STRV_FOREACH(m, g->members) {
+ required += sizeof(char*); /* space for ptr array entry */
+ required += strlen(*m) + 1;
+ n++;
+ }
+ STRV_FOREACH(m, extra_members) {
+ if (strv_contains(g->members, *m))
+ continue;
+
+ required += sizeof(char*);
+ required += strlen(*m) + 1;
+ n++;
+ }
+
+ required += sizeof(char*); /* trailing NULL in ptr array entry */
+
+ if (buflen < required)
+ return -ERANGE;
+
+ array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */
+ p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */
+
+ STRV_FOREACH(m, g->members) {
+ array[i++] = p;
+ p = stpcpy(p, *m) + 1;
+ }
+ STRV_FOREACH(m, extra_members) {
+ if (strv_contains(g->members, *m))
+ continue;
+
+ array[i++] = p;
+ p = stpcpy(p, *m) + 1;
+ }
+
+ assert_se(i == n);
+ array[n] = NULL;
+
+ *gr = (struct group) {
+ .gr_name = strcpy(p, g->group_name),
+ .gr_gid = g->gid,
+ .gr_passwd = (char*) "x", /* means: see shadow file */
+ .gr_mem = array,
+ };
+
+ return 0;
+}
+
+enum nss_status userdb_getgrnam(
+ const char *name,
+ struct group *gr,
+ char *buffer,
+ size_t buflen,
+ int *errnop) {
+
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ _cleanup_strv_free_ char **members = NULL;
+ int r;
+
+ assert(gr);
+ assert(errnop);
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = groupdb_by_name(name, nss_glue_userdb_flags(), &g);
+ if (r < 0 && r != -ESRCH) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = membershipdb_by_group_strv(name, nss_glue_userdb_flags(), &members);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ if (!g) {
+ _cleanup_close_ int lock_fd = -1;
+
+ if (strv_isempty(members))
+ return NSS_STATUS_NOTFOUND;
+
+ /* Grmbl, so we are supposed to extend a group entry, but the group entry itself is not
+ * accessible via non-NSS. Hence let's do what we have to do, and query NSS after all to
+ * acquire it, so that we can extend it (that's because glibc's group merging feature will
+ * merge groups only if both GID and name match and thus we need to have both first). It
+ * sucks behaving recursively likely this, but it's apparently what everybody does. We break
+ * the recursion for ourselves via the userdb_nss_compat_disable() lock. */
+
+ lock_fd = userdb_nss_compat_disable();
+ if (lock_fd < 0 && lock_fd != -EBUSY)
+ return lock_fd;
+
+ r = nss_group_record_by_name(name, &g);
+ if (r == -ESRCH)
+ return NSS_STATUS_NOTFOUND;
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ }
+
+ r = nss_pack_group_record(g, members, gr, buffer, buflen);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ return NSS_STATUS_SUCCESS;
+}
+
+enum nss_status userdb_getgrgid(
+ gid_t gid,
+ struct group *gr,
+ char *buffer,
+ size_t buflen,
+ int *errnop) {
+
+
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ _cleanup_strv_free_ char **members = NULL;
+ bool from_nss;
+ int r;
+
+ assert(gr);
+ assert(errnop);
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r) {
+ *errnop = EHOSTDOWN;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ r = groupdb_by_gid(gid, nss_glue_userdb_flags(), &g);
+ if (r < 0 && r != -ESRCH) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ if (!g) {
+ _cleanup_close_ int lock_fd = -1;
+
+ /* So, quite possibly we have to extend an existing group record with additional members. But
+ * to do this we need to know the group name first. The group didn't exist via non-NSS
+ * queries though, hence let's try to acquire it here recursively via NSS. */
+
+ lock_fd = userdb_nss_compat_disable();
+ if (lock_fd < 0 && lock_fd != -EBUSY)
+ return lock_fd;
+
+ r = nss_group_record_by_gid(gid, &g);
+ if (r == -ESRCH)
+ return NSS_STATUS_NOTFOUND;
+
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ from_nss = true;
+ } else
+ from_nss = false;
+
+ r = membershipdb_by_group_strv(g->group_name, nss_glue_userdb_flags(), &members);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ /* If we acquired the record via NSS then there's no reason to respond unless we have to agument the
+ * list of members of the group */
+ if (from_nss && strv_isempty(members))
+ return NSS_STATUS_NOTFOUND;
+
+ r = nss_pack_group_record(g, members, gr, buffer, buflen);
+ if (r < 0) {
+ *errnop = -r;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ return NSS_STATUS_SUCCESS;
+}
diff --git a/src/nss-systemd/userdb-glue.h b/src/nss-systemd/userdb-glue.h
new file mode 100644
index 0000000000000..02add24b6b814
--- /dev/null
+++ b/src/nss-systemd/userdb-glue.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "userdb.h"
+
+UserDBFlags nss_glue_userdb_flags(void);
+
+int nss_pack_user_record(UserRecord *hr, struct passwd *pwd, char *buffer, size_t buflen);
+int nss_pack_group_record(GroupRecord *g, char **extra_members, struct group *gr, char *buffer, size_t buflen);
+
+enum nss_status userdb_getpwnam(const char *name, struct passwd *pwd, char *buffer, size_t buflen, int *errnop);
+enum nss_status userdb_getpwuid(uid_t uid, struct passwd *pwd, char *buffer, size_t buflen, int *errnop);
+
+enum nss_status userdb_getgrnam(const char *name, struct group *gr, char *buffer, size_t buflen, int *errnop);
+enum nss_status userdb_getgrgid(gid_t gid, struct group *gr, char *buffer, size_t buflen, int *errnop);
diff --git a/src/portable/portable.c b/src/portable/portable.c
index d37880cfd1d8f..15a4c2c618bde 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
+#include
+
#include "bus-common-errors.h"
#include "bus-error.h"
#include "conf-files.h"
@@ -359,7 +361,7 @@ static int portable_extract_by_path(
assert(path);
- r = loop_device_make_by_path(path, O_RDONLY, &d);
+ r = loop_device_make_by_path(path, O_RDONLY, LO_FLAGS_PARTSCAN, &d);
if (r == -EISDIR) {
/* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory
* tree and not a raw device. It's easy then. */
diff --git a/src/core/chown-recursive.c b/src/shared/chown-recursive.c
similarity index 76%
rename from src/core/chown-recursive.c
rename to src/shared/chown-recursive.c
index 24cdf25b838b2..1aebac307ac30 100644
--- a/src/core/chown-recursive.c
+++ b/src/shared/chown-recursive.c
@@ -139,3 +139,40 @@ int path_chown_recursive(
return chown_recursive_internal(TAKE_FD(fd), &st, uid, gid, mask); /* we donate the fd to the call, regardless if it succeeded or failed */
}
+
+int fd_chown_recursive(
+ int fd,
+ uid_t uid,
+ gid_t gid,
+ mode_t mask) {
+
+ int duplicated_fd = -1;
+ struct stat st;
+
+ /* Note that the slightly different order of fstat() and the checks here and in
+ * path_chown_recursive(). That's because when we open the dirctory ourselves we can specify
+ * O_DIRECTORY and we always want to ensure we are operating on a directory before deciding whether
+ * the operation is otherwise redundant. */
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+
+ if (!S_ISDIR(st.st_mode))
+ return -ENOTDIR;
+
+ if (!uid_is_valid(uid) && !gid_is_valid(gid) && (mask & 07777) == 07777)
+ return 0; /* nothing to do */
+
+ /* Shortcut, as above */
+ if ((!uid_is_valid(uid) || st.st_uid == uid) &&
+ (!gid_is_valid(gid) || st.st_gid == gid) &&
+ ((st.st_mode & ~mask & 07777) == 0))
+ return 0;
+
+ /* Let's duplicate the fd here, as opendir() wants to take possession of it and close it afterwards */
+ duplicated_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (duplicated_fd < 0)
+ return -errno;
+
+ return chown_recursive_internal(duplicated_fd, &st, uid, gid, mask); /* fd donated even on failure */
+}
diff --git a/src/core/chown-recursive.h b/src/shared/chown-recursive.h
similarity index 69%
rename from src/core/chown-recursive.h
rename to src/shared/chown-recursive.h
index bfee05f3be561..14a79733f531e 100644
--- a/src/core/chown-recursive.h
+++ b/src/shared/chown-recursive.h
@@ -4,3 +4,5 @@
#include
int path_chown_recursive(const char *path, uid_t uid, gid_t gid, mode_t mask);
+
+int fd_chown_recursive(int fd, uid_t uid, gid_t gid, mode_t mask);
diff --git a/src/shared/format-table.h b/src/shared/format-table.h
index aacf978978847..80f319d054a30 100644
--- a/src/shared/format-table.h
+++ b/src/shared/format-table.h
@@ -53,6 +53,12 @@ typedef enum TableDataType {
#define TABLE_PID TABLE_INT32
assert_cc(sizeof(pid_t) == sizeof(int32_t));
+/* UIDs/GIDs are just 32bit unsigned integers on Linux */
+#define TABLE_UID TABLE_UINT32
+#define TABLE_GID TABLE_UINT32
+assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+assert_cc(sizeof(gid_t) == sizeof(uint32_t));
+
typedef struct Table Table;
typedef struct TableCell TableCell;
diff --git a/src/shared/gpt.h b/src/shared/gpt.h
index 31e01bd5a5cc9..3d44514d12407 100644
--- a/src/shared/gpt.h
+++ b/src/shared/gpt.h
@@ -19,6 +19,7 @@
#define GPT_SWAP SD_ID128_MAKE(06,57,fd,6d,a4,ab,43,c4,84,e5,09,33,c8,4b,4f,4f)
#define GPT_HOME SD_ID128_MAKE(93,3a,c7,e1,2e,b4,4f,13,b8,44,0e,14,e2,ae,f9,15)
#define GPT_SRV SD_ID128_MAKE(3b,8f,84,25,20,e0,4f,3b,90,7f,1a,25,a7,6f,98,e8)
+#define GPT_USER_HOME SD_ID128_MAKE(77,3f,91,ef,66,d4,49,b5,bd,83,d6,83,bf,40,ad,16)
/* Verity partitions for the root partitions above (we only define them for the root partitions, because only they are
* are commonly read-only and hence suitable for verity). */
diff --git a/src/shared/group-record-nss.c b/src/shared/group-record-nss.c
new file mode 100644
index 0000000000000..e31f2ce697ac5
--- /dev/null
+++ b/src/shared/group-record-nss.c
@@ -0,0 +1,211 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "errno-util.h"
+#include "group-record-nss.h"
+#include "strv.h"
+#include "user-util.h"
+
+int nss_group_to_group_record(
+ const struct group *grp,
+ const struct sgrp *sgrp,
+ GroupRecord **ret) {
+
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ int r;
+
+ assert(grp);
+ assert(ret);
+
+ if (isempty(grp->gr_name))
+ return -EINVAL;
+
+ if (sgrp && !streq_ptr(sgrp->sg_namp, grp->gr_name))
+ return -EINVAL;
+
+ g = group_record_new();
+ if (!g)
+ return -ENOMEM;
+
+ r = free_and_strdup(&g->group_name, grp->gr_name);
+ if (r < 0)
+ return r;
+
+ strv_free(g->members);
+ g->members = strv_copy(grp->gr_mem);
+ if (!g->members)
+ return -ENOMEM;
+
+ g->gid = grp->gr_gid;
+
+ if (sgrp) {
+ if (hashed_password_valid(sgrp->sg_passwd)) {
+ strv_free_erase(g->hashed_password);
+ g->hashed_password = strv_new(sgrp->sg_passwd);
+ if (!g->hashed_password)
+ return -ENOMEM;
+ } else
+ g->hashed_password = strv_free_erase(g->hashed_password);
+
+ r = strv_extend_strv(&g->members, sgrp->sg_mem, 1);
+ if (r < 0)
+ return r;
+
+ strv_free(g->administrators);
+ g->administrators = strv_copy(sgrp->sg_adm);
+ if (!g->administrators)
+ return -ENOMEM;
+ } else {
+ g->hashed_password = strv_free_erase(g->hashed_password);
+ g->administrators = strv_free(g->administrators);
+ }
+
+ g->json = json_variant_unref(g->json);
+ r = json_build(&g->json, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(g->gid)),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->members), "members", JSON_BUILD_STRV(g->members)),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(g->hashed_password)))),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->administrators), "administrators", JSON_BUILD_STRV(g->administrators))));
+ if (r < 0)
+ return r;
+
+ g->mask = USER_RECORD_REGULAR |
+ (!strv_isempty(g->hashed_password) ? USER_RECORD_PRIVILEGED : 0);
+
+ *ret = TAKE_PTR(g);
+ return 0;
+}
+
+int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer) {
+ size_t buflen = 4096;
+ int r;
+
+ assert(grp);
+ assert(ret_sgrp);
+ assert(ret_buffer);
+
+ for (;;) {
+ _cleanup_free_ char *buf = NULL;
+ struct sgrp sgrp, *result;
+
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getsgnam_r(grp->gr_name, &sgrp, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+
+ *ret_sgrp = *result;
+ *ret_buffer = TAKE_PTR(buf);
+ return 0;
+ }
+ if (r < 0)
+ return -EIO; /* Weird, this should not return negative! */
+ if (r != ERANGE)
+ return -r;
+
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+}
+
+int nss_group_record_by_name(const char *name, GroupRecord **ret) {
+ _cleanup_free_ char *buf = NULL, *sbuf = NULL;
+ struct group grp, *result;
+ bool incomplete = false;
+ size_t buflen = 4096;
+ struct sgrp sgrp;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ for (;;) {
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getgrnam_r(name, &grp, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+
+ break;
+ }
+
+ if (r < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrnam_r() returned a negative value");
+ if (r != ERANGE)
+ return -r;
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+
+ r = nss_sgrp_for_group(result, &sgrp, &sbuf);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret);
+ if (r < 0)
+ return r;
+
+ (*ret)->incomplete = incomplete;
+ return 0;
+}
+
+int nss_group_record_by_gid(gid_t gid, GroupRecord **ret) {
+ _cleanup_free_ char *buf = NULL, *sbuf = NULL;
+ struct group grp, *result;
+ bool incomplete = false;
+ size_t buflen = 4096;
+ struct sgrp sgrp;
+ int r;
+
+ assert(ret);
+
+ for (;;) {
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getgrgid_r(gid, &grp, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+ break;
+ }
+
+ if (r < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrgid_r() returned a negative value");
+ if (r != ERANGE)
+ return -r;
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+
+ r = nss_sgrp_for_group(result, &sgrp, &sbuf);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret);
+ if (r < 0)
+ return r;
+
+ (*ret)->incomplete = incomplete;
+ return 0;
+}
diff --git a/src/shared/group-record-nss.h b/src/shared/group-record-nss.h
new file mode 100644
index 0000000000000..38b2995178ff7
--- /dev/null
+++ b/src/shared/group-record-nss.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#include "group-record.h"
+
+/* Synthesize GroupRecord objects from NSS data */
+
+int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, GroupRecord **ret);
+int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer);
+
+int nss_group_record_by_name(const char *name, GroupRecord **ret);
+int nss_group_record_by_gid(gid_t gid, GroupRecord **ret);
diff --git a/src/shared/group-record-show.c b/src/shared/group-record-show.c
new file mode 100644
index 0000000000000..d0300e483c743
--- /dev/null
+++ b/src/shared/group-record-show.c
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "format-util.h"
+#include "group-record-show.h"
+#include "strv.h"
+#include "user-util.h"
+#include "userdb.h"
+
+void group_record_show(GroupRecord *gr, bool show_full_user_info) {
+ int r;
+
+ printf(" Group name: %s\n",
+ group_record_group_name_and_realm(gr));
+
+ printf(" Disposition: %s\n", user_disposition_to_string(group_record_disposition(gr)));
+
+ if (gr->last_change_usec != USEC_INFINITY) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), gr->last_change_usec));
+ }
+
+ if (gid_is_valid(gr->gid))
+ printf(" GID: " GID_FMT "\n", gr->gid);
+
+ if (show_full_user_info) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ r = membershipdb_by_group(gr->group_name, 0, &iterator);
+ if (r < 0) {
+ errno = -r;
+ printf(" Members: (can't acquire: %m)");
+ } else {
+ const char *prefix = " Members:";
+
+ for (;;) {
+ _cleanup_free_ char *user = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user, NULL);
+ if (r == -ESRCH)
+ break;
+ if (r < 0) {
+ errno = -r;
+ printf("%s (can't iterate: %m\n", prefix);
+ break;
+ }
+
+ printf("%s %s\n", prefix, user);
+ prefix = " ";
+ }
+ }
+ } else {
+ const char *prefix = " Members:";
+ char **i;
+
+ STRV_FOREACH(i, gr->members) {
+ printf("%s %s\n", prefix, *i);
+ prefix = " ";
+ }
+ }
+
+ if (!strv_isempty(gr->administrators)) {
+ const char *prefix = " Admins:";
+ char **i;
+
+ STRV_FOREACH(i, gr->administrators) {
+ printf("%s %s\n", prefix, *i);
+ prefix = " ";
+ }
+ }
+
+ if (!strv_isempty(gr->hashed_password))
+ printf(" Passwords: %zu\n", strv_length(gr->hashed_password));
+
+ if (gr->service)
+ printf(" Service: %s\n", gr->service);
+}
diff --git a/src/shared/group-record-show.h b/src/shared/group-record-show.h
new file mode 100644
index 0000000000000..12bdbd17243c9
--- /dev/null
+++ b/src/shared/group-record-show.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "group-record.h"
+
+void group_record_show(GroupRecord *gr, bool show_full_user_info);
diff --git a/src/shared/group-record.c b/src/shared/group-record.c
new file mode 100644
index 0000000000000..5c4534242f5c7
--- /dev/null
+++ b/src/shared/group-record.c
@@ -0,0 +1,347 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "group-record.h"
+#include "strv.h"
+#include "user-util.h"
+
+GroupRecord* group_record_new(void) {
+ GroupRecord *h;
+
+ h = new(GroupRecord, 1);
+ if (!h)
+ return NULL;
+
+ *h = (GroupRecord) {
+ .n_ref = 1,
+ .disposition = _USER_DISPOSITION_INVALID,
+ .last_change_usec = UINT64_MAX,
+ .gid = GID_INVALID,
+ };
+
+ return h;
+}
+
+static GroupRecord *group_record_free(GroupRecord *g) {
+ if (!g)
+ return NULL;
+
+ free(g->group_name);
+ free(g->realm);
+ free(g->group_name_and_realm_auto);
+
+ strv_free(g->members);
+ free(g->service);
+ strv_free(g->administrators);
+ strv_free_erase(g->hashed_password);
+
+ json_variant_unref(g->json);
+
+ return mfree(g);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(GroupRecord, group_record, group_record_free);
+
+static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch privileged_dispatch_table[] = {
+ { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(GroupRecord, hashed_password), JSON_SAFE },
+ {},
+ };
+
+ return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata);
+}
+
+static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch binding_dispatch_table[] = {
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
+ {},
+ };
+
+ char smid[SD_ID128_STRING_MAX];
+ JsonVariant *m;
+ sd_id128_t mid;
+ int r;
+
+ if (!json_variant_is_object(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to determine machine ID: %m");
+
+ m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
+ if (!m)
+ return 0;
+
+ return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata);
+}
+
+static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch per_machine_dispatch_table[] = {
+ { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
+ { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
+ { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 },
+ { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 },
+ {},
+ };
+
+ JsonVariant *e;
+ int r;
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+ bool matching = false;
+ JsonVariant *m;
+
+ if (!json_variant_is_object(e))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name));
+
+ m = json_variant_by_key(e, "matchMachineId");
+ if (m) {
+ r = per_machine_id_match(m, flags);
+ if (r < 0)
+ return r;
+
+ matching = r > 0;
+ }
+
+ if (!matching) {
+ m = json_variant_by_key(e, "matchHostname");
+ if (m) {
+ r = per_machine_hostname_match(m, flags);
+ if (r < 0)
+ return r;
+
+ matching = r > 0;
+ }
+ }
+
+ if (!matching)
+ continue;
+
+ r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch status_dispatch_table[] = {
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE },
+ {},
+ };
+
+ char smid[SD_ID128_STRING_MAX];
+ JsonVariant *m;
+ sd_id128_t mid;
+ int r;
+
+ if (!json_variant_is_object(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to determine machine ID: %m");
+
+ m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
+ if (!m)
+ return 0;
+
+ return json_dispatch(m, status_dispatch_table, NULL, flags, userdata);
+}
+
+static int group_record_augment(GroupRecord *h, JsonDispatchFlags json_flags) {
+ assert(h);
+
+ if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR))
+ return 0;
+
+ assert(h->group_name);
+
+ if (!h->group_name_and_realm_auto && h->realm) {
+ h->group_name_and_realm_auto = strjoin(h->group_name, "@", h->realm);
+ if (!h->group_name_and_realm_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+
+ return 0;
+}
+
+int group_record_load(
+ GroupRecord *h,
+ JsonVariant *v,
+ UserRecordLoadFlags load_flags) {
+
+ static const JsonDispatch group_dispatch_table[] = {
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), 0 },
+ { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 },
+ { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(GroupRecord, disposition), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE },
+ { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(GroupRecord, last_change_usec), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
+ { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 },
+ { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 },
+
+ { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
+
+ /* Not defined for now, for groups, but let's at least generate sensible errors about it */
+ { "secret", JSON_VARIANT_OBJECT, json_dispatch_unsupported, 0, 0 },
+
+ /* Ignore the perMachine, binding and status stuff here, and process it later, so that it overrides whatever is set above */
+ { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 },
+ { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 },
+ { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 },
+
+ /* Ignore 'signature', we check it with explicit accessors instead */
+ { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 },
+ {},
+ };
+
+ JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags);
+ JsonVariant *per_machine, *binding, *status;
+ int r;
+
+ assert(h);
+ assert(!h->json);
+
+ /* Note that this call will leave a half-initialized record around on failure! */
+
+ if ((USER_RECORD_REQUIRE_MASK(load_flags) & (USER_RECORD_SECRET|USER_RECORD_PRIVILEGED)) != 0)
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Secret and privileged section currently not available for groups, refusing.");
+
+ r = user_group_record_mangle(v, load_flags, &h->json, &h->mask);
+ if (r < 0)
+ return r;
+
+ r = json_dispatch(h->json, group_dispatch_table, NULL, json_flags, h);
+ if (r < 0)
+ return r;
+
+ /* During the parsing operation above we ignored the 'perMachine' and 'binding' fields, since we want
+ * them to override the global options. Let's process them now. */
+
+ per_machine = json_variant_by_key(h->json, "perMachine");
+ if (per_machine) {
+ r = dispatch_per_machine("perMachine", per_machine, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ binding = json_variant_by_key(h->json, "binding");
+ if (binding) {
+ r = dispatch_binding("binding", binding, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ status = json_variant_by_key(h->json, "status");
+ if (status) {
+ r = dispatch_status("binding", status, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->group_name)
+ return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "Group name field missing, refusing.");
+
+ r = group_record_augment(h, json_flags);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int group_record_build(GroupRecord **ret, ...) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ va_list ap;
+ int r;
+
+ assert(ret);
+
+ va_start(ap, ret);
+ r = json_buildv(&v, ap);
+ va_end(ap);
+
+ if (r < 0)
+ return r;
+
+ g = group_record_new();
+ if (!g)
+ return -ENOMEM;
+
+ r = group_record_load(g, v, USER_RECORD_LOAD_FULL);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(g);
+ return 0;
+}
+
+const char *group_record_group_name_and_realm(GroupRecord *h) {
+ assert(h);
+
+ /* Return the pre-initialized joined string if it is defined */
+ if (h->group_name_and_realm_auto)
+ return h->group_name_and_realm_auto;
+
+ /* If it's not defined then we cannot have a realm */
+ assert(!h->realm);
+ return h->group_name;
+}
+
+UserDisposition group_record_disposition(GroupRecord *h) {
+ assert(h);
+
+ if (h->disposition >= 0)
+ return h->disposition;
+
+ /* If not declared, derive from GID */
+
+ if (!gid_is_valid(h->gid))
+ return _USER_DISPOSITION_INVALID;
+
+ if (h->gid == 0 || h->gid == GID_NOBODY)
+ return USER_INTRINSIC;
+
+ if (gid_is_system(h->gid))
+ return USER_SYSTEM;
+
+ if (gid_is_dynamic(h->gid))
+ return USER_DYNAMIC;
+
+ if (gid_is_container(h->gid))
+ return USER_CONTAINER;
+
+ if (h->gid > INT32_MAX)
+ return USER_RESERVED;
+
+ return USER_REGULAR;
+}
+
+int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord **ret) {
+ _cleanup_(group_record_unrefp) GroupRecord *c = NULL;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ c = group_record_new();
+ if (!c)
+ return -ENOMEM;
+
+ r = group_record_load(c, h->json, flags);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
diff --git a/src/shared/group-record.h b/src/shared/group-record.h
new file mode 100644
index 0000000000000..b72a43e50d8d6
--- /dev/null
+++ b/src/shared/group-record.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "json.h"
+#include "user-record.h"
+
+typedef struct GroupRecord {
+ unsigned n_ref;
+ UserRecordMask mask;
+ bool incomplete;
+
+ char *group_name;
+ char *realm;
+ char *group_name_and_realm_auto;
+
+ UserDisposition disposition;
+ uint64_t last_change_usec;
+
+ gid_t gid;
+
+ char **members;
+
+ char *service;
+
+ /* The following exist mostly so that we can cover the full /etc/gshadow set of fields, we currently
+ * do not actually make use of these */
+ char **administrators; /* maps to 'struct sgrp' .sg_adm field */
+ char **hashed_password; /* maps to 'struct sgrp' .sg_passwd field */
+
+ JsonVariant *json;
+} GroupRecord;
+
+GroupRecord* group_record_new(void);
+GroupRecord* group_record_ref(GroupRecord *g);
+GroupRecord* group_record_unref(GroupRecord *g);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(GroupRecord*, group_record_unref);
+
+int group_record_load(GroupRecord *h, JsonVariant *v, UserRecordLoadFlags flags);
+int group_record_build(GroupRecord **ret, ...);
+int group_record_clone(GroupRecord *g, UserRecordLoadFlags flags, GroupRecord **ret);
+
+const char *group_record_group_name_and_realm(GroupRecord *h);
+UserDisposition group_record_disposition(GroupRecord *h);
diff --git a/src/shared/json.c b/src/shared/json.c
index f1bb50cfa2c8c..6222c188a1843 100644
--- a/src/shared/json.c
+++ b/src/shared/json.c
@@ -24,6 +24,7 @@
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
+#include "user-util.h"
#include "utf8.h"
/* Refuse putting together variants with a larger depth than 4K by default (as a protection against overflowing stacks
@@ -72,6 +73,15 @@ struct JsonVariant {
/* While comparing two arrays, we use this for marking what we already have seen */
bool is_marked:1;
+ /* Erase from memory when freeing */
+ bool sensitive:1;
+
+ /* If this is an object the fields are strictly ordered by name */
+ bool sorted:1;
+
+ /* If in addition to this object all objects referenced by it are also ordered strictly by name */
+ bool normalized:1;
+
/* The current 'depth' of the JsonVariant, i.e. how many levels of member variants this has */
uint16_t depth;
@@ -213,10 +223,10 @@ static uint16_t json_variant_depth(JsonVariant *v) {
return v->depth;
}
-static JsonVariant *json_variant_normalize(JsonVariant *v) {
+static JsonVariant *json_variant_formalize(JsonVariant *v) {
- /* Converts json variants to their normalized form, i.e. fully dereferenced and wherever possible converted to
- * the "magic" version if there is one */
+ /* Converts json variant pointers to their normalized form, i.e. fully dereferenced and wherever
+ * possible converted to the "magic" version if there is one */
if (!v)
return NULL;
@@ -257,9 +267,9 @@ static JsonVariant *json_variant_normalize(JsonVariant *v) {
}
}
-static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) {
+static JsonVariant *json_variant_conservative_formalize(JsonVariant *v) {
- /* Much like json_variant_normalize(), but won't simplify if the variant has a source/line location attached to
+ /* Much like json_variant_formalize(), but won't simplify if the variant has a source/line location attached to
* it, in order not to lose context */
if (!v)
@@ -271,7 +281,7 @@ static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) {
if (v->source || v->line > 0 || v->column > 0)
return v;
- return json_variant_normalize(v);
+ return json_variant_formalize(v);
}
static int json_variant_new(JsonVariant **ret, JsonVariantType type, size_t space) {
@@ -403,6 +413,20 @@ int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n) {
return 0;
}
+int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n) {
+ _cleanup_free_ char *s = NULL;
+ ssize_t k;
+
+ assert_return(ret, -EINVAL);
+ assert_return(n == 0 || p, -EINVAL);
+
+ k = base64mem(p, n, &s);
+ if (k < 0)
+ return k;
+
+ return json_variant_new_stringn(ret, s, k);
+}
+
static void json_variant_set(JsonVariant *a, JsonVariant *b) {
assert(a);
@@ -449,7 +473,7 @@ static void json_variant_set(JsonVariant *a, JsonVariant *b) {
case JSON_VARIANT_ARRAY:
case JSON_VARIANT_OBJECT:
a->is_reference = true;
- a->reference = json_variant_ref(json_variant_conservative_normalize(b));
+ a->reference = json_variant_ref(json_variant_conservative_formalize(b));
break;
case JSON_VARIANT_NULL:
@@ -474,6 +498,7 @@ static void json_variant_copy_source(JsonVariant *v, JsonVariant *from) {
int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ bool normalized = true;
assert_return(ret, -EINVAL);
if (n == 0) {
@@ -509,8 +534,13 @@ int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) {
json_variant_set(w, c);
json_variant_copy_source(w, c);
+
+ if (!json_variant_is_normalized(c))
+ normalized = false;
}
+ v->normalized = normalized;
+
*ret = TAKE_PTR(v);
return 0;
}
@@ -548,6 +578,8 @@ int json_variant_new_array_bytes(JsonVariant **ret, const void *p, size_t n) {
};
}
+ v->normalized = true;
+
*ret = v;
return 0;
}
@@ -599,12 +631,16 @@ int json_variant_new_array_strv(JsonVariant **ret, char **l) {
memcpy(w->string, l[v->n_elements], k+1);
}
+ v->normalized = true;
+
*ret = TAKE_PTR(v);
return 0;
}
int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ const char *prev = NULL;
+ bool sorted = true, normalized = true;
assert_return(ret, -EINVAL);
if (n == 0) {
@@ -628,9 +664,20 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) {
*c = array[v->n_elements];
uint16_t d;
- if ((v->n_elements & 1) == 0 &&
- !json_variant_is_string(c))
- return -EINVAL; /* Every second one needs to be a string, as it is the key name */
+ if ((v->n_elements & 1) == 0) {
+ const char *k;
+
+ if (!json_variant_is_string(c))
+ return -EINVAL; /* Every second one needs to be a string, as it is the key name */
+
+ assert_se(k = json_variant_string(c));
+
+ if (prev && strcmp(k, prev) <= 0)
+ sorted = normalized = false;
+
+ prev = k;
+ } else if (!json_variant_is_normalized(c))
+ normalized = false;
d = json_variant_depth(c);
if (d >= DEPTH_MAX) /* Refuse too deep nesting */
@@ -647,11 +694,53 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) {
json_variant_copy_source(w, c);
}
+ v->normalized = normalized;
+ v->sorted = sorted;
+
*ret = TAKE_PTR(v);
return 0;
}
-static void json_variant_free_inner(JsonVariant *v) {
+static size_t json_variant_size(JsonVariant* v) {
+
+ if (!json_variant_is_regular(v))
+ return 0;
+
+ if (v->is_reference)
+ return offsetof(JsonVariant, reference) + sizeof(JsonVariant*);
+
+ switch (v->type) {
+
+ case JSON_VARIANT_STRING:
+ return offsetof(JsonVariant, string) + strlen(v->string) + 1;
+
+ case JSON_VARIANT_REAL:
+ return offsetof(JsonVariant, value) + sizeof(long double);
+
+ case JSON_VARIANT_UNSIGNED:
+ return offsetof(JsonVariant, value) + sizeof(uintmax_t);
+
+ case JSON_VARIANT_INTEGER:
+ return offsetof(JsonVariant, value) + sizeof(intmax_t);
+
+ case JSON_VARIANT_BOOLEAN:
+ return offsetof(JsonVariant, value) + sizeof(bool);
+
+ case JSON_VARIANT_ARRAY:
+ case JSON_VARIANT_OBJECT:
+ return offsetof(JsonVariant, n_elements) + sizeof(size_t);
+
+ case JSON_VARIANT_NULL:
+ return offsetof(JsonVariant, value);
+
+ default:
+ assert_not_reached("unexpected type");
+ }
+}
+
+static void json_variant_free_inner(JsonVariant *v, bool force_sensitive) {
+ bool sensitive;
+
assert(v);
if (!json_variant_is_regular(v))
@@ -659,7 +748,12 @@ static void json_variant_free_inner(JsonVariant *v) {
json_source_unref(v->source);
+ sensitive = v->sensitive || force_sensitive;
+
if (v->is_reference) {
+ if (sensitive)
+ json_variant_sensitive(v->reference);
+
json_variant_unref(v->reference);
return;
}
@@ -668,8 +762,11 @@ static void json_variant_free_inner(JsonVariant *v) {
size_t i;
for (i = 0; i < v->n_elements; i++)
- json_variant_free_inner(v + 1 + i);
+ json_variant_free_inner(v + 1 + i, sensitive);
}
+
+ if (sensitive)
+ explicit_bzero(v, json_variant_size(v));
}
JsonVariant *json_variant_ref(JsonVariant *v) {
@@ -701,7 +798,7 @@ JsonVariant *json_variant_unref(JsonVariant *v) {
v->n_ref--;
if (v->n_ref == 0) {
- json_variant_free_inner(v);
+ json_variant_free_inner(v, false);
free(v);
}
}
@@ -947,6 +1044,13 @@ bool json_variant_is_negative(JsonVariant *v) {
return false;
}
+bool json_variant_is_blank_object(JsonVariant *v) {
+ /* Returns true if the specified object is null or empty */
+ return !v ||
+ json_variant_is_null(v) ||
+ (json_variant_is_object(v) && json_variant_elements(v) == 0);
+}
+
JsonVariantType json_variant_type(JsonVariant *v) {
if (!v)
@@ -1069,7 +1173,7 @@ JsonVariant *json_variant_by_index(JsonVariant *v, size_t idx) {
if (idx >= v->n_elements)
return NULL;
- return json_variant_conservative_normalize(v + 1 + idx);
+ return json_variant_conservative_formalize(v + 1 + idx);
mismatch:
log_debug("Element in non-array/non-object JSON variant requested by index, returning NULL.");
@@ -1092,6 +1196,37 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria
if (v->is_reference)
return json_variant_by_key(v->reference, key);
+ if (v->sorted) {
+ size_t a = 0, b = v->n_elements/2;
+
+ /* If the variant is sorted we can use bisection to find the entry we need in O(log(n)) time */
+
+ while (b > a) {
+ JsonVariant *p;
+ const char *f;
+ int c;
+
+ i = (a + b) / 2;
+ p = json_variant_dereference(v + 1 + i*2);
+
+ assert_se(f = json_variant_string(p));
+
+ c = strcmp(key, f);
+ if (c == 0) {
+ if (ret_key)
+ *ret_key = json_variant_conservative_formalize(v + 1 + i*2);
+
+ return json_variant_conservative_formalize(v + 1 + i*2 + 1);
+ } else if (c < 0)
+ b = i;
+ else
+ a = i + 1;
+ }
+
+ goto not_found;
+ }
+
+ /* The variant is not sorted, hence search for the field linearly */
for (i = 0; i < v->n_elements; i += 2) {
JsonVariant *p;
@@ -1103,9 +1238,9 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria
if (streq(json_variant_string(p), key)) {
if (ret_key)
- *ret_key = json_variant_conservative_normalize(v + 1 + i);
+ *ret_key = json_variant_conservative_formalize(v + 1 + i);
- return json_variant_conservative_normalize(v + 1 + i + 1);
+ return json_variant_conservative_formalize(v + 1 + i + 1);
}
}
@@ -1130,8 +1265,8 @@ JsonVariant *json_variant_by_key(JsonVariant *v, const char *key) {
bool json_variant_equal(JsonVariant *a, JsonVariant *b) {
JsonVariantType t;
- a = json_variant_normalize(a);
- b = json_variant_normalize(b);
+ a = json_variant_formalize(a);
+ b = json_variant_formalize(b);
if (a == b)
return true;
@@ -1230,6 +1365,26 @@ bool json_variant_equal(JsonVariant *a, JsonVariant *b) {
}
}
+void json_variant_sensitive(JsonVariant *v) {
+ assert(v);
+
+ /* Marks a variant as "sensitive", so that it is erased from memory when it is destroyed. This is a
+ * one-way operation: as soon as it is marked this way it remains marked this way until it's
+ * destoryed. A magic variant is never sensitive though, even when asked, since it's too
+ * basic. Similar, const string variant are never sensitive either, after all they are included in
+ * the source code as they are, which is not suitable for inclusion of secrets.
+ *
+ * Note that this flag has a recursive effect: when we destroy an object or array we'll propagate the
+ * flag to all contained variants. And if those are then destroyed this is propagated further down,
+ * and so on. */
+
+ v = json_variant_formalize(v);
+ if (!json_variant_is_regular(v))
+ return;
+
+ v->sensitive = true;
+}
+
int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column) {
assert_return(v, -EINVAL);
@@ -1597,6 +1752,9 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha
if (((flags & (JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_COLOR)) == JSON_FORMAT_COLOR_AUTO) && colors_enabled())
flags |= JSON_FORMAT_COLOR;
+ if (((flags & (JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_PRETTY)) == JSON_FORMAT_PRETTY_AUTO))
+ flags |= on_tty() ? JSON_FORMAT_PRETTY : JSON_FORMAT_NEWLINE;
+
if (flags & JSON_FORMAT_SSE)
fputs("data: ", f);
if (flags & JSON_FORMAT_SEQ)
@@ -1608,6 +1766,341 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha
fputc('\n', f);
if (flags & JSON_FORMAT_SSE)
fputc('\n', f); /* In case of SSE add a second newline */
+
+ if (flags & JSON_FORMAT_FLUSH)
+ fflush(f);
+}
+
+int json_variant_filter(JsonVariant **v, char **to_remove) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ size_t i, n = 0, k = 0;
+ int r;
+
+ assert(v);
+
+ if (json_variant_is_blank_object(*v))
+ return 0;
+ if (!json_variant_is_object(*v))
+ return -EINVAL;
+
+ if (strv_isempty(to_remove))
+ return 0;
+
+ for (i = 0; i < json_variant_elements(*v); i += 2) {
+ JsonVariant *p;
+
+ p = json_variant_by_index(*v, i);
+ if (!json_variant_has_type(p, JSON_VARIANT_STRING))
+ return -EINVAL;
+
+ if (strv_contains(to_remove, json_variant_string(p))) {
+ if (!array) {
+ array = new(JsonVariant*, json_variant_elements(*v) - 2);
+ if (!array)
+ return -ENOMEM;
+
+ for (k = 0; k < i; k++)
+ array[k] = json_variant_by_index(*v, k);
+ }
+
+ n++;
+ } else if (array) {
+ array[k++] = p;
+ array[k++] = json_variant_by_index(*v, i + 1);
+ }
+ }
+
+ if (n == 0)
+ return 0;
+
+ r = json_variant_new_object(&w, array, k);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(*v);
+ *v = TAKE_PTR(w);
+
+ return (int) n;
+}
+
+int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value) {
+ _cleanup_(json_variant_unrefp) JsonVariant *field_variant = NULL, *w = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ size_t i, k = 0;
+ int r;
+
+ assert(v);
+ assert(field);
+
+ if (json_variant_is_blank_object(*v)) {
+ array = new(JsonVariant*, 2);
+ if (!array)
+ return -ENOMEM;
+
+ } else {
+ if (!json_variant_is_object(*v))
+ return -EINVAL;
+
+ for (i = 0; i < json_variant_elements(*v); i += 2) {
+ JsonVariant *p;
+
+ p = json_variant_by_index(*v, i);
+ if (!json_variant_is_string(p))
+ return -EINVAL;
+
+ if (streq(json_variant_string(p), field)) {
+
+ if (!array) {
+ array = new(JsonVariant*, json_variant_elements(*v));
+ if (!array)
+ return -ENOMEM;
+
+ for (k = 0; k < i; k++)
+ array[k] = json_variant_by_index(*v, k);
+ }
+
+ } else if (array) {
+ array[k++] = p;
+ array[k++] = json_variant_by_index(*v, i + 1);
+ }
+ }
+
+ if (!array) {
+ array = new(JsonVariant*, json_variant_elements(*v) + 2);
+ if (!array)
+ return -ENOMEM;
+
+ for (k = 0; k < json_variant_elements(*v); k++)
+ array[k] = json_variant_by_index(*v, k);
+ }
+ }
+
+ r = json_variant_new_string(&field_variant, field);
+ if (r < 0)
+ return r;
+
+ array[k++] = field_variant;
+ array[k++] = value;
+
+ r = json_variant_new_object(&w, array, k);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(*v);
+ *v = TAKE_PTR(w);
+
+ return 1;
+}
+
+int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value) {
+ _cleanup_(json_variant_unrefp) JsonVariant *m = NULL;
+ int r;
+
+ r = json_variant_new_string(&m, value);
+ if (r < 0)
+ return r;
+
+ return json_variant_set_field(v, field, m);
+}
+
+int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t i) {
+ _cleanup_(json_variant_unrefp) JsonVariant *m = NULL;
+ int r;
+
+ r = json_variant_new_integer(&m, i);
+ if (r < 0)
+ return r;
+
+ return json_variant_set_field(v, field, m);
+}
+
+int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t u) {
+ _cleanup_(json_variant_unrefp) JsonVariant *m = NULL;
+ int r;
+
+ r = json_variant_new_unsigned(&m, u);
+ if (r < 0)
+ return r;
+
+ return json_variant_set_field(v, field, m);
+}
+
+int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b) {
+ _cleanup_(json_variant_unrefp) JsonVariant *m = NULL;
+ int r;
+
+ r = json_variant_new_boolean(&m, b);
+ if (r < 0)
+ return r;
+
+ return json_variant_set_field(v, field, m);
+}
+
+int json_variant_merge(JsonVariant **v, JsonVariant *m) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_free_ JsonVariant **array = NULL;
+ size_t v_elements, m_elements, i, k;
+ bool v_blank, m_blank;
+ int r;
+
+ m = json_variant_dereference(m);
+
+ v_blank = json_variant_is_blank_object(*v);
+ m_blank = json_variant_is_blank_object(m);
+
+ if (!v_blank && !json_variant_is_object(*v))
+ return -EINVAL;
+ if (!m_blank && !json_variant_is_object(m))
+ return -EINVAL;
+
+ if (m_blank)
+ return 0; /* nothing to do */
+
+ if (v_blank) {
+ json_variant_unref(*v);
+ *v = json_variant_ref(m);
+ return 1;
+ }
+
+ v_elements = json_variant_elements(*v);
+ m_elements = json_variant_elements(m);
+ if (v_elements > SIZE_MAX - m_elements) /* overflow check */
+ return -ENOMEM;
+
+ array = new(JsonVariant*, v_elements + m_elements);
+ if (!array)
+ return -ENOMEM;
+
+ k = 0;
+ for (i = 0; i < v_elements; i += 2) {
+ JsonVariant *u;
+
+ u = json_variant_by_index(*v, i);
+ if (!json_variant_is_string(u))
+ return -EINVAL;
+
+ if (json_variant_by_key(m, json_variant_string(u)))
+ continue; /* skip if exists in second variant */
+
+ array[k++] = u;
+ array[k++] = json_variant_by_index(*v, i + 1);
+ }
+
+ for (i = 0; i < m_elements; i++)
+ array[k++] = json_variant_by_index(m, i);
+
+ r = json_variant_new_object(&w, array, k);
+ if (r < 0)
+ return r;
+
+ json_variant_unref(*v);
+ *v = TAKE_PTR(w);
+
+ return 1;
+}
+
+int json_variant_append_array(JsonVariant **v, JsonVariant *element) {
+ _cleanup_(json_variant_unrefp) JsonVariant *nv = NULL;
+ bool blank;
+ int r;
+
+ assert(v);
+ assert(element);
+
+
+ if (!*v || json_variant_is_null(*v))
+ blank = true;
+ else if (!json_variant_is_array(*v))
+ return -EINVAL;
+ else
+ blank = json_variant_elements(*v) == 0;
+
+ if (blank)
+ r = json_variant_new_array(&nv, (JsonVariant*[]) { element }, 1);
+ else {
+ _cleanup_free_ JsonVariant **array = NULL;
+ size_t i;
+
+ array = new(JsonVariant*, json_variant_elements(*v));
+ if (array)
+ return -ENOMEM;
+
+ for (i = 0; i < json_variant_elements(*v); i++)
+ array[i] = json_variant_by_index(*v, i);
+
+ array[i] = element;
+
+ r = json_variant_new_array(&nv, array, i + 1);
+ }
+
+ if (r < 0)
+ return r;
+
+ json_variant_unref(*v);
+ *v = TAKE_PTR(nv);
+
+ return 0;
+}
+
+int json_variant_strv(JsonVariant *v, char ***ret) {
+ char **l = NULL;
+ size_t n, i;
+ bool sensitive;
+ int r;
+
+ assert(ret);
+
+ if (!v || json_variant_is_null(v)) {
+ l = new0(char*, 1);
+ if (!l)
+ return -ENOMEM;
+
+ *ret = l;
+ return 0;
+ }
+
+ if (!json_variant_is_array(v))
+ return -EINVAL;
+
+ sensitive = v->sensitive;
+
+ n = json_variant_elements(v);
+ l = new(char*, n+1);
+ if (!l)
+ return -ENOMEM;
+
+ for (i = 0; i < n; i++) {
+ JsonVariant *e;
+
+ assert_se(e = json_variant_by_index(v, i));
+ sensitive = sensitive || e->sensitive;
+
+ if (!json_variant_is_string(e)) {
+ l[i] = NULL;
+ r = -EINVAL;
+ goto fail;
+ }
+
+ l[i] = strdup(json_variant_string(e));
+ if (!l[i]) {
+ r = -ENOMEM;
+ goto fail;
+ }
+ }
+
+ l[i] = NULL;
+ *ret = TAKE_PTR(l);
+
+ return 0;
+
+fail:
+ if (sensitive)
+ strv_free_erase(l);
+ else
+ strv_free(l);
+
+ return r;
}
static int json_variant_copy(JsonVariant **nv, JsonVariant *v) {
@@ -1673,7 +2166,7 @@ static int json_variant_copy(JsonVariant **nv, JsonVariant *v) {
c->n_ref = 1;
c->type = t;
c->is_reference = true;
- c->reference = json_variant_ref(json_variant_normalize(v));
+ c->reference = json_variant_ref(json_variant_formalize(v));
*nv = c;
return 0;
@@ -2275,6 +2768,7 @@ static void json_stack_release(JsonStack *s) {
static int json_parse_internal(
const char **input,
JsonSource *source,
+ JsonParseFlags flags,
JsonVariant **ret,
unsigned *line,
unsigned *column,
@@ -2593,6 +3087,12 @@ static int json_parse_internal(
}
if (add) {
+ /* If we are asked to make this parsed object sensitive, then let's apply this
+ * immediately after allocating each variant, so that when we abort half-way
+ * everything we already allocated that is then freed is correctly marked. */
+ if (FLAGS_SET(flags, JSON_PARSE_SENSITIVE))
+ json_variant_sensitive(add);
+
(void) json_variant_set_source(&add, source, line_token, column_token);
if (!GREEDY_REALLOC(current->elements, current->n_elements_allocated, current->n_elements + 1)) {
@@ -2621,15 +3121,15 @@ static int json_parse_internal(
return r;
}
-int json_parse(const char *input, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
- return json_parse_internal(&input, NULL, ret, ret_line, ret_column, false);
+int json_parse(const char *input, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
+ return json_parse_internal(&input, NULL, flags, ret, ret_line, ret_column, false);
}
-int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
- return json_parse_internal(p, NULL, ret, ret_line, ret_column, true);
+int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
+ return json_parse_internal(p, NULL, flags, ret, ret_line, ret_column, true);
}
-int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
+int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
_cleanup_(json_source_unrefp) JsonSource *source = NULL;
_cleanup_free_ char *text = NULL;
const char *p;
@@ -2638,7 +3138,7 @@ int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_
if (f)
r = read_full_stream(f, &text, NULL);
else if (path)
- r = read_full_file(path, &text, NULL);
+ r = read_full_file_full(dir_fd, path, 0, &text, NULL);
else
return -EINVAL;
if (r < 0)
@@ -2651,7 +3151,7 @@ int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_
}
p = text;
- return json_parse_internal(&p, source, ret, ret_line, ret_column, false);
+ return json_parse_internal(&p, source, flags, ret, ret_line, ret_column, false);
}
int json_buildv(JsonVariant **ret, va_list ap) {
@@ -2875,6 +3375,36 @@ int json_buildv(JsonVariant **ret, va_list ap) {
break;
+ case _JSON_BUILD_VARIANT_ARRAY: {
+ JsonVariant **array;
+ size_t n;
+
+ if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) {
+ r = -EINVAL;
+ goto finish;
+ }
+
+ array = va_arg(ap, JsonVariant**);
+ n = va_arg(ap, size_t);
+
+ if (current->n_suppress == 0) {
+ r = json_variant_new_array(&add, array, n);
+ if (r < 0)
+ goto finish;
+ }
+
+ n_subtract = 1;
+
+ if (current->expect == EXPECT_TOPLEVEL)
+ current->expect = EXPECT_END;
+ else if (current->expect == EXPECT_OBJECT_VALUE)
+ current->expect = EXPECT_OBJECT_KEY;
+ else
+ assert(current->expect == EXPECT_ARRAY_ELEMENT);
+
+ break;
+ }
+
case _JSON_BUILD_LITERAL: {
const char *l;
@@ -2889,7 +3419,7 @@ int json_buildv(JsonVariant **ret, va_list ap) {
/* Note that we don't care for current->n_suppress here, we should generate parsing
* errors even in suppressed object properties */
- r = json_parse(l, &add, NULL, NULL);
+ r = json_parse(l, 0, &add, NULL, NULL);
if (r < 0)
goto finish;
} else
@@ -2986,6 +3516,36 @@ int json_buildv(JsonVariant **ret, va_list ap) {
break;
}
+ case _JSON_BUILD_BASE64: {
+ const void *p;
+ size_t n;
+
+ if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) {
+ r = -EINVAL;
+ goto finish;
+ }
+
+ p = va_arg(ap, const void *);
+ n = va_arg(ap, size_t);
+
+ if (current->n_suppress == 0) {
+ r = json_variant_new_base64(&add, p, n);
+ if (r < 0)
+ goto finish;
+ }
+
+ n_subtract = 1;
+
+ if (current->expect == EXPECT_TOPLEVEL)
+ current->expect = EXPECT_END;
+ else if (current->expect == EXPECT_OBJECT_VALUE)
+ current->expect = EXPECT_OBJECT_KEY;
+ else
+ assert(current->expect == EXPECT_ARRAY_ELEMENT);
+
+ break;
+ }
+
case _JSON_BUILD_OBJECT_BEGIN:
if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) {
@@ -3320,6 +3880,11 @@ int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchF
assert(variant);
assert(b);
+ if (json_variant_is_null(variant)) {
+ *b = -1;
+ return 0;
+ }
+
if (!json_variant_is_boolean(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a boolean.", strna(name));
@@ -3400,6 +3965,9 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla
if (!json_variant_is_string(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+ if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant)))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name));
+
r = free_and_strdup(s, json_variant_string(variant));
if (r < 0)
return json_log(variant, flags, r, "Failed to allocate string: %m");
@@ -3407,6 +3975,27 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla
return 0;
}
+int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ const char **s = userdata;
+
+ assert(variant);
+ assert(s);
+
+ if (json_variant_is_null(variant)) {
+ *s = NULL;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant)))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name));
+
+ *s = json_variant_string(variant);
+ return 0;
+}
+
int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
_cleanup_strv_free_ char **l = NULL;
char ***s = userdata;
@@ -3421,6 +4010,19 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags
return 0;
}
+ /* Let's be flexible here: accept a single string in place of a single-item array */
+ if (json_variant_is_string(variant)) {
+ if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant)))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name));
+
+ l = strv_new(json_variant_string(variant));
+ if (!l)
+ return log_oom();
+
+ strv_free_and_replace(*s, l);
+ return 0;
+ }
+
if (!json_variant_is_array(variant))
return json_log(variant, SYNTHETIC_ERRNO(EINVAL), flags, "JSON field '%s' is not an array.", strna(name));
@@ -3428,6 +4030,9 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags
if (!json_variant_is_string(e))
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string.");
+ if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(e)))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name));
+
r = strv_extend(&l, json_variant_string(e));
if (r < 0)
return json_log(e, flags, r, "Failed to append array element: %m");
@@ -3449,6 +4054,227 @@ int json_dispatch_variant(const char *name, JsonVariant *variant, JsonDispatchFl
return 0;
}
+int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uid_t *uid = userdata;
+ uintmax_t k;
+
+ assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+ assert_cc(sizeof(gid_t) == sizeof(uint32_t));
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wtype-limits"
+ assert_cc(((uid_t) -1 < (uid_t) 0) == ((gid_t) -1 < (gid_t) 0));
+#pragma GCC diagnostic pop
+
+ if (json_variant_is_null(variant)) {
+ *uid = UID_INVALID;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k > UINT32_MAX || !uid_is_valid(k))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid UID/GID.", strna(name));
+
+ *uid = k;
+ return 0;
+}
+
+int json_dispatch_user_group_name(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!valid_user_group_name(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid user/group name.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ sd_id128_t *uuid = userdata;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *uuid = SD_ID128_NULL;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ r = sd_id128_from_string(json_variant_string(variant), uuid);
+ if (r < 0)
+ return json_log(variant, flags, r, "JSON field '%s' is not a valid UID.", strna(name));
+
+ return 0;
+}
+
+int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not allowed in this object.", strna(name));
+}
+
+static int json_cmp_strings(const void *x, const void *y) {
+ JsonVariant *const *a = x, *const *b = y;
+
+ if (!json_variant_is_string(*a) || !json_variant_is_string(*b))
+ return CMP(*a, *b);
+
+ return strcmp(json_variant_string(*a), json_variant_string(*b));
+}
+
+int json_variant_sort(JsonVariant **v) {
+ _cleanup_free_ JsonVariant **a = NULL;
+ JsonVariant *n = NULL;
+ size_t i, m;
+ int r;
+
+ assert(v);
+
+ if (json_variant_is_sorted(*v))
+ return 0;
+
+ if (!json_variant_is_object(*v))
+ return -EMEDIUMTYPE;
+
+ /* Sorts they key/value pairs in an object variant */
+
+ m = json_variant_elements(*v);
+ a = new(JsonVariant*, m);
+ if (!a)
+ return -ENOMEM;
+
+ for (i = 0; i < m; i++)
+ a[i] = json_variant_by_index(*v, i);
+
+ qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings);
+
+ r = json_variant_new_object(&n, a, m);
+ if (r < 0)
+ return r;
+ if (!n->sorted) /* Check if this worked. This will fail if there are multiple identical keys used. */
+ return -ENOTUNIQ;
+
+ json_variant_unref(*v);
+ *v = n;
+
+ return 1;
+}
+
+int json_variant_normalize(JsonVariant **v) {
+ _cleanup_free_ JsonVariant **a = NULL;
+ JsonVariant *n = NULL;
+ size_t i, j, m;
+ int r;
+
+ assert(v);
+
+ if (json_variant_is_normalized(*v))
+ return 0;
+
+ if (!json_variant_is_object(*v) && !json_variant_is_array(*v))
+ return -EMEDIUMTYPE;
+
+ /* Sorts the key/value pairs in an object variant anywhere down the tree in the specified variant */
+
+ m = json_variant_elements(*v);
+ a = new(JsonVariant*, m);
+ if (!a)
+ return -ENOMEM;
+
+ for (i = 0; i < m; i++) {
+ a[i] = json_variant_ref(json_variant_by_index(*v, i));
+
+ r = json_variant_normalize(a + i);
+ if (r < 0)
+ goto finish;
+ }
+
+ qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings);
+
+ if (json_variant_is_object(*v))
+ r = json_variant_new_object(&n, a, m);
+ else {
+ assert(json_variant_is_array(*v));
+ r = json_variant_new_array(&n, a, m);
+ }
+ if (r < 0)
+ goto finish;
+ if (!n->normalized) { /* Let's see if normalization worked. It will fail if there are multiple
+ * identical keys used in the same object anywhere, or if there are floating
+ * point numbers used (see below) */
+ r = -ENOTUNIQ;
+ goto finish;
+ }
+
+ json_variant_unref(*v);
+ *v = n;
+
+ r = 1;
+
+finish:
+ for (j = 0; j < i; j++)
+ json_variant_unref(a[j]);
+
+ return r;
+}
+
+bool json_variant_is_normalized(JsonVariant *v) {
+
+ /* For now, let's consider anything containing numbers not expressible as integers as
+ * non-normalized. That's because we cannot sensibly compare them due to accuracy issues, nor even
+ * store them if they are too large. */
+ if (json_variant_is_real(v) && !json_variant_is_integer(v) && !json_variant_is_unsigned(v))
+ return false;
+
+ /* The concept only applies to variants that include other variants, i.e. objects and arrays. All
+ * others are normalized anyway. */
+ if (!json_variant_is_object(v) && !json_variant_is_array(v))
+ return true;
+
+ /* Empty objects/arrays don't include any other variant, hence are always normalized too */
+ if (json_variant_elements(v) == 0)
+ return true;
+
+ return v->normalized; /* For everything else there's an explicit boolean we maintain */
+}
+
+bool json_variant_is_sorted(JsonVariant *v) {
+
+ /* Returns true if all key/value pairs of an object are properly sorted. Note that this only applies
+ * to objects, not arrays. */
+
+ if (!json_variant_is_object(v))
+ return true;
+ if (json_variant_elements(v) <= 1)
+ return true;
+
+ return v->sorted;
+}
+
+int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size) {
+
+ if (!json_variant_is_string(v))
+ return -EINVAL;
+
+ return unbase64mem(json_variant_string(v), (size_t) -1, ret, ret_size);
+}
+
static const char* const json_variant_type_table[_JSON_VARIANT_TYPE_MAX] = {
[JSON_VARIANT_STRING] = "string",
[JSON_VARIANT_INTEGER] = "integer",
diff --git a/src/shared/json.h b/src/shared/json.h
index 1f9c620ebb816..71f7e606b2086 100644
--- a/src/shared/json.h
+++ b/src/shared/json.h
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
+#include
#include
#include
#include
@@ -54,6 +55,7 @@ typedef enum JsonVariantType {
} JsonVariantType;
int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n);
+int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n);
int json_variant_new_integer(JsonVariant **ret, intmax_t i);
int json_variant_new_unsigned(JsonVariant **ret, uintmax_t u);
int json_variant_new_real(JsonVariant **ret, long double d);
@@ -120,6 +122,9 @@ static inline bool json_variant_is_null(JsonVariant *v) {
}
bool json_variant_is_negative(JsonVariant *v);
+bool json_variant_is_blank_object(JsonVariant *v);
+bool json_variant_is_normalized(JsonVariant *v);
+bool json_variant_is_sorted(JsonVariant *v);
size_t json_variant_elements(JsonVariant *v);
JsonVariant *json_variant_by_index(JsonVariant *v, size_t index);
@@ -128,6 +133,8 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria
bool json_variant_equal(JsonVariant *a, JsonVariant *b);
+void json_variant_sensitive(JsonVariant *v);
+
struct json_variant_foreach_state {
JsonVariant *variant;
size_t idx;
@@ -153,21 +160,48 @@ struct json_variant_foreach_state {
int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column);
typedef enum JsonFormatFlags {
- JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */
- JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */
- JSON_FORMAT_COLOR = 1 << 2, /* insert ANSI color sequences */
- JSON_FORMAT_COLOR_AUTO = 1 << 3, /* insert ANSI color sequences if colors_enabled() says so */
- JSON_FORMAT_SOURCE = 1 << 4, /* prefix with source filename/line/column */
- JSON_FORMAT_SSE = 1 << 5, /* prefix/suffix with W3C server-sent events */
- JSON_FORMAT_SEQ = 1 << 6, /* prefix/suffix with RFC 7464 application/json-seq */
+ JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */
+ JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */
+ JSON_FORMAT_PRETTY_AUTO = 1 << 2, /* same, but only if connected to a tty (and JSON_FORMAT_NEWLINE otherwise) */
+ JSON_FORMAT_COLOR = 1 << 3, /* insert ANSI color sequences */
+ JSON_FORMAT_COLOR_AUTO = 1 << 4, /* insert ANSI color sequences if colors_enabled() says so */
+ JSON_FORMAT_SOURCE = 1 << 5, /* prefix with source filename/line/column */
+ JSON_FORMAT_SSE = 1 << 6, /* prefix/suffix with W3C server-sent events */
+ JSON_FORMAT_SEQ = 1 << 7, /* prefix/suffix with RFC 7464 application/json-seq */
+ JSON_FORMAT_FLUSH = 1 << 8, /* call fflush() after dumping JSON */
} JsonFormatFlags;
int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret);
void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix);
-int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
-int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
-int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
+int json_variant_filter(JsonVariant **v, char **to_remove);
+
+int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value);
+int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value);
+int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t value);
+int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value);
+int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b);
+
+int json_variant_append_array(JsonVariant **v, JsonVariant *element);
+
+int json_variant_merge(JsonVariant **v, JsonVariant *m);
+
+int json_variant_strv(JsonVariant *v, char ***ret);
+
+int json_variant_sort(JsonVariant **v);
+int json_variant_normalize(JsonVariant **v);
+
+typedef enum JsonParseFlags {
+ JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */
+} JsonParseFlags;
+
+int json_parse(const char *string, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
+int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
+int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column);
+
+static inline int json_parse_file(FILE *f, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) {
+ return json_parse_file_at(f, AT_FDCWD, path, flags, ret, ret_line, ret_column);
+}
enum {
_JSON_BUILD_STRING,
@@ -183,8 +217,10 @@ enum {
_JSON_BUILD_PAIR_CONDITION,
_JSON_BUILD_NULL,
_JSON_BUILD_VARIANT,
+ _JSON_BUILD_VARIANT_ARRAY,
_JSON_BUILD_LITERAL,
_JSON_BUILD_STRV,
+ _JSON_BUILD_BASE64,
_JSON_BUILD_MAX,
};
@@ -194,13 +230,17 @@ enum {
#define JSON_BUILD_REAL(d) _JSON_BUILD_REAL, ({ long double _x = d; _x; })
#define JSON_BUILD_BOOLEAN(b) _JSON_BUILD_BOOLEAN, ({ bool _x = b; _x; })
#define JSON_BUILD_ARRAY(...) _JSON_BUILD_ARRAY_BEGIN, __VA_ARGS__, _JSON_BUILD_ARRAY_END
+#define JSON_BUILD_EMPTY_ARRAY _JSON_BUILD_ARRAY_BEGIN, _JSON_BUILD_ARRAY_END
#define JSON_BUILD_OBJECT(...) _JSON_BUILD_OBJECT_BEGIN, __VA_ARGS__, _JSON_BUILD_OBJECT_END
+#define JSON_BUILD_EMPTY_OBJECT _JSON_BUILD_OBJECT_BEGIN, _JSON_BUILD_OBJECT_END
#define JSON_BUILD_PAIR(n, ...) _JSON_BUILD_PAIR, ({ const char *_x = n; _x; }), __VA_ARGS__
#define JSON_BUILD_PAIR_CONDITION(c, n, ...) _JSON_BUILD_PAIR_CONDITION, ({ bool _x = c; _x; }), ({ const char *_x = n; _x; }), __VA_ARGS__
#define JSON_BUILD_NULL _JSON_BUILD_NULL
#define JSON_BUILD_VARIANT(v) _JSON_BUILD_VARIANT, ({ JsonVariant *_x = v; _x; })
+#define JSON_BUILD_VARIANT_ARRAY(v, n) _JSON_BUILD_VARIANT_ARRAY, ({ JsonVariant **_x = v; _x; }), ({ size_t _y = n; _y; })
#define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; })
#define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; })
+#define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; })
int json_build(JsonVariant **ret, ...);
int json_buildv(JsonVariant **ret, va_list ap);
@@ -213,10 +253,11 @@ typedef enum JsonDispatchFlags {
JSON_PERMISSIVE = 1 << 0, /* Shall parsing errors be considered fatal for this property? */
JSON_MANDATORY = 1 << 1, /* Should existence of this property be mandatory? */
JSON_LOG = 1 << 2, /* Should the parser log about errors? */
+ JSON_SAFE = 1 << 3, /* Don't accept "unsafe" strings in json_dispatch_string() + json_dispatch_string() */
/* The following two may be passed into log_json() in addition to the three above */
- JSON_DEBUG = 1 << 3, /* Indicates that this log message is a debug message */
- JSON_WARNING = 1 << 4, /* Indicates that this log message is a warning message */
+ JSON_DEBUG = 1 << 4, /* Indicates that this log message is a debug message */
+ JSON_WARNING = 1 << 5, /* Indicates that this log message is a warning message */
} JsonDispatchFlags;
typedef int (*JsonDispatchCallback)(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
@@ -232,6 +273,7 @@ typedef struct JsonDispatch {
int json_dispatch(JsonVariant *v, const JsonDispatch table[], JsonDispatchCallback bad, JsonDispatchFlags flags, void *userdata);
int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_boolean(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
@@ -240,6 +282,10 @@ int json_dispatch_integer(const char *name, JsonVariant *variant, JsonDispatchFl
int json_dispatch_unsigned(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_uint32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_int32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_user_group_name(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
assert_cc(sizeof(uintmax_t) == sizeof(uint64_t));
#define json_dispatch_uint64 json_dispatch_unsigned
@@ -275,6 +321,9 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi
: -ERRNO_VALUE(_e); \
})
+#define json_log_oom(variant, flags) \
+ json_log(variant, flags, SYNTHETIC_ERRNO(ENOMEM), "Out of memory.")
+
#define JSON_VARIANT_STRING_CONST(x) _JSON_VARIANT_STRING_CONST(UNIQ, (x))
#define _JSON_VARIANT_STRING_CONST(xq, x) \
@@ -284,5 +333,7 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi
(JsonVariant*) ((uintptr_t) UNIQ_T(json_string_const, xq) + 1); \
})
+int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size);
+
const char *json_variant_type_to_string(JsonVariantType t);
JsonVariantType json_variant_type_from_string(const char *s);
diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c
index 22525d5c63303..6952a7f654de0 100644
--- a/src/shared/loop-util.c
+++ b/src/shared/loop-util.c
@@ -2,26 +2,35 @@
#include
#include
+#include
+#include
#include
+#include
#include
#include
#include "alloc-util.h"
#include "fd-util.h"
+#include "fileio.h"
#include "loop-util.h"
+#include "parse-util.h"
#include "stat-util.h"
-int loop_device_make(int fd, int open_flags, LoopDevice **ret) {
- const struct loop_info64 info = {
- .lo_flags = LO_FLAGS_AUTOCLEAR|LO_FLAGS_PARTSCAN|(open_flags == O_RDONLY ? LO_FLAGS_READ_ONLY : 0),
- };
+int loop_device_make_full(
+ int fd,
+ int open_flags,
+ uint64_t offset,
+ uint64_t size,
+ uint32_t loop_flags,
+ LoopDevice **ret) {
_cleanup_close_ int control = -1, loop = -1;
_cleanup_free_ char *loopdev = NULL;
unsigned n_attempts = 0;
+ struct loop_info64 info;
+ LoopDevice *d = NULL;
struct stat st;
- LoopDevice *d;
- int nr, r;
+ int nr = -1, r;
assert(fd >= 0);
assert(ret);
@@ -31,31 +40,42 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) {
return -errno;
if (S_ISBLK(st.st_mode)) {
- int copy;
+ if (ioctl(loop, LOOP_GET_STATUS64, &info) >= 0) {
+ /* Oh! This is a loopback device? That's interesting! */
+ nr = info.lo_number;
- /* If this is already a block device, store a copy of the fd as it is */
+ if (asprintf(&loopdev, "/dev/loop%i", nr) < 0)
+ return -ENOMEM;
+ }
- copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
- if (copy < 0)
- return -errno;
+ if (offset == 0 && IN_SET(size, 0, UINT64_MAX)) {
+ int copy;
- d = new0(LoopDevice, 1);
- if (!d)
- return -ENOMEM;
+ /* If this is already a block device, store a copy of the fd as it is */
- *d = (LoopDevice) {
- .fd = copy,
- .nr = -1,
- .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */
- };
+ copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (copy < 0)
+ return -errno;
- *ret = d;
- return d->fd;
- }
+ d = new0(LoopDevice, 1);
+ if (!d)
+ return -ENOMEM;
- r = stat_verify_regular(&st);
- if (r < 0)
- return r;
+ *d = (LoopDevice) {
+ .fd = copy,
+ .nr = nr,
+ .node = TAKE_PTR(loopdev),
+ .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */
+ };
+
+ *ret = d;
+ return d->fd;
+ }
+ } else {
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return r;
+ }
control = open("/dev/loop-control", O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK);
if (control < 0)
@@ -87,12 +107,23 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) {
loop = safe_close(loop);
}
- if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0)
- return -errno;
+ info = (struct loop_info64) {
+ /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */
+ .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((loop_flags & O_ACCMODE) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR,
+ .lo_offset = offset,
+ .lo_sizelimit = size == UINT64_MAX ? 0 : size,
+ };
+
+ if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) {
+ r = -errno;
+ goto fail;
+ }
d = new(LoopDevice, 1);
- if (!d)
- return -ENOMEM;
+ if (!d) {
+ r = -ENOMEM;
+ goto fail;
+ }
*d = (LoopDevice) {
.fd = TAKE_FD(loop),
@@ -102,9 +133,17 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) {
*ret = d;
return d->fd;
+
+fail:
+ if (fd >= 0)
+ (void) ioctl(fd, LOOP_CLR_FD);
+ if (d && d->fd >= 0)
+ (void) ioctl(d->fd, LOOP_CLR_FD);
+
+ return r;
}
-int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret) {
+int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret) {
_cleanup_close_ int fd = -1;
assert(path);
@@ -115,7 +154,7 @@ int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret)
if (fd < 0)
return -errno;
- return loop_device_make(fd, open_flags, ret);
+ return loop_device_make(fd, open_flags, loop_flags, ret);
}
LoopDevice* loop_device_unref(LoopDevice *d) {
@@ -157,3 +196,188 @@ void loop_device_relinquish(LoopDevice *d) {
d->relinquished = true;
}
+
+int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) {
+ _cleanup_close_ int loop_fd = -1;
+ _cleanup_free_ char *p = NULL;
+ struct loop_info64 info;
+ struct stat st;
+ LoopDevice *d;
+ int nr;
+
+ assert(loop_path);
+ assert(ret);
+
+ loop_fd = open(loop_path, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|open_flags);
+ if (loop_fd < 0)
+ return -errno;
+
+ if (fstat(loop_fd, &st) < 0)
+ return -errno;
+ if (!S_ISBLK(st.st_mode))
+ return -ENOTBLK;
+
+ if (ioctl(loop_fd, LOOP_GET_STATUS64, &info) >= 0)
+ nr = info.lo_number;
+ else
+ nr = -1;
+
+ p = strdup(loop_path);
+ if (!p)
+ return -ENOMEM;
+
+ d = new(LoopDevice, 1);
+ if (!d)
+ return -ENOMEM;
+
+ *d = (LoopDevice) {
+ .fd = TAKE_FD(loop_fd),
+ .nr = nr,
+ .node = TAKE_PTR(p),
+ .relinquished = true, /* It's not ours, don't try to destroy it when this object is freed */
+ };
+
+ *ret = d;
+ return d->fd;
+}
+
+static int resize_partition(int partition_fd, uint64_t offset, uint64_t size) {
+ _cleanup_free_ char *sysfs = NULL, *whole = NULL, *buffer = NULL;
+ uint64_t current_offset, current_size, partno;
+ _cleanup_close_ int whole_fd = -1;
+ struct blkpg_ioctl_arg ba;
+ struct blkpg_partition bp;
+ struct stat st;
+ dev_t devno;
+ int r;
+
+ assert(partition_fd >= 0);
+
+ /* Resizes the partition the loopback device refer to (assuming it refers to one instead of an actual
+ * loopback device), and changes the offset, if needed. This is a fancy wrapper around
+ * BLKPG_RESIZE_PARTITION. */
+
+ if (fstat(partition_fd, &st) < 0)
+ return -errno;
+
+ assert(S_ISBLK(st.st_mode));
+
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return -ENOMEM;
+
+ r = read_one_line_file(sysfs, &buffer);
+ if (r == -ENOENT) /* not a partition, cannot resize */
+ return -ENOTTY;
+ if (r < 0)
+ return r;
+ r = safe_atou64(buffer, &partno);
+ if (r < 0)
+ return r;
+
+ sysfs = mfree(sysfs);
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return -ENOMEM;
+
+ buffer = mfree(buffer);
+ r = read_one_line_file(sysfs, &buffer);
+ if (r < 0)
+ return r;
+ r = safe_atou64(buffer, ¤t_offset);
+ if (r < 0)
+ return r;
+ if (current_offset > UINT64_MAX/512U)
+ return -EINVAL;
+ current_offset *= 512U;
+
+ if (ioctl(partition_fd, BLKGETSIZE64, ¤t_size) < 0)
+ return -EINVAL;
+
+ if (size == UINT64_MAX && offset == UINT64_MAX)
+ return 0;
+ if (current_size == size && current_offset == offset)
+ return 0;
+
+ sysfs = mfree(sysfs);
+ if (asprintf(&sysfs, "/sys/dev/block/%u:%u/../dev", major(st.st_rdev), minor(st.st_rdev)) < 0)
+ return -ENOMEM;
+
+ buffer = mfree(buffer);
+ r = read_one_line_file(sysfs, &buffer);
+ if (r < 0)
+ return r;
+ r = parse_dev(buffer, &devno);
+ if (r < 0)
+ return r;
+
+ r = device_path_make_major_minor(S_IFBLK, devno, &whole);
+ if (r < 0)
+ return r;
+
+ whole_fd = open(whole, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (whole_fd < 0)
+ return -errno;
+
+ bp = (struct blkpg_partition) {
+ .pno = partno,
+ .start = offset == UINT64_MAX ? current_offset : offset,
+ .length = size == UINT64_MAX ? current_size : size,
+ };
+
+ ba = (struct blkpg_ioctl_arg) {
+ .op = BLKPG_RESIZE_PARTITION,
+ .data = &bp,
+ .datalen = sizeof(bp),
+ };
+
+ if (ioctl(whole_fd, BLKPG, &ba) < 0)
+ return -errno;
+
+ return 0;
+}
+
+int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size) {
+ struct loop_info64 info;
+ assert(d);
+
+ /* Changes the offset/start of the loop device relative to the beginning of the underlying file or
+ * block device. If this loop device actually refers to a partition and not a loopback device, we'll
+ * try to adjust the partition offsets instead.
+ *
+ * If either offset or size is UINT64_MAX we won't change that parameter. */
+
+ if (d->fd < 0)
+ return -EBADF;
+
+ if (d->nr < 0) /* not a loopback device */
+ return resize_partition(d->fd, offset, size);
+
+ if (ioctl(d->fd, LOOP_GET_STATUS64, &info) < 0)
+ return -errno;
+
+ if (size == UINT64_MAX && offset == UINT64_MAX)
+ return 0;
+ if (info.lo_sizelimit == size && info.lo_offset == offset)
+ return 0;
+
+ if (size != UINT64_MAX)
+ info.lo_sizelimit = size;
+ if (offset != UINT64_MAX)
+ info.lo_offset = offset;
+
+ if (ioctl(d->fd, LOOP_SET_STATUS64, &info) < 0)
+ return -errno;
+
+ return 0;
+}
+
+int loop_device_flock(LoopDevice *d, int operation) {
+ assert(d);
+
+ if (d->fd < 0)
+ return -EBADF;
+
+ if (flock(d->fd, operation) < 0)
+ return -errno;
+
+ return 0;
+}
diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h
index d78466c5ee683..5156b46ad611a 100644
--- a/src/shared/loop-util.h
+++ b/src/shared/loop-util.h
@@ -14,10 +14,19 @@ struct LoopDevice {
bool relinquished;
};
-int loop_device_make(int fd, int open_flags, LoopDevice **ret);
-int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret);
+int loop_device_make_full(int fd, int open_flags, uint64_t offset, uint64_t size, uint32_t loop_flags, LoopDevice **ret);
+static inline int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret) {
+ return loop_device_make_full(fd, open_flags, 0, 0, loop_flags, ret);
+}
+
+int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret);
+int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret);
LoopDevice* loop_device_unref(LoopDevice *d);
DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref);
void loop_device_relinquish(LoopDevice *d);
+
+int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size);
+
+int loop_device_flock(LoopDevice *d, int operation);
diff --git a/src/shared/machine-image.c b/src/shared/machine-image.c
index 7007374192796..4e0bea41d1a57 100644
--- a/src/shared/machine-image.c
+++ b/src/shared/machine-image.c
@@ -3,6 +3,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -10,7 +12,6 @@
#include
#include
#include
-#include
#include "alloc-util.h"
#include "btrfs-util.h"
@@ -1168,7 +1169,7 @@ int image_read_metadata(Image *i) {
_cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
_cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
- r = loop_device_make_by_path(i->path, O_RDONLY, &d);
+ r = loop_device_make_by_path(i->path, O_RDONLY, LO_FLAGS_PARTSCAN, &d);
if (r < 0)
return r;
diff --git a/src/shared/main-func.h b/src/shared/main-func.h
index 6c26cb9fb5673..cf23ad450c5ce 100644
--- a/src/shared/main-func.h
+++ b/src/shared/main-func.h
@@ -3,6 +3,8 @@
#include
+#include "sd-daemon.h"
+
#include "pager.h"
#include "selinux-util.h"
#include "spawn-ask-password-agent.h"
@@ -16,6 +18,8 @@
save_argc_argv(argc, argv); \
intro; \
r = impl; \
+ if (r < 0) \
+ (void) sd_notifyf(0, "ERRNO=%i", -r); \
ask_password_agent_close(); \
polkit_agent_close(); \
pager_close(); \
diff --git a/src/shared/meson.build b/src/shared/meson.build
index e9005a30e3a52..1dc6a721c4461 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -35,6 +35,8 @@ shared_sources = files('''
calendarspec.h
cgroup-show.c
cgroup-show.h
+ chown-recursive.c
+ chown-recursive.h
clean-ipc.c
clean-ipc.h
clock-util.c
@@ -82,6 +84,12 @@ shared_sources = files('''
generator.c
generator.h
gpt.h
+ group-record-nss.c
+ group-record-nss.h
+ group-record-show.c
+ group-record-show.h
+ group-record.c
+ group-record.h
id128-print.c
id128-print.h
ima-util.c
@@ -145,6 +153,8 @@ shared_sources = files('''
ptyfwd.h
reboot-util.c
reboot-util.h
+ resize-fs.c
+ resize-fs.h
resolve-util.c
resolve-util.h
seccomp-util.h
@@ -174,6 +184,14 @@ shared_sources = files('''
uid-range.h
unit-file.c
unit-file.h
+ user-record-nss.c
+ user-record-nss.h
+ user-record-show.c
+ user-record-show.h
+ user-record.c
+ user-record.h
+ userdb.c
+ userdb.h
utmp-wtmp.h
varlink.c
varlink.h
@@ -218,6 +236,10 @@ if conf.get('HAVE_KMOD') == 1
shared_sources += files('module-util.c')
endif
+if conf.get('HAVE_PAM') == 1
+ shared_sources += files('pam-util.c', 'pam-util.h')
+endif
+
generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh')
ip_protocol_list_txt = custom_target(
'ip-protocol-list.txt',
@@ -274,7 +296,8 @@ libshared_deps = [threads,
libidn,
libxz,
liblz4,
- libblkid]
+ libblkid,
+ libpam]
libshared_sym_path = '@0@/libshared.sym'.format(meson.current_source_dir())
diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c
new file mode 100644
index 0000000000000..f000798ce0683
--- /dev/null
+++ b/src/shared/pam-util.c
@@ -0,0 +1,81 @@
+#include
+#include
+#include
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "macro.h"
+#include "pam-util.h"
+
+int pam_log_oom(pam_handle_t *handle) {
+ /* This is like log_oom(), but uses PAM logging */
+ pam_syslog(handle, LOG_ERR, "Out of memory.");
+ return PAM_BUF_ERR;
+}
+
+int pam_bus_log_create_error(pam_handle_t *handle, int r) {
+ /* This is like bus_log_create_error(), but uses PAM logging */
+ pam_syslog(handle, LOG_ERR, "Failed to create bus message: %s", strerror_safe(r));
+ return PAM_BUF_ERR;
+}
+
+int pam_bus_log_parse_error(pam_handle_t *handle, int r) {
+ /* This is like bus_log_parse_error(), but uses PAM logging */
+ pam_syslog(handle, LOG_ERR, "Failed to parse bus message: %s", strerror_safe(r));
+ return PAM_BUF_ERR;
+}
+
+static void cleanup_system_bus(pam_handle_t *handle, void *data, int error_status) {
+ sd_bus_flush_close_unref(data);
+}
+
+int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret) {
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ assert(handle);
+ assert(ret);
+
+ /* We cache the bus connection so that we can share it between the session and the authentication hooks */
+ r = pam_get_data(handle, "systemd-system-bus", (const void**) &bus);
+ if (r == PAM_SUCCESS && bus) {
+ *ret = sd_bus_ref(TAKE_PTR(bus)); /* Increase the reference counter, so that the PAM data stays valid */
+ return PAM_SUCCESS;
+ }
+ if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
+ pam_syslog(handle, LOG_ERR, "Failed to get bus connection: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
+ return PAM_SERVICE_ERR;
+ }
+
+ r = pam_set_data(handle, "systemd-system-bus", bus, cleanup_system_bus);
+ if (r != PAM_SUCCESS) {
+ pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r));
+ return r;
+ }
+
+ sd_bus_ref(bus);
+ *ret = TAKE_PTR(bus);
+
+ return PAM_SUCCESS;
+}
+
+int pam_release_bus_connection(pam_handle_t *handle) {
+ int r;
+
+ r = pam_set_data(handle, "systemd-system-bus", NULL, NULL);
+ if (r != PAM_SUCCESS)
+ pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r));
+
+ return r;
+}
+
+void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) {
+ /* A generic destructor for pam_set_data() that just frees the specified data */
+ free(data);
+}
diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h
new file mode 100644
index 0000000000000..26d07b7f0cbed
--- /dev/null
+++ b/src/shared/pam-util.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "sd-bus.h"
+
+int pam_log_oom(pam_handle_t *handle);
+int pam_bus_log_create_error(pam_handle_t *handle, int r);
+int pam_bus_log_parse_error(pam_handle_t *handle, int r);
+
+int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret);
+int pam_release_bus_connection(pam_handle_t *handle);
+
+void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status);
diff --git a/src/shared/resize-fs.c b/src/shared/resize-fs.c
new file mode 100644
index 0000000000000..9f33dd77d88a3
--- /dev/null
+++ b/src/shared/resize-fs.c
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+#include
+#include
+
+#include "blockdev-util.h"
+#include "fs-util.h"
+#include "missing_fs.h"
+#include "missing_magic.h"
+#include "missing_xfs.h"
+#include "resize-fs.h"
+#include "stat-util.h"
+
+int resize_fs(int fd, uint64_t sz) {
+ struct statfs sfs;
+ int r;
+
+ assert(fd >= 0);
+
+ /* Rounds down to next block size */
+
+ if (sz <= 0 || sz == UINT64_MAX)
+ return -ERANGE;
+
+ if (fstatfs(fd, &sfs) < 0)
+ return -errno;
+
+ if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) {
+ uint64_t u;
+
+ if (sz < EXT4_MINIMAL_SIZE)
+ return -ERANGE;
+
+ u = sz / sfs.f_bsize;
+
+ if (ioctl(fd, EXT4_IOC_RESIZE_FS, &u) < 0)
+ return -errno;
+
+ } else if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) {
+ struct btrfs_ioctl_vol_args args = {};
+
+ /* 256M is the minimize size enforced by the btrfs kernel code when resizing (which is
+ * strange btw, as mkfs.btrfs is fine creating file systems > 109M). It will return EINVAL in
+ * that case, let's catch this error beforehand though, and report a more explanatory
+ * error. */
+
+ if (sz < BTRFS_MINIMAL_SIZE)
+ return -ERANGE;
+
+ r = snprintf(args.name, sizeof(args.name), "%" PRIu64, sz);
+ assert((size_t) r < sizeof(args.name));
+
+ if (ioctl(fd, BTRFS_IOC_RESIZE, &args) < 0)
+ return -errno;
+
+ } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) {
+ xfs_fsop_geom_t geo;
+ xfs_growfs_data_t d;
+
+ if (sz < XFS_MINIMAL_SIZE)
+ return -ERANGE;
+
+ if (ioctl(fd, XFS_IOC_FSGEOMETRY, &geo) < 0)
+ return -errno;
+
+ d = (xfs_growfs_data_t) {
+ .imaxpct = geo.imaxpct,
+ .newblocks = sz / geo.blocksize,
+ };
+
+ if (ioctl(fd, XFS_IOC_FSGROWFSDATA, &d) < 0)
+ return -errno;
+
+ } else
+ return -EOPNOTSUPP;
+
+ return 0;
+}
+
+uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic) {
+
+ switch (magic) {
+
+ case (statfs_f_type_t) EXT4_SUPER_MAGIC:
+ return EXT4_MINIMAL_SIZE;
+
+ case (statfs_f_type_t) XFS_SB_MAGIC:
+ return XFS_MINIMAL_SIZE;
+
+ case (statfs_f_type_t) BTRFS_SUPER_MAGIC:
+ return BTRFS_MINIMAL_SIZE;
+
+ default:
+ return UINT64_MAX;
+ }
+}
+
+uint64_t minimal_size_by_fs_name(const char *name) {
+
+ if (streq_ptr(name, "ext4"))
+ return EXT4_MINIMAL_SIZE;
+
+ if (streq_ptr(name, "xfs"))
+ return XFS_MINIMAL_SIZE;
+
+ if (streq_ptr(name, "btrfs"))
+ return BTRFS_MINIMAL_SIZE;
+
+ return UINT64_MAX;
+}
diff --git a/src/shared/resize-fs.h b/src/shared/resize-fs.h
new file mode 100644
index 0000000000000..b5441765289ec
--- /dev/null
+++ b/src/shared/resize-fs.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "stat-util.h"
+
+int resize_fs(int fd, uint64_t sz);
+
+#define BTRFS_MINIMAL_SIZE (256U*1024U*1024U)
+#define XFS_MINIMAL_SIZE (14U*1024U*1024U)
+#define EXT4_MINIMAL_SIZE (1024U*1024U)
+
+uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic);
+uint64_t minimal_size_by_fs_name(const char *str);
diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c
new file mode 100644
index 0000000000000..2d881fec8a0ee
--- /dev/null
+++ b/src/shared/user-record-nss.c
@@ -0,0 +1,288 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "errno-util.h"
+#include "format-util.h"
+#include "strv.h"
+#include "user-record-nss.h"
+#include "user-util.h"
+
+int nss_passwd_to_user_record(
+ const struct passwd *pwd,
+ const struct spwd *spwd,
+ UserRecord **ret) {
+
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ int r;
+
+ assert(pwd);
+ assert(ret);
+
+ if (isempty(pwd->pw_name))
+ return -EINVAL;
+
+ if (spwd && !streq_ptr(spwd->sp_namp, pwd->pw_name))
+ return -EINVAL;
+
+ hr = user_record_new();
+ if (!hr)
+ return -ENOMEM;
+
+ r = free_and_strdup(&hr->user_name, pwd->pw_name);
+ if (r < 0)
+ return r;
+
+ if (isempty(pwd->pw_gecos) || streq_ptr(pwd->pw_gecos, hr->user_name))
+ hr->real_name = mfree(hr->real_name);
+ else {
+ r = free_and_strdup(&hr->real_name, pwd->pw_gecos);
+ if (r < 0)
+ return r;
+ }
+
+ if (isempty(pwd->pw_dir))
+ hr->home_directory = mfree(hr->home_directory);
+ else {
+ r = free_and_strdup(&hr->home_directory, pwd->pw_dir);
+ if (r < 0)
+ return r;
+ }
+
+ if (isempty(pwd->pw_shell))
+ hr->shell = mfree(hr->shell);
+ else {
+ r = free_and_strdup(&hr->shell, pwd->pw_shell);
+ if (r < 0)
+ return r;
+ }
+
+ hr->uid = pwd->pw_uid;
+ hr->gid = pwd->pw_gid;
+
+ if (spwd) {
+ if (hashed_password_valid(spwd->sp_pwdp)) {
+ strv_free_erase(hr->hashed_password);
+ hr->hashed_password = strv_new(spwd->sp_pwdp);
+ if (!hr->hashed_password)
+ return -ENOMEM;
+ } else
+ hr->hashed_password = strv_free_erase(hr->hashed_password);
+
+ /* shadow-utils suggests using "chage -E 0" (or -E 1, depending on which man page you check)
+ * for locking a whole account, hence check for that. Note that it also defines a way to lock
+ * just a password instead of the whole account, but that's mostly pointless in times of
+ * password-less authorization, hence let's not bother. */
+
+ if (spwd->sp_expire >= 0)
+ hr->locked = spwd->sp_expire <= 1;
+ else
+ hr->locked = -1;
+
+ if (spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->not_after_usec = spwd->sp_expire * USEC_PER_DAY;
+ else
+ hr->not_after_usec = UINT64_MAX;
+
+ if (spwd->sp_lstchg >= 0)
+ hr->password_change_now = spwd->sp_lstchg == 0;
+ else
+ hr->password_change_now = -1;
+
+ if (spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->last_password_change_usec = spwd->sp_lstchg * USEC_PER_DAY;
+ else
+ hr->last_password_change_usec = UINT64_MAX;
+
+ if (spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->password_change_min_usec = spwd->sp_min * USEC_PER_DAY;
+ else
+ hr->password_change_min_usec = UINT64_MAX;
+
+ if (spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->password_change_max_usec = spwd->sp_max * USEC_PER_DAY;
+ else
+ hr->password_change_max_usec = UINT64_MAX;
+
+ if (spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->password_change_warn_usec = spwd->sp_warn * USEC_PER_DAY;
+ else
+ hr->password_change_warn_usec = UINT64_MAX;
+
+ if (spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY)
+ hr->password_change_inactive_usec = spwd->sp_inact * USEC_PER_DAY;
+ else
+ hr->password_change_inactive_usec = UINT64_MAX;
+ } else {
+ hr->hashed_password = strv_free_erase(hr->hashed_password);
+ hr->locked = -1;
+ hr->not_after_usec = UINT64_MAX;
+ hr->password_change_now = -1,
+ hr->last_password_change_usec = UINT64_MAX;
+ hr->password_change_min_usec = UINT64_MAX;
+ hr->password_change_max_usec = UINT64_MAX;
+ hr->password_change_warn_usec = UINT64_MAX;
+ hr->password_change_inactive_usec = UINT64_MAX;
+ }
+
+ hr->json = json_variant_unref(hr->json);
+ r = json_build(&hr->json, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(hr->user_name)),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(hr->uid)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(hr->gid)),
+ JSON_BUILD_PAIR_CONDITION(hr->real_name, "realName", JSON_BUILD_STRING(hr->real_name)),
+ JSON_BUILD_PAIR_CONDITION(hr->home_directory, "homeDirectory", JSON_BUILD_STRING(hr->home_directory)),
+ JSON_BUILD_PAIR_CONDITION(hr->shell, "shell", JSON_BUILD_STRING(hr->shell)),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(hr->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(hr->hashed_password)))),
+ JSON_BUILD_PAIR_CONDITION(hr->locked >= 0, "locked", JSON_BUILD_BOOLEAN(hr->locked)),
+ JSON_BUILD_PAIR_CONDITION(hr->not_after_usec != UINT64_MAX, "notAfterUSec", JSON_BUILD_UNSIGNED(hr->not_after_usec)),
+ JSON_BUILD_PAIR_CONDITION(hr->password_change_now >= 0, "passwordChangeNow", JSON_BUILD_BOOLEAN(hr->password_change_now)),
+ JSON_BUILD_PAIR_CONDITION(hr->last_password_change_usec != UINT64_MAX, "lastPasswordChangeUSec", JSON_BUILD_UNSIGNED(hr->last_password_change_usec)),
+ JSON_BUILD_PAIR_CONDITION(hr->password_change_min_usec != UINT64_MAX, "passwordChangeMinUSec", JSON_BUILD_UNSIGNED(hr->password_change_min_usec)),
+ JSON_BUILD_PAIR_CONDITION(hr->password_change_max_usec != UINT64_MAX, "passwordChangeMaxUSec", JSON_BUILD_UNSIGNED(hr->password_change_max_usec)),
+ JSON_BUILD_PAIR_CONDITION(hr->password_change_warn_usec != UINT64_MAX, "passwordChangeWarnUSec", JSON_BUILD_UNSIGNED(hr->password_change_warn_usec)),
+ JSON_BUILD_PAIR_CONDITION(hr->password_change_inactive_usec != UINT64_MAX, "passwordChangeInactiveUSec", JSON_BUILD_UNSIGNED(hr->password_change_inactive_usec))));
+
+ if (r < 0)
+ return r;
+
+ hr->mask = USER_RECORD_REGULAR |
+ (!strv_isempty(hr->hashed_password) ? USER_RECORD_PRIVILEGED : 0);
+
+ *ret = TAKE_PTR(hr);
+ return 0;
+}
+
+int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer) {
+ size_t buflen = 4096;
+ int r;
+
+ assert(pwd);
+ assert(ret_spwd);
+ assert(ret_buffer);
+
+ for (;;) {
+ _cleanup_free_ char *buf = NULL;
+ struct spwd spwd, *result;
+
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getspnam_r(pwd->pw_name, &spwd, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+
+ *ret_spwd = *result;
+ *ret_buffer = TAKE_PTR(buf);
+ return 0;
+ }
+ if (r < 0)
+ return -EIO; /* Weird, this should not return negative! */
+ if (r != ERANGE)
+ return -r;
+
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+}
+
+int nss_user_record_by_name(const char *name, UserRecord **ret) {
+ _cleanup_free_ char *buf = NULL, *sbuf = NULL;
+ struct passwd pwd, *result;
+ bool incomplete = false;
+ size_t buflen = 4096;
+ struct spwd spwd;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ for (;;) {
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getpwnam_r(name, &pwd, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+
+ break;
+ }
+
+ if (r < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwnam_r() returned a negative value");
+ if (r != ERANGE)
+ return -r;
+
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+
+ r = nss_spwd_for_passwd(result, &spwd, &sbuf);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do shadow lookup for user %s, ignoring: %m", name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret);
+ if (r < 0)
+ return r;
+
+ (*ret)->incomplete = incomplete;
+ return 0;
+}
+
+int nss_user_record_by_uid(uid_t uid, UserRecord **ret) {
+ _cleanup_free_ char *buf = NULL, *sbuf = NULL;
+ struct passwd pwd, *result;
+ bool incomplete = false;
+ size_t buflen = 4096;
+ struct spwd spwd;
+ int r;
+
+ assert(ret);
+
+ for (;;) {
+ buf = malloc(buflen);
+ if (!buf)
+ return -ENOMEM;
+
+ r = getpwuid_r(uid, &pwd, buf, buflen, &result);
+ if (r == 0) {
+ if (!result)
+ return -ESRCH;
+
+ break;
+ }
+ if (r < 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwuid_r() returned a negative value");
+ if (r != ERANGE)
+ return -r;
+
+ if (buflen > SIZE_MAX / 2)
+ return -ERANGE;
+
+ buflen *= 2;
+ buf = mfree(buf);
+ }
+
+ r = nss_spwd_for_passwd(result, &spwd, &sbuf);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do shadow lookup for UID " UID_FMT ", ignoring: %m", uid);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret);
+ if (r < 0)
+ return r;
+
+ (*ret)->incomplete = incomplete;
+ return 0;
+}
diff --git a/src/shared/user-record-nss.h b/src/shared/user-record-nss.h
new file mode 100644
index 0000000000000..d5fb23ad2a294
--- /dev/null
+++ b/src/shared/user-record-nss.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#include "user-record.h"
+
+/* Synthesizes a UserRecord object from NSS data */
+
+int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, UserRecord **ret);
+int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer);
+
+int nss_user_record_by_name(const char *name, UserRecord **ret);
+int nss_user_record_by_uid(uid_t uid, UserRecord **ret);
diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c
new file mode 100644
index 0000000000000..80f89e0acc3e0
--- /dev/null
+++ b/src/shared/user-record-show.c
@@ -0,0 +1,414 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "format-util.h"
+#include "fs-util.h"
+#include "group-record.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "user-record-show.h"
+#include "user-util.h"
+#include "userdb.h"
+
+const char *user_record_state_color(const char *state) {
+ if (STR_IN_SET(state, "unfixated", "absent"))
+ return ansi_grey();
+ else if (streq(state, "active"))
+ return ansi_highlight_green();
+ else if (streq(state, "locked"))
+ return ansi_highlight_yellow();
+
+ return NULL;
+}
+
+void user_record_show(UserRecord *hr, bool show_full_group_info) {
+ const char *hd, *ip, *shell;
+ UserStorage storage;
+ usec_t t;
+ int r, b;
+
+ printf(" User name: %s\n",
+ user_record_user_name_and_realm(hr));
+
+ if (hr->state) {
+ const char *color;
+
+ color = user_record_state_color(hr->state);
+
+ printf(" State: %s%s%s\n",
+ strempty(color), hr->state, color ? ansi_normal() : "");
+ }
+
+ printf(" Disposition: %s\n", user_disposition_to_string(user_record_disposition(hr)));
+
+ if (hr->last_change_usec != USEC_INFINITY) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), hr->last_change_usec));
+ }
+
+ if (hr->last_password_change_usec != USEC_INFINITY &&
+ hr->last_password_change_usec != hr->last_change_usec) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Last Passw.: %s\n", format_timestamp(buf, sizeof(buf), hr->last_password_change_usec));
+ }
+
+ r = user_record_test_blocked(hr);
+ switch (r) {
+
+ case -ESTALE:
+ printf(" Login OK: %sno%s (last change time is in the future)\n", ansi_highlight_red(), ansi_normal());
+ break;
+
+ case -ENOLCK:
+ printf(" Login OK: %sno%s (record is locked)\n", ansi_highlight_red(), ansi_normal());
+ break;
+
+ case -EL2HLT:
+ printf(" Login OK: %sno%s (record not valid yet))\n", ansi_highlight_red(), ansi_normal());
+ break;
+
+ case -EL3HLT:
+ printf(" Login OK: %sno%s (record not valid anymore))\n", ansi_highlight_red(), ansi_normal());
+ break;
+
+ default: {
+ usec_t y;
+
+ if (r < 0) {
+ errno = -r;
+ printf(" Login OK: %sno%s (%m)\n", ansi_highlight_red(), ansi_normal());
+ break;
+ }
+
+ if (is_nologin_shell(user_record_shell(hr))) {
+ printf(" Login OK: %sno%s (nologin shell)\n", ansi_highlight_red(), ansi_normal());
+ break;
+ }
+
+ y = user_record_ratelimit_next_try(hr);
+ if (y != USEC_INFINITY && y > now(CLOCK_REALTIME)) {
+ printf(" Login OK: %sno%s (ratelimit)\n", ansi_highlight_red(), ansi_normal());
+ break;
+ }
+
+ printf(" Login OK: %syes%s\n", ansi_highlight_green(), ansi_normal());
+ break;
+ }}
+
+ if (uid_is_valid(hr->uid))
+ printf(" UID: " UID_FMT "\n", hr->uid);
+ if (gid_is_valid(hr->gid)) {
+ if (show_full_group_info) {
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+
+ r = groupdb_by_gid(hr->gid, 0, &gr);
+ if (r < 0) {
+ errno = -r;
+ printf(" GID: " GID_FMT " (unresolvable: %m)\n", hr->gid);
+ } else
+ printf(" GID: " GID_FMT " (%s)\n", hr->gid, gr->group_name);
+ } else
+ printf(" GID: " GID_FMT "\n", hr->gid);
+ } else if (uid_is_valid(hr->uid)) /* Show UID as GID if not separately configured */
+ printf(" GID: " GID_FMT "\n", (gid_t) hr->uid);
+
+ if (show_full_group_info) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ r = membershipdb_by_user(hr->user_name, 0, &iterator);
+ if (r < 0) {
+ errno = -r;
+ printf(" Aux. Groups: (can't acquire: %m)\n");
+ } else {
+ const char *prefix = " Aux. Groups:";
+
+ for (;;) {
+ _cleanup_free_ char *group = NULL;
+
+ r = membershipdb_iterator_get(iterator, NULL, &group);
+ if (r == -ESRCH)
+ break;
+ if (r < 0) {
+ errno = -r;
+ printf("%s (can't iterate: %m)\n", prefix);
+ break;
+ }
+
+ printf("%s %s\n", prefix, group);
+ prefix = " ";
+ }
+ }
+ }
+
+ if (hr->real_name && !streq(hr->real_name, hr->user_name))
+ printf(" Real Name: %s\n", hr->real_name);
+
+ hd = user_record_home_directory(hr);
+ if (hd)
+ printf(" Directory: %s\n", hd);
+
+ 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),
+ storage == USER_LUKS ? " (strong encryption)" :
+ storage == USER_FSCRYPT ? " (weak encryption)" :
+ IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME) ? " (no encryption)" : "");
+
+ ip = user_record_image_path(hr);
+ if (ip && !streq_ptr(ip, hd))
+ printf(" Image Path: %s\n", ip);
+
+ b = user_record_removable(hr);
+ if (b >= 0)
+ printf(" Removable: %s\n", yes_no(b));
+
+ shell = user_record_shell(hr);
+ if (shell)
+ printf(" Shell: %s\n", shell);
+
+ if (hr->email_address)
+ printf(" Email: %s\n", hr->email_address);
+ if (hr->location)
+ printf(" Location: %s\n", hr->location);
+ if (hr->password_hint)
+ printf(" Passw. Hint: %s\n", hr->password_hint);
+ if (hr->icon_name)
+ printf(" Icon Name: %s\n", hr->icon_name);
+
+ if (hr->time_zone)
+ printf(" Time Zone: %s\n", hr->time_zone);
+
+ if (hr->preferred_language)
+ printf(" Language: %s\n", hr->preferred_language);
+
+ if (!strv_isempty(hr->environment)) {
+ char **i;
+
+ STRV_FOREACH(i, hr->environment) {
+ printf(i == hr->environment ?
+ " Environment: %s\n" :
+ " %s\n", *i);
+ }
+ }
+
+ if (hr->locked >= 0)
+ printf(" Locked: %s\n", yes_no(hr->locked));
+
+ if (hr->not_before_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Not Before: %s\n", format_timestamp(buf, sizeof(buf), hr->not_before_usec));
+ }
+
+ if (hr->not_after_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Not After: %s\n", format_timestamp(buf, sizeof(buf), hr->not_after_usec));
+ }
+
+ if (hr->umask != MODE_INVALID)
+ printf(" UMask: 0%03o\n", hr->umask);
+
+ if (nice_is_valid(hr->nice_level))
+ printf(" Nice: %i\n", hr->nice_level);
+
+ for (int j = 0; j < _RLIMIT_MAX; j++) {
+ if (hr->rlimits[j])
+ printf(" Limit: RLIMIT_%s=%" PRIu64 ":%" PRIu64 "\n",
+ rlimit_to_string(j), (uint64_t) hr->rlimits[j]->rlim_cur, (uint64_t) hr->rlimits[j]->rlim_max);
+ }
+
+ if (hr->tasks_max != UINT64_MAX)
+ printf(" Tasks Max: %" PRIu64 "\n", hr->tasks_max);
+
+ if (hr->memory_high != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Memory High: %s\n", format_bytes(buf, sizeof(buf), hr->memory_high));
+ }
+
+ if (hr->memory_max != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Memory Max: %s\n", format_bytes(buf, sizeof(buf), hr->memory_max));
+ }
+
+ if (hr->cpu_weight != UINT64_MAX)
+ printf(" CPU Weight: %" PRIu64 "\n", hr->cpu_weight);
+
+ if (hr->io_weight != UINT64_MAX)
+ printf(" IO Weight: %" PRIu64 "\n", hr->io_weight);
+
+ if (hr->access_mode != MODE_INVALID)
+ printf(" Access Mode: 0%03oo\n", user_record_access_mode(hr));
+
+ if (storage == USER_LUKS) {
+ printf("LUKS Discard: %s\n", yes_no(user_record_luks_discard(hr)));
+
+ if (!sd_id128_is_null(hr->luks_uuid))
+ printf(" LUKS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->luks_uuid));
+ if (!sd_id128_is_null(hr->partition_uuid))
+ printf(" Part UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->partition_uuid));
+ if (!sd_id128_is_null(hr->file_system_uuid))
+ printf(" FS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->file_system_uuid));
+
+ if (hr->file_system_type)
+ printf(" File System: %s\n", user_record_file_system_type(hr));
+
+ if (hr->luks_cipher)
+ printf(" LUKS Cipher: %s\n", hr->luks_cipher);
+ if (hr->luks_cipher_mode)
+ printf(" Cipher Mode: %s\n", hr->luks_cipher_mode);
+ if (hr->luks_volume_key_size != UINT64_MAX)
+ printf(" Volume Key: %zubit\n", hr->luks_volume_key_size * 8);
+
+ if (hr->luks_pbkdf_type)
+ printf(" PBKDF Type: %s\n", hr->luks_pbkdf_type);
+ if (hr->luks_pbkdf_hash_algorithm)
+ printf(" PBKDF Hash: %s\n", hr->luks_pbkdf_hash_algorithm);
+ if (hr->luks_pbkdf_time_cost_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ printf(" PBKDF Time: %s\n", format_timespan(buf, sizeof(buf), hr->luks_pbkdf_time_cost_usec, 0));
+ }
+ if (hr->luks_pbkdf_memory_cost != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" PBKDF Bytes: %s\n", format_bytes(buf, sizeof(buf), hr->luks_pbkdf_memory_cost));
+ }
+ if (hr->luks_pbkdf_parallel_threads != UINT64_MAX)
+ printf("PBKDF Thread: %" PRIu64 "\n", hr->luks_pbkdf_parallel_threads);
+
+ } else if (storage == USER_CIFS) {
+
+ if (hr->cifs_service)
+ printf("CIFS Service: %s\n", hr->cifs_service);
+ }
+
+ if (hr->cifs_user_name)
+ printf(" CIFS User: %s\n", user_record_cifs_user_name(hr));
+ if (hr->cifs_domain)
+ printf(" CIFS Domain: %s\n", hr->cifs_domain);
+
+ if (storage != USER_CLASSIC)
+ printf(" Mount Flags: %s %s %s\n",
+ hr->nosuid ? "nosuid" : "suid",
+ hr->nodev ? "nodev" : "dev",
+ hr->noexec ? "noexec" : "exec");
+
+ if (hr->skeleton_directory)
+ printf(" Skel. Dir.: %s\n", user_record_skeleton_directory(hr));
+
+ if (hr->disk_size != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Disk Size: %s\n", format_bytes(buf, sizeof(buf), hr->disk_size));
+ }
+
+ if (hr->disk_usage != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Disk Usage: %s\n", format_bytes(buf, sizeof(buf), hr->disk_usage));
+ }
+
+ if (hr->disk_free != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Disk Free: %s\n", format_bytes(buf, sizeof(buf), hr->disk_free));
+ }
+
+ if (hr->disk_floor != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf(" Disk Floor: %s\n", format_bytes(buf, sizeof(buf), hr->disk_floor));
+ }
+
+ if (hr->disk_ceiling != UINT64_MAX) {
+ char buf[FORMAT_BYTES_MAX];
+ printf("Disk Ceiling: %s\n", format_bytes(buf, sizeof(buf), hr->disk_ceiling));
+ }
+
+ if (hr->good_authentication_counter != UINT64_MAX)
+ printf(" Good Auth.: %" PRIu64 "\n", hr->good_authentication_counter);
+
+ if (hr->last_good_authentication_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Last Good: %s\n", format_timestamp(buf, sizeof(buf), hr->last_good_authentication_usec));
+ }
+
+ if (hr->bad_authentication_counter != UINT64_MAX)
+ printf(" Bad Auth.: %" PRIu64 "\n", hr->bad_authentication_counter);
+
+ if (hr->last_bad_authentication_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ printf(" Last Bad: %s\n", format_timestamp(buf, sizeof(buf), hr->last_bad_authentication_usec));
+ }
+
+ t = user_record_ratelimit_next_try(hr);
+ if (t != USEC_INFINITY) {
+ usec_t n = now(CLOCK_REALTIME);
+
+ if (t <= n)
+ printf(" Next Try: anytime\n");
+ else {
+ char buf[FORMAT_TIMESPAN_MAX];
+ printf(" Next Try: %sin %s%s\n",
+ ansi_highlight_red(),
+ format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC),
+ ansi_normal());
+ }
+ }
+
+ if (storage != USER_CLASSIC) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ printf(" Auth. Limit: %" PRIu64 " attempts per %s\n", user_record_ratelimit_burst(hr),
+ format_timespan(buf, sizeof(buf), user_record_ratelimit_interval_usec(hr), 0));
+ }
+
+ if (hr->enforce_password_policy >= 0)
+ printf(" Passwd Pol.: %s\n", yes_no(hr->enforce_password_policy));
+
+ if (hr->password_change_min_usec != UINT64_MAX ||
+ hr->password_change_max_usec != UINT64_MAX ||
+ hr->password_change_warn_usec != UINT64_MAX ||
+ hr->password_change_inactive_usec != UINT64_MAX) {
+
+ char buf[FORMAT_TIMESPAN_MAX];
+ printf(" Passwd Chg.:");
+
+ if (hr->password_change_min_usec != UINT64_MAX) {
+ printf(" min %s", format_timespan(buf, sizeof(buf), hr->password_change_min_usec, 0));
+
+ if (hr->password_change_max_usec != UINT64_MAX)
+ printf(" …");
+ }
+
+ if (hr->password_change_max_usec != UINT64_MAX)
+ printf(" max %s", format_timespan(buf, sizeof(buf), hr->password_change_max_usec, 0));
+
+ if (hr->password_change_warn_usec != UINT64_MAX)
+ printf("/warn %s", format_timespan(buf, sizeof(buf), hr->password_change_warn_usec, 0));
+
+ if (hr->password_change_inactive_usec != UINT64_MAX)
+ printf("/inactive %s", format_timespan(buf, sizeof(buf), hr->password_change_inactive_usec, 0));
+
+ printf("\n");
+ }
+
+ if (hr->password_change_now >= 0)
+ printf("Pas. Ch. Now: %s\n", yes_no(hr->password_change_now));
+
+ if (!strv_isempty(hr->ssh_authorized_keys))
+ printf("SSH Pub. Key: %zu\n", strv_length(hr->ssh_authorized_keys));
+
+ if (!strv_isempty(hr->hashed_password))
+ printf(" Passwords: %zu\n", strv_length(hr->hashed_password));
+
+ if (hr->signed_locally >= 0)
+ printf(" Local Sig.: %s\n", yes_no(hr->signed_locally));
+
+ if (hr->stop_delay_usec != UINT64_MAX) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ printf(" Stop Delay: %s\n", format_timespan(buf, sizeof(buf), hr->stop_delay_usec, 0));
+ }
+
+ if (hr->auto_login >= 0)
+ printf("Autom. Login: %s\n", yes_no(hr->auto_login));
+
+ if (hr->kill_processes >= 0)
+ printf(" Kill Proc.: %s\n", yes_no(hr->kill_processes));
+
+ if (hr->service)
+ printf(" Service: %s\n", hr->service);
+}
diff --git a/src/shared/user-record-show.h b/src/shared/user-record-show.h
new file mode 100644
index 0000000000000..bd22be2ae04ad
--- /dev/null
+++ b/src/shared/user-record-show.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "user-record.h"
+
+const char *user_record_state_color(const char *state);
+
+void user_record_show(UserRecord *hr, bool show_full_group_info);
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
new file mode 100644
index 0000000000000..f5f9a9375409d
--- /dev/null
+++ b/src/shared/user-record.c
@@ -0,0 +1,1680 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "cgroup-util.h"
+#include "dns-domain.h"
+#include "env-util.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "hostname-util.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "rlimit-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "user-record.h"
+#include "user-util.h"
+
+#define DEFAULT_RATELIMIT_BURST 10
+#define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE)
+
+UserRecord* user_record_new(void) {
+ UserRecord *h;
+
+ h = new(UserRecord, 1);
+ if (!h)
+ return NULL;
+
+ *h = (UserRecord) {
+ .n_ref = 1,
+ .disposition = _USER_DISPOSITION_INVALID,
+ .last_change_usec = UINT64_MAX,
+ .last_password_change_usec = UINT64_MAX,
+ .umask = MODE_INVALID,
+ .nice_level = INT_MAX,
+ .not_before_usec = UINT64_MAX,
+ .not_after_usec = UINT64_MAX,
+ .locked = -1,
+ .storage = _USER_STORAGE_INVALID,
+ .access_mode = MODE_INVALID,
+ .disk_size = UINT64_MAX,
+ .disk_size_relative = UINT64_MAX,
+ .tasks_max = UINT64_MAX,
+ .memory_high = UINT64_MAX,
+ .memory_max = UINT64_MAX,
+ .cpu_weight = UINT64_MAX,
+ .io_weight = UINT64_MAX,
+ .uid = UID_INVALID,
+ .gid = GID_INVALID,
+ .nodev = true,
+ .nosuid = true,
+ .luks_discard = -1,
+ .luks_volume_key_size = UINT64_MAX,
+ .luks_pbkdf_time_cost_usec = UINT64_MAX,
+ .luks_pbkdf_memory_cost = UINT64_MAX,
+ .luks_pbkdf_parallel_threads = UINT64_MAX,
+ .disk_usage = UINT64_MAX,
+ .disk_free = UINT64_MAX,
+ .disk_ceiling = UINT64_MAX,
+ .disk_floor = UINT64_MAX,
+ .signed_locally = -1,
+ .good_authentication_counter = UINT64_MAX,
+ .bad_authentication_counter = UINT64_MAX,
+ .last_good_authentication_usec = UINT64_MAX,
+ .last_bad_authentication_usec = UINT64_MAX,
+ .ratelimit_begin_usec = UINT64_MAX,
+ .ratelimit_count = UINT64_MAX,
+ .ratelimit_interval_usec = UINT64_MAX,
+ .ratelimit_burst = UINT64_MAX,
+ .removable = -1,
+ .enforce_password_policy = -1,
+ .auto_login = -1,
+ .stop_delay_usec = UINT64_MAX,
+ .kill_processes = -1,
+ .password_change_min_usec = UINT64_MAX,
+ .password_change_max_usec = UINT64_MAX,
+ .password_change_warn_usec = UINT64_MAX,
+ .password_change_inactive_usec = UINT64_MAX,
+ .password_change_now = -1,
+ };
+
+ return h;
+}
+
+static UserRecord* user_record_free(UserRecord *h) {
+ if (!h)
+ return NULL;
+
+ free(h->user_name);
+ free(h->realm);
+ free(h->user_name_and_realm_auto);
+ free(h->real_name);
+ free(h->email_address);
+ erase_and_free(h->password_hint);
+ free(h->location);
+ free(h->icon_name);
+
+ free(h->shell);
+
+ strv_free(h->environment);
+ free(h->time_zone);
+ free(h->preferred_language);
+ rlimit_free_all(h->rlimits);
+
+ free(h->skeleton_directory);
+
+ strv_free_erase(h->hashed_password);
+ strv_free_erase(h->ssh_authorized_keys);
+ strv_free_erase(h->password);
+
+ free(h->fscrypt_salt);
+
+ free(h->cifs_service);
+ free(h->cifs_user_name);
+ free(h->cifs_domain);
+
+ free(h->image_path);
+ free(h->image_path_auto);
+ free(h->home_directory);
+ free(h->home_directory_auto);
+
+ strv_free(h->member_of);
+
+ free(h->file_system_type);
+ free(h->luks_cipher);
+ free(h->luks_cipher_mode);
+ free(h->luks_pbkdf_hash_algorithm);
+ free(h->luks_pbkdf_type);
+
+ free(h->state);
+ free(h->service);
+
+ json_variant_unref(h->json);
+
+ return mfree(h);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(UserRecord, user_record, user_record_free);
+
+int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ r = dns_name_is_valid(n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to check if JSON field '%s' is a valid DNS domain.", strna(name));
+ if (r == 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid DNS domain.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_gecos(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ _cleanup_free_ char *k = NULL;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!valid_gecos(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid GECOS compatible real name.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_nice(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ int *nl = userdata;
+ intmax_t m;
+
+ if (json_variant_is_null(variant)) {
+ *nl = INT_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_integer(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ m = json_variant_integer(variant);
+ if (m < PRIO_MIN || m >= PRIO_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not a valid nice level.", strna(name));
+
+ *nl = m;
+ return 0;
+}
+
+static int json_dispatch_rlimit_value(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ rlim_t *ret = userdata;
+
+ if (json_variant_is_null(variant))
+ *ret = RLIM_INFINITY;
+ else if (json_variant_is_unsigned(variant)) {
+ uintmax_t w;
+
+ w = json_variant_unsigned(variant);
+ if (w == RLIM_INFINITY || (uintmax_t) w != json_variant_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "Resource limit value '%s' is out of range.", name);
+
+ *ret = (rlim_t) w;
+ } else
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit value '%s' is not an unsigned integer.", name);
+
+ return 0;
+}
+
+static int json_dispatch_rlimits(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ struct rlimit** limits = userdata;
+ JsonVariant *value;
+ const char *key;
+ int r;
+
+ assert_se(limits);
+
+ if (json_variant_is_null(variant)) {
+ rlimit_free_all(limits);
+ 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) {
+ JsonVariant *jcur, *jmax;
+ struct rlimit rl;
+ const char *p;
+ int l;
+
+ p = startswith(key, "RLIMIT_");
+ if (!p)
+ l = -1;
+ else
+ l = rlimit_from_string(p);
+ if (l < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' not known.", key);
+
+ if (!json_variant_is_object(value))
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' has invalid value.", key);
+
+ if (json_variant_elements(value) != 4)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' value is does not have two fields as expected.", key);
+
+ jcur = json_variant_by_key(value, "cur");
+ if (!jcur)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'cur' field.", key);
+ r = json_dispatch_rlimit_value("cur", jcur, flags, &rl.rlim_cur);
+ if (r < 0)
+ return r;
+
+ jmax = json_variant_by_key(value, "max");
+ if (!jmax)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'max' field.", key);
+ r = json_dispatch_rlimit_value("max", jmax, flags, &rl.rlim_max);
+ if (r < 0)
+ return r;
+
+ if (limits[l])
+ *(limits[l]) = rl;
+ else {
+ limits[l] = newdup(struct rlimit, &rl, 1);
+ if (!limits[l])
+ return log_oom();
+ }
+ }
+
+ return 0;
+}
+
+static int json_dispatch_filename_or_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ _cleanup_free_ char *k = NULL;
+ const char *n;
+ int r;
+
+ assert(s);
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!filename_is_valid(n) && !path_is_normalized(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid file name or normalized path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ _cleanup_free_ char *k = NULL;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!path_is_normalized(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a normalized file system path.", strna(name));
+ if (!path_is_absolute(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an absolute file system path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_home_directory(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ _cleanup_free_ char *k = NULL;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!valid_home(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid home directory path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_image_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ _cleanup_free_ char *k = NULL;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (empty_or_root(n) || !path_is_valid(n) || !path_is_absolute(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid image path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_umask(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ mode_t *m = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *m = (mode_t) -1;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k > 0777)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…0777.", strna(name));
+
+ *m = (mode_t) k;
+ return 0;
+}
+
+static int json_dispatch_access_mode(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ mode_t *m = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *m = (mode_t) -1;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k > 07777)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…07777.", strna(name));
+
+ *m = (mode_t) k;
+ return 0;
+}
+
+static int json_dispatch_environment(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **n = NULL;
+ char ***l = userdata;
+ size_t i;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *l = strv_free(*l);
+ return 0;
+ }
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
+
+ for (i = 0; i < json_variant_elements(variant); i++) {
+ _cleanup_free_ char *c = NULL;
+ JsonVariant *e;
+ const char *a;
+
+ e = json_variant_by_index(variant, i);
+ if (!json_variant_is_string(e))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ assert_se(a = json_variant_string(e));
+
+ if (!env_assignment_is_valid(a))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of environment variables.", strna(name));
+
+ c = strdup(a);
+ if (!c)
+ return json_log_oom(variant, flags);
+
+ r = strv_env_replace(&n, c);
+ if (r < 0)
+ return json_log_oom(variant, flags);
+
+ c = NULL;
+ }
+
+ strv_free_and_replace(*l, n);
+ return 0;
+}
+
+int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserDisposition *disposition = userdata, k;
+
+ if (json_variant_is_null(variant)) {
+ *disposition = _USER_DISPOSITION_INVALID;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ k = user_disposition_from_string(json_variant_string(variant));
+ if (k < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Disposition type '%s' not known.", json_variant_string(variant));
+
+ *disposition = k;
+ return 0;
+}
+
+static int json_dispatch_storage(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserStorage *storage = userdata, k;
+
+ if (json_variant_is_null(variant)) {
+ *storage = _USER_STORAGE_INVALID;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ k = user_storage_from_string(json_variant_string(variant));
+ if (k < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Storage type '%s' not known.", json_variant_string(variant));
+
+ *storage = k;
+ return 0;
+}
+
+static int json_dispatch_disk_size(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *size = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *size = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k < USER_DISK_SIZE_MIN || k > USER_DISK_SIZE_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX);
+
+ *size = k;
+ return 0;
+}
+
+static int json_dispatch_tasks_or_memory_max(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *limit = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *limit = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k <= 0 || k >= UINT64_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) 1, UINT64_MAX-1);
+
+ *limit = k;
+ return 0;
+}
+
+static int json_dispatch_weight(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *weight = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *weight = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k <= CGROUP_WEIGHT_MIN || k >= CGROUP_WEIGHT_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) CGROUP_WEIGHT_MIN, (uint64_t) CGROUP_WEIGHT_MAX);
+
+ *weight = k;
+ return 0;
+}
+
+int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ char ***list = userdata;
+ JsonVariant *e;
+ int r;
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+
+ if (!json_variant_is_string(e))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string.");
+
+ if (!valid_user_group_name(json_variant_string(e)))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a valid user/group name: %s", json_variant_string(e));
+
+ r = strv_extend(&l, json_variant_string(e));
+ if (r < 0)
+ return json_log(e, flags, r, "Failed to append array element: %m");
+ }
+
+ r = strv_extend_strv(list, l, true);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to merge user/group arrays: %m");
+
+ return 0;
+}
+
+static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch secret_dispatch_table[] = {
+ { "password", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, password), 0 },
+ {},
+ };
+
+ return json_dispatch(variant, secret_dispatch_table, NULL, flags, userdata);
+}
+
+static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch privileged_dispatch_table[] = {
+ { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 },
+ { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE },
+ { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 },
+ {},
+ };
+
+ return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata);
+}
+
+static int dispatch_fscrypt_salt(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserRecord *h = userdata;
+ size_t l;
+ void *b;
+ int r;
+
+ assert_se(h);
+
+ if (json_variant_is_null(variant)) {
+ h->fscrypt_salt = mfree(h->fscrypt_salt);
+ h->fscrypt_salt_size = 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to decode fscrypt salt: %m");
+
+ free(h->fscrypt_salt);
+ h->fscrypt_salt = b;
+ h->fscrypt_salt_size = l;
+
+ return 0;
+}
+
+static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch binding_dispatch_table[] = {
+ { "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 },
+ { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 },
+ {},
+ };
+
+ char smid[SD_ID128_STRING_MAX];
+ JsonVariant *m;
+ sd_id128_t mid;
+ int r;
+
+ if (!json_variant_is_object(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to determine machine ID: %m");
+
+ m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
+ if (!m)
+ return 0;
+
+ return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata);
+}
+
+int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) {
+ sd_id128_t mid;
+ int r;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(ids, flags, r, "Failed to acquire machine ID: %m");
+
+ if (json_variant_is_string(ids)) {
+ sd_id128_t k;
+
+ r = sd_id128_from_string(json_variant_string(ids), &k);
+ if (r < 0) {
+ json_log(ids, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(ids));
+ return 0;
+ }
+
+ return sd_id128_equal(mid, k);
+ }
+
+ if (json_variant_is_array(ids)) {
+ JsonVariant *e;
+
+ JSON_VARIANT_ARRAY_FOREACH(e, ids) {
+ sd_id128_t k;
+
+ if (!json_variant_is_string(e)) {
+ json_log(e, flags, 0, "Machine ID is not a string, ignoring: %m");
+ continue;
+ }
+
+ r = sd_id128_from_string(json_variant_string(e), &k);
+ if (r < 0) {
+ json_log(e, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(e));
+ continue;
+ }
+
+ if (sd_id128_equal(mid, k))
+ return true;
+ }
+
+ return false;
+ }
+
+ json_log(ids, flags, 0, "Machine ID is not a string or array of strings, ignoring: %m");
+ return false;
+}
+
+int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags) {
+ _cleanup_free_ char *hn = NULL;
+ int r;
+
+ r = gethostname_strict(&hn);
+ if (r == -ENXIO) {
+ json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m");
+ return false;
+ }
+ if (r < 0)
+ return json_log(hns, flags, r, "Failed to acquire hostname: %m");
+
+ if (json_variant_is_string(hns))
+ return streq(json_variant_string(hns), hn);
+
+ if (json_variant_is_array(hns)) {
+ JsonVariant *e;
+
+ JSON_VARIANT_ARRAY_FOREACH(e, hns) {
+
+ if (!json_variant_is_string(e)) {
+ json_log(e, flags, 0, "Hostname is not a string, ignoring: %m");
+ continue;
+ }
+
+ if (streq(json_variant_string(hns), hn))
+ return true;
+ }
+
+ return false;
+ }
+
+ json_log(hns, flags, 0, "Hostname is not a string or array of strings, ignoring: %m");
+ return false;
+}
+
+static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch per_machine_dispatch_table[] = {
+ { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
+ { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 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 },
+ { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
+ { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
+ { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
+ { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
+ { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
+ { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 },
+ { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 },
+ { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 },
+ { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 },
+ { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
+ { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 },
+ { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 },
+ { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 },
+ { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 },
+ { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 },
+ { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 },
+ { "mountNoSUID", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 },
+ { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 },
+ { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 },
+ { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE },
+ { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE },
+ { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE },
+ { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "partitionUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
+ { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 },
+ { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE },
+ { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE },
+ { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 },
+ { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 },
+ { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 },
+ { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 },
+ { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 },
+ { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 },
+ { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 },
+ { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 },
+ { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 },
+ { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 },
+ { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 },
+ { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 },
+ { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 },
+ { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
+ {},
+ };
+
+ JsonVariant *e;
+ int r;
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+ bool matching = false;
+ JsonVariant *m;
+
+ if (!json_variant_is_object(e))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name));
+
+ m = json_variant_by_key(e, "matchMachineId");
+ if (m) {
+ r = per_machine_id_match(m, flags);
+ if (r < 0)
+ return r;
+
+ matching = r > 0;
+ }
+
+ if (!matching) {
+ m = json_variant_by_key(e, "matchHostname");
+ if (m) {
+ r = per_machine_hostname_match(m, flags);
+ if (r < 0)
+ return r;
+
+ matching = r > 0;
+ }
+ }
+
+ if (!matching)
+ continue;
+
+ r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch status_dispatch_table[] = {
+ { "diskUsage", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
+ { "diskFree", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
+ { "diskCeiling", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
+ { "diskFloor", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
+ { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
+ { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
+ { "goodAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
+ { "badAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
+ { "lastGoodAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
+ { "lastBadAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
+ { "rateLimitBeginUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
+ { "rateLimitCount", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
+ { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 },
+ {},
+ };
+
+ char smid[SD_ID128_STRING_MAX];
+ JsonVariant *m;
+ sd_id128_t mid;
+ int r;
+
+ if (!json_variant_is_object(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to determine machine ID: %m");
+
+ m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
+ if (!m)
+ return 0;
+
+ return json_dispatch(m, status_dispatch_table, NULL, flags, userdata);
+}
+
+static int user_record_augment(UserRecord *h, JsonDispatchFlags json_flags) {
+ assert(h);
+
+ if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR))
+ return 0;
+
+ assert(h->user_name);
+
+ if (!h->user_name_and_realm_auto && h->realm) {
+ h->user_name_and_realm_auto = strjoin(h->user_name, "@", h->realm);
+ if (!h->user_name_and_realm_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+
+ /* Let's add in the following automatisms only for regular users, they dont make sense for any others */
+ if (user_record_disposition(h) != USER_REGULAR)
+ return 0;
+
+ if (!h->home_directory && !h->home_directory_auto) {
+ h->home_directory_auto = path_join("/home/", h->user_name);
+ if (!h->home_directory_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+
+ if (!h->image_path && !h->image_path_auto) {
+ const char *suffix;
+ UserStorage storage;
+
+ storage = user_record_storage(h);
+ if (storage == USER_LUKS)
+ suffix = ".home";
+ else if (IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT))
+ suffix = ".homedir";
+ else
+ suffix = NULL;
+
+ if (suffix) {
+ h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), suffix);
+ if (!h->image_path_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+ }
+
+ return 0;
+}
+
+int user_group_record_mangle(
+ JsonVariant *v,
+ UserRecordLoadFlags load_flags,
+ JsonVariant **ret_variant,
+ UserRecordMask *ret_mask) {
+
+ static const struct {
+ UserRecordMask mask;
+ const char *name;
+ } mask_field[] = {
+ { USER_RECORD_PRIVILEGED, "privileged" },
+ { USER_RECORD_SECRET, "secret" },
+ { USER_RECORD_BINDING, "binding" },
+ { USER_RECORD_PER_MACHINE, "perMachine" },
+ { USER_RECORD_STATUS, "status" },
+ { USER_RECORD_SIGNATURE, "signature" },
+ };
+
+ JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags);
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ JsonVariant *array[ELEMENTSOF(mask_field) * 2];
+ size_t n_retain = 0, i;
+ UserRecordMask m = 0;
+ int r;
+
+ assert((load_flags & _USER_RECORD_MASK_MAX) == 0); /* detect mistakes when accidentally passing
+ * UserRecordMask bit masks as UserRecordLoadFlags
+ * value */
+
+ assert(v);
+ assert(ret_variant);
+ assert(ret_mask);
+
+ /* Note that this function is shared with the group record parser, hence we try to be generic in our
+ * log message wording here, to cover both cases. */
+
+ if (!json_variant_is_object(v))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is not a JSON object, refusing.");
+
+ if (USER_RECORD_ALLOW_MASK(load_flags) == 0) /* allow nothing? */
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Nothing allowed in record, refusing.");
+
+ if (USER_RECORD_STRIP_MASK(load_flags) == _USER_RECORD_MASK_MAX) /* strip everything? */
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Stripping everything from record, refusing.");
+
+ /* Check if we have the special sections and if they match our flags set */
+ for (i = 0; i < ELEMENTSOF(mask_field); i++) {
+ JsonVariant *e, *k;
+
+ if (FLAGS_SET(USER_RECORD_STRIP_MASK(load_flags), mask_field[i].mask)) {
+ if (!w)
+ w = json_variant_ref(v);
+
+ r = json_variant_filter(&w, STRV_MAKE(mask_field[i].name));
+ if (r < 0)
+ return json_log(w, json_flags, r, "Failed to remove field from variant: %m");
+
+ v = w;
+ continue;
+ }
+
+ e = json_variant_by_key_full(v, mask_field[i].name, &k);
+ if (e) {
+ if (!FLAGS_SET(USER_RECORD_ALLOW_MASK(load_flags), mask_field[i].mask))
+ return json_log(e, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", mask_field[i].name);
+
+ if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) {
+ array[n_retain++] = k;
+ array[n_retain++] = e;
+ }
+
+ m |= mask_field[i].mask;
+ } else {
+ if (FLAGS_SET(USER_RECORD_REQUIRE_MASK(load_flags), mask_field[i].mask))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks '%s' field, which is required.", mask_field[i].name);
+ }
+ }
+
+ if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) {
+ _cleanup_(json_variant_unrefp) JsonVariant *k = NULL;
+
+ /* If we are supposed to strip regular items, then let's instead just allocate a new object
+ * with just the stuff we need. */
+
+ r = json_variant_new_object(&k, array, n_retain);
+ if (r < 0)
+ return json_log(v, json_flags, r, "Failed to allocate new object: %m");
+
+ json_variant_unref(w);
+ w = TAKE_PTR(k);
+
+ v = w;
+ } else {
+ /* And now check if there's anything else in the record */
+ for (i = 0; i < json_variant_elements(v); i += 2) {
+ const char *f;
+ bool special = false;
+ size_t j;
+
+ assert_se(f = json_variant_string(json_variant_by_index(v, i)));
+
+ for (j = 0; j < ELEMENTSOF(mask_field); j++)
+ if (streq(f, mask_field[j].name)) { /* already covered in the loop above */
+ special = true;
+ continue;
+ }
+
+ if (!special) {
+ if ((load_flags & (USER_RECORD_ALLOW_REGULAR|USER_RECORD_REQUIRE_REGULAR)) == 0)
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", f);
+
+ m |= USER_RECORD_REGULAR;
+ break;
+ }
+ }
+ }
+
+ if (FLAGS_SET(load_flags, USER_RECORD_REQUIRE_REGULAR) && !FLAGS_SET(m, USER_RECORD_REGULAR))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks basic identity fields, which are required.");
+
+ if (m == 0)
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is empty.");
+
+ if (w)
+ *ret_variant = TAKE_PTR(w);
+ else
+ *ret_variant = json_variant_ref(v);
+
+ *ret_mask = m;
+ return 0;
+}
+
+int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_flags) {
+
+ static const JsonDispatch user_dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), 0 },
+ { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 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 },
+ { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 },
+ { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 },
+ { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 },
+ { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 },
+ { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 },
+ { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
+ { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
+ { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
+ { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
+ { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
+ { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 },
+ { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 },
+ { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 },
+ { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 },
+ { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
+ { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 },
+ { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 },
+ { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 },
+ { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 },
+ { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 },
+ { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 },
+ { "mountNoSUID", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 },
+ { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 },
+ { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 },
+ { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE },
+ { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE },
+ { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE },
+ { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 },
+ { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "partitionUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
+ { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 },
+ { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE },
+ { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE },
+ { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 },
+ { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 },
+ { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
+ { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 },
+ { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 },
+ { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 },
+ { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 },
+ { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 },
+ { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 },
+ { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 },
+ { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 },
+ { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 },
+ { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 },
+ { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
+
+ { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
+ { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
+
+ /* Ignore the perMachine, binding, status stuff here, and process it later, so that it overrides whatever is set above */
+ { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 },
+ { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 },
+ { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 },
+
+ /* Ignore 'signature', we check it with explicit accessors instead */
+ { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 },
+ {},
+ };
+
+ JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags);
+ JsonVariant *per_machine, *binding, *status;
+ int r;
+
+ assert(h);
+ assert(!h->json);
+
+ /* Note that this call will leave a half-initialized record around on failure! */
+
+ r = user_group_record_mangle(v, load_flags, &h->json, &h->mask);
+ if (r < 0)
+ return r;
+
+ r = json_dispatch(h->json, user_dispatch_table, NULL, json_flags, h);
+ if (r < 0)
+ return r;
+
+ /* During the parsing operation above we ignored the 'perMachine', 'binding' and 'status' fields,
+ * since we want them to override the global options. Let's process them now. */
+
+ per_machine = json_variant_by_key(h->json, "perMachine");
+ if (per_machine) {
+ r = dispatch_per_machine("perMachine", per_machine, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ binding = json_variant_by_key(h->json, "binding");
+ if (binding) {
+ r = dispatch_binding("binding", binding, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ status = json_variant_by_key(h->json, "status");
+ if (status) {
+ r = dispatch_status("binding", status, json_flags, h);
+ if (r < 0)
+ return r;
+ }
+
+ if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->user_name)
+ return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "User name field missing, refusing.");
+
+ r = user_record_augment(h, json_flags);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int user_record_build(UserRecord **ret, ...) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *u = NULL;
+ va_list ap;
+ int r;
+
+ assert(ret);
+
+ va_start(ap, ret);
+ r = json_buildv(&v, ap);
+ va_end(ap);
+
+ if (r < 0)
+ return r;
+
+ u = user_record_new();
+ if (!u)
+ return -ENOMEM;
+
+ r = user_record_load(u, v, USER_RECORD_LOAD_FULL);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(u);
+ return 0;
+}
+
+const char *user_record_user_name_and_realm(UserRecord *h) {
+ assert(h);
+
+ /* Return the pre-initialized joined string if it is defined */
+ if (h->user_name_and_realm_auto)
+ return h->user_name_and_realm_auto;
+
+ /* If it's not defined then we cannot have a realm */
+ assert(!h->realm);
+ return h->user_name;
+}
+
+UserStorage user_record_storage(UserRecord *h) {
+ assert(h);
+
+ if (h->storage >= 0)
+ return h->storage;
+
+ return USER_CLASSIC;
+}
+
+const char *user_record_file_system_type(UserRecord *h) {
+ assert(h);
+
+ return h->file_system_type ?: "ext4";
+}
+
+const char *user_record_skeleton_directory(UserRecord *h) {
+ assert(h);
+
+ return h->skeleton_directory ?: "/etc/skel";
+}
+
+mode_t user_record_access_mode(UserRecord *h) {
+ assert(h);
+
+ return h->access_mode != (mode_t) -1 ? h->access_mode : 0700;
+}
+
+const char* user_record_home_directory(UserRecord *h) {
+ assert(h);
+
+ if (h->home_directory)
+ return h->home_directory;
+ if (h->home_directory_auto)
+ return h->home_directory_auto;
+
+ /* The root user is special, hence be special about it */
+ if (streq_ptr(h->user_name, "root"))
+ return "/root";
+
+ return "/";
+}
+
+const char *user_record_image_path(UserRecord *h) {
+ assert(h);
+
+ if (h->image_path)
+ return h->image_path;
+ if (h->image_path_auto)
+ return h->image_path_auto;
+
+ return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL;
+}
+
+const char *user_record_cifs_user_name(UserRecord *h) {
+ assert(h);
+
+ return h->cifs_user_name ?: h->user_name;
+}
+
+unsigned long user_record_mount_flags(UserRecord *h) {
+ assert(h);
+
+ return (h->nosuid ? MS_NOSUID : 0) |
+ (h->noexec ? MS_NOEXEC : 0) |
+ (h->nodev ? MS_NODEV : 0);
+}
+
+const char *user_record_shell(UserRecord *h) {
+ assert(h);
+
+ if (h->shell)
+ return h->shell;
+
+ if (streq_ptr(h->user_name, "root") ||
+ user_record_disposition(h) == USER_REGULAR)
+ return "/bin/bash";
+
+ return NOLOGIN;
+}
+
+const char *user_record_real_name(UserRecord *h) {
+ assert(h);
+
+ return h->real_name ?: h->user_name;
+}
+
+bool user_record_luks_discard(UserRecord *h) {
+ const char *ip;
+
+ assert(h);
+
+ if (h->luks_discard >= 0)
+ return h->luks_discard;
+
+ ip = user_record_image_path(h);
+ if (!ip)
+ return false;
+
+ /* Use discard by default if we are referring to a real block device, but not when operating on a
+ * loopback device. We want to optimize for SSD and flash storage after all, but we should be careful
+ * when storing stuff on top of regular file systems in loopback files as doing discard then would
+ * mean thin provisioning and we should not do that willy-nilly since it means we'll risk EIO later
+ * on should the disk space to back our file systems not be available. */
+
+ return path_startswith(ip, "/dev/");
+}
+
+const char *user_record_luks_cipher(UserRecord *h) {
+ assert(h);
+
+ return h->luks_cipher ?: "aes";
+}
+
+const char *user_record_luks_cipher_mode(UserRecord *h) {
+ assert(h);
+
+ return h->luks_cipher_mode ?: "xts-plain64";
+}
+
+uint64_t user_record_luks_volume_key_size(UserRecord *h) {
+ assert(h);
+
+ /* We return a value here that can be cast without loss into size_t which is what libcrypsetup expects */
+
+ if (h->luks_volume_key_size == UINT64_MAX)
+ return 256 / 8;
+
+ return MIN(h->luks_volume_key_size, SIZE_MAX);
+}
+
+const char* user_record_luks_pbkdf_type(UserRecord *h) {
+ assert(h);
+
+ return h->luks_pbkdf_type ?: "argon2i";
+}
+
+uint64_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h) {
+ assert(h);
+
+ /* Returns a value with ms granularity, since that's what libcryptsetup expects */
+
+ if (h->luks_pbkdf_time_cost_usec == UINT64_MAX)
+ return 500 * USEC_PER_MSEC; /* We default to 500ms, in contrast to libcryptsetup's 2s, which is just awfully slow on every login */
+
+ return MIN(DIV_ROUND_UP(h->luks_pbkdf_time_cost_usec, USEC_PER_MSEC), UINT32_MAX) * USEC_PER_MSEC;
+}
+
+uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h) {
+ assert(h);
+
+ /* Returns a value with kb granularity, since that's what libcryptsetup expects */
+
+ if (h->luks_pbkdf_memory_cost == UINT64_MAX)
+ return 64*1024*1024; /* We default to 64M, since this should work on smaller systems too */
+
+ return MIN(DIV_ROUND_UP(h->luks_pbkdf_memory_cost, 1024), UINT32_MAX) * 1024;
+}
+
+uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h) {
+ assert(h);
+
+ if (h->luks_pbkdf_memory_cost == UINT64_MAX)
+ return 1; /* We default to 1, since this should work on smaller systems too */
+
+ return MIN(h->luks_pbkdf_parallel_threads, UINT32_MAX);
+}
+
+const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h) {
+ assert(h);
+
+ return h->luks_pbkdf_hash_algorithm ?: "sha512";
+}
+
+gid_t user_record_gid(UserRecord *h) {
+ assert(h);
+
+ if (gid_is_valid(h->gid))
+ return h->gid;
+
+ return (gid_t) h->uid;
+}
+
+UserDisposition user_record_disposition(UserRecord *h) {
+ assert(h);
+
+ if (h->disposition >= 0)
+ return h->disposition;
+
+ /* If not declared, derive from UID */
+
+ if (!uid_is_valid(h->uid))
+ return _USER_DISPOSITION_INVALID;
+
+ if (h->uid == 0 || h->uid == UID_NOBODY)
+ return USER_INTRINSIC;
+
+ if (uid_is_system(h->uid))
+ return USER_SYSTEM;
+
+ if (uid_is_dynamic(h->uid))
+ return USER_DYNAMIC;
+
+ if (uid_is_container(h->uid))
+ return USER_CONTAINER;
+
+ if (h->uid > INT32_MAX)
+ return USER_RESERVED;
+
+ return USER_REGULAR;
+}
+
+int user_record_removable(UserRecord *h) {
+ UserStorage storage;
+ assert(h);
+
+ if (h->removable >= 0)
+ return h->removable;
+
+ /* Refuse to decide for classic records */
+ storage = user_record_storage(h);
+ if (h->storage < 0 || h->storage == USER_CLASSIC)
+ return -1;
+
+ /* For now consider only LUKS home directories with a reference by path as removable */
+ return storage == USER_LUKS && path_startswith(user_record_image_path(h), "/dev/");
+}
+
+uint64_t user_record_ratelimit_interval_usec(UserRecord *h) {
+ assert(h);
+
+ if (h->ratelimit_interval_usec == UINT64_MAX)
+ return DEFAULT_RATELIMIT_INTERVAL_USEC;
+
+ return h->ratelimit_interval_usec;
+}
+
+uint64_t user_record_ratelimit_burst(UserRecord *h) {
+ assert(h);
+
+ if (h->ratelimit_burst == UINT64_MAX)
+ return DEFAULT_RATELIMIT_BURST;
+
+ return h->ratelimit_burst;
+}
+
+uint64_t user_record_ratelimit_next_try(UserRecord *h) {
+ assert(h);
+
+ /* Calculates when the it's possible to login next. Returns:
+ *
+ * UINT64_MAX → Nothing known
+ * 0 → Right away
+ * Any other → Next time in CLOCK_REALTIME in usec (which could be in the past)
+ */
+
+ if (h->ratelimit_begin_usec == UINT64_MAX ||
+ h->ratelimit_count == UINT64_MAX)
+ return UINT64_MAX;
+
+ if (h->ratelimit_count < user_record_ratelimit_burst(h))
+ return 0;
+
+ return usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h));
+}
+
+bool user_record_equal(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ /* We assume that when a record is modified its JSON data is updated at the same time, hence it's
+ * sufficient to compare the JSON data. */
+
+ return json_variant_equal(a->json, b->json);
+}
+
+bool user_record_compatible(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ /* If either lacks a the regular section, we can't really decide, let's hence say they are
+ * incompatible. */
+ if (!(a->mask & b->mask & USER_RECORD_REGULAR))
+ return false;
+
+ return streq_ptr(a->user_name, b->user_name) &&
+ streq_ptr(a->realm, b->realm);
+}
+
+int user_record_compare_last_change(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ if (a->last_change_usec == b->last_change_usec)
+ return 0;
+
+ /* Always consider a record with a timestamp newer than one without */
+ if (a->last_change_usec == UINT64_MAX)
+ return -1;
+ if (b->last_change_usec == UINT64_MAX)
+ return 1;
+
+ return CMP(a->last_change_usec, b->last_change_usec);
+}
+
+int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *c = NULL;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ c = user_record_new();
+ if (!c)
+ return -ENOMEM;
+
+ r = user_record_load(c, h->json, flags);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
+
+int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask) {
+ _cleanup_(user_record_unrefp) UserRecord *x = NULL, *y = NULL;
+ int r;
+
+ assert(a);
+ assert(b);
+
+ /* Compares the two records, but ignores anything not listed in the specified mask */
+
+ if ((a->mask & ~mask) != 0) {
+ r = user_record_clone(a, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &x);
+ if (r < 0)
+ return r;
+
+ a = x;
+ }
+
+ if ((b->mask & ~mask) != 0) {
+ r = user_record_clone(b, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &y);
+ if (r < 0)
+ return r;
+
+ b = y;
+ }
+
+ return user_record_equal(a, b);
+}
+
+int user_record_test_blocked(UserRecord *h) {
+ usec_t n;
+
+ /* Checks whether access to the specified user shall be allowed at the moment. Returns:
+ *
+ * -ESTALE: Record is from the future
+ * -ENOLCK: Record is blocked
+ * -EL2HLT: Record is not valid yet
+ * -EL3HLT: Record is not valid anymore
+ *
+ */
+
+ assert(h);
+
+ n = now(CLOCK_REALTIME);
+ if (h->last_change_usec != UINT64_MAX &&
+ h->last_change_usec > n) /* Don't allow log ins when the record is from the future */
+ return -ESTALE;
+
+ if (h->locked > 0)
+ return -ENOLCK;
+
+ if (h->not_before_usec != UINT64_MAX && n < h->not_before_usec)
+ return -EL2HLT;
+ if (h->not_after_usec != UINT64_MAX && n > h->not_after_usec)
+ return -EL3HLT;
+
+ return 0;
+}
+
+static const char* const user_storage_table[_USER_STORAGE_MAX] = {
+ [USER_CLASSIC] = "classic",
+ [USER_LUKS] = "luks",
+ [USER_DIRECTORY] = "directory",
+ [USER_SUBVOLUME] = "subvolume",
+ [USER_FSCRYPT] = "fscrypt",
+ [USER_CIFS] = "cifs",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(user_storage, UserStorage);
+
+static const char* const user_disposition_table[_USER_DISPOSITION_MAX] = {
+ [USER_INTRINSIC] = "intrinsic",
+ [USER_SYSTEM] = "system",
+ [USER_DYNAMIC] = "dynamic",
+ [USER_REGULAR] = "regular",
+ [USER_CONTAINER] = "container",
+ [USER_RESERVED] = "reserved",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(user_disposition, UserDisposition);
diff --git a/src/shared/user-record.h b/src/shared/user-record.h
new file mode 100644
index 0000000000000..83ef1b3f33c27
--- /dev/null
+++ b/src/shared/user-record.h
@@ -0,0 +1,356 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#include "sd-id128.h"
+
+#include "json.h"
+#include "missing_resource.h"
+#include "time-util.h"
+
+/* But some limits on disk sizes: not less than 5M, not more than 5T */
+#define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024)
+#define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024)
+
+/* The default disk size to use when nothing else is specified, relative to free disk space */
+#define USER_DISK_SIZE_DEFAULT_PERCENT 85
+
+typedef enum UserDisposition {
+ USER_INTRINSIC, /* root and nobody */
+ USER_SYSTEM, /* statically allocated users for system services */
+ USER_DYNAMIC, /* dynamically allocated users for system services */
+ USER_REGULAR, /* regular (typically human users) */
+ USER_CONTAINER, /* UID ranges allocated for container uses */
+ USER_RESERVED, /* Range above 2^31 */
+ _USER_DISPOSITION_MAX,
+ _USER_DISPOSITION_INVALID = -1,
+} UserDisposition;
+
+typedef enum UserHomeStorage {
+ USER_CLASSIC,
+ USER_LUKS,
+ USER_DIRECTORY, /* A directory, and a .identity file in it, which USER_CLASSIC lacks */
+ USER_SUBVOLUME,
+ USER_FSCRYPT,
+ USER_CIFS,
+ _USER_STORAGE_MAX,
+ _USER_STORAGE_INVALID = -1
+} UserStorage;
+
+typedef enum UserRecordMask {
+ /* The various sections an identity record may have, as bit mask */
+ USER_RECORD_REGULAR = 1U << 0,
+ USER_RECORD_SECRET = 1U << 1,
+ USER_RECORD_PRIVILEGED = 1U << 2,
+ USER_RECORD_PER_MACHINE = 1U << 3,
+ USER_RECORD_BINDING = 1U << 4,
+ USER_RECORD_STATUS = 1U << 5,
+ USER_RECORD_SIGNATURE = 1U << 6,
+ _USER_RECORD_MASK_MAX = (1U << 7)-1
+} UserRecordMask;
+
+typedef enum UserRecordLoadFlags {
+ /* A set of flags used while loading a user record from JSON data. We leave the lower 6 bits free,
+ * just as a safety precaution so that we can detect borked conversions between UserRecordMask and
+ * UserRecordLoadFlags. */
+
+ /* What to require */
+ USER_RECORD_REQUIRE_REGULAR = USER_RECORD_REGULAR << 7,
+ USER_RECORD_REQUIRE_SECRET = USER_RECORD_SECRET << 7,
+ USER_RECORD_REQUIRE_PRIVILEGED = USER_RECORD_PRIVILEGED << 7,
+ USER_RECORD_REQUIRE_PER_MACHINE = USER_RECORD_PER_MACHINE << 7,
+ USER_RECORD_REQUIRE_BINDING = USER_RECORD_BINDING << 7,
+ USER_RECORD_REQUIRE_STATUS = USER_RECORD_STATUS << 7,
+ USER_RECORD_REQUIRE_SIGNATURE = USER_RECORD_SIGNATURE << 7,
+
+ /* What to allow */
+ USER_RECORD_ALLOW_REGULAR = USER_RECORD_REGULAR << 14,
+ USER_RECORD_ALLOW_SECRET = USER_RECORD_SECRET << 14,
+ USER_RECORD_ALLOW_PRIVILEGED = USER_RECORD_PRIVILEGED << 14,
+ USER_RECORD_ALLOW_PER_MACHINE = USER_RECORD_PER_MACHINE << 14,
+ USER_RECORD_ALLOW_BINDING = USER_RECORD_BINDING << 14,
+ USER_RECORD_ALLOW_STATUS = USER_RECORD_STATUS << 14,
+ USER_RECORD_ALLOW_SIGNATURE = USER_RECORD_SIGNATURE << 14,
+
+ /* What to strip */
+ USER_RECORD_STRIP_REGULAR = USER_RECORD_REGULAR << 21,
+ USER_RECORD_STRIP_SECRET = USER_RECORD_SECRET << 21,
+ USER_RECORD_STRIP_PRIVILEGED = USER_RECORD_PRIVILEGED << 21,
+ USER_RECORD_STRIP_PER_MACHINE = USER_RECORD_PER_MACHINE << 21,
+ USER_RECORD_STRIP_BINDING = USER_RECORD_BINDING << 21,
+ USER_RECORD_STRIP_STATUS = USER_RECORD_STATUS << 21,
+ USER_RECORD_STRIP_SIGNATURE = USER_RECORD_SIGNATURE << 21,
+
+ /* Some special combinations that deserve explicit names */
+ USER_RECORD_LOAD_FULL = USER_RECORD_REQUIRE_REGULAR |
+ USER_RECORD_ALLOW_SECRET |
+ USER_RECORD_ALLOW_PRIVILEGED |
+ USER_RECORD_ALLOW_PER_MACHINE |
+ USER_RECORD_ALLOW_BINDING |
+ USER_RECORD_ALLOW_STATUS |
+ USER_RECORD_ALLOW_SIGNATURE,
+
+ USER_RECORD_LOAD_REFUSE_SECRET = USER_RECORD_REQUIRE_REGULAR |
+ USER_RECORD_ALLOW_PRIVILEGED |
+ USER_RECORD_ALLOW_PER_MACHINE |
+ USER_RECORD_ALLOW_BINDING |
+ USER_RECORD_ALLOW_STATUS |
+ USER_RECORD_ALLOW_SIGNATURE,
+
+ USER_RECORD_LOAD_MASK_SECRET = USER_RECORD_REQUIRE_REGULAR |
+ USER_RECORD_ALLOW_PRIVILEGED |
+ USER_RECORD_ALLOW_PER_MACHINE |
+ USER_RECORD_ALLOW_BINDING |
+ USER_RECORD_ALLOW_STATUS |
+ USER_RECORD_ALLOW_SIGNATURE |
+ USER_RECORD_STRIP_SECRET,
+
+ USER_RECORD_EXTRACT_SECRET = USER_RECORD_REQUIRE_SECRET |
+ USER_RECORD_STRIP_REGULAR |
+ USER_RECORD_STRIP_PRIVILEGED |
+ USER_RECORD_STRIP_PER_MACHINE |
+ USER_RECORD_STRIP_BINDING |
+ USER_RECORD_STRIP_STATUS |
+ USER_RECORD_STRIP_SIGNATURE,
+
+ USER_RECORD_LOAD_SIGNABLE = USER_RECORD_REQUIRE_REGULAR |
+ USER_RECORD_ALLOW_PRIVILEGED |
+ USER_RECORD_ALLOW_PER_MACHINE,
+
+ USER_RECORD_EXTRACT_SIGNABLE = USER_RECORD_LOAD_SIGNABLE |
+ USER_RECORD_STRIP_SECRET |
+ USER_RECORD_STRIP_BINDING |
+ USER_RECORD_STRIP_STATUS |
+ USER_RECORD_STRIP_SIGNATURE,
+
+ USER_RECORD_LOAD_EMBEDDED = USER_RECORD_REQUIRE_REGULAR |
+ USER_RECORD_ALLOW_PRIVILEGED |
+ USER_RECORD_ALLOW_PER_MACHINE |
+ USER_RECORD_ALLOW_SIGNATURE,
+
+ USER_RECORD_EXTRACT_EMBEDDED = USER_RECORD_LOAD_EMBEDDED |
+ USER_RECORD_STRIP_SECRET |
+ USER_RECORD_STRIP_BINDING |
+ USER_RECORD_STRIP_STATUS,
+
+ /* Whether to log about loader errors beyond LOG_DEBUG */
+ USER_RECORD_LOG = 1U << 28,
+
+ /* Whether to ignore errors and load what we can */
+ USER_RECORD_PERMISSIVE = 1U << 29,
+} UserRecordLoadFlags;
+
+static inline UserRecordLoadFlags USER_RECORD_REQUIRE(UserRecordMask m) {
+ assert((m & ~_USER_RECORD_MASK_MAX) == 0);
+ return m << 7;
+}
+
+static inline UserRecordLoadFlags USER_RECORD_ALLOW(UserRecordMask m) {
+ assert((m & ~_USER_RECORD_MASK_MAX) == 0);
+ return m << 14;
+}
+
+static inline UserRecordLoadFlags USER_RECORD_STRIP(UserRecordMask m) {
+ assert((m & ~_USER_RECORD_MASK_MAX) == 0);
+ return m << 21;
+}
+
+static inline UserRecordMask USER_RECORD_REQUIRE_MASK(UserRecordLoadFlags f) {
+ return (f >> 7) & _USER_RECORD_MASK_MAX;
+}
+
+static inline UserRecordMask USER_RECORD_ALLOW_MASK(UserRecordLoadFlags f) {
+ return ((f >> 14) & _USER_RECORD_MASK_MAX) | USER_RECORD_REQUIRE_MASK(f);
+}
+
+static inline UserRecordMask USER_RECORD_STRIP_MASK(UserRecordLoadFlags f) {
+ return (f >> 21) & _USER_RECORD_MASK_MAX;
+}
+
+static inline JsonDispatchFlags USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(UserRecordLoadFlags flags) {
+ return (FLAGS_SET(flags, USER_RECORD_LOG) ? JSON_LOG : 0) |
+ (FLAGS_SET(flags, USER_RECORD_PERMISSIVE) ? JSON_PERMISSIVE : 0);
+}
+
+typedef struct UserRecord {
+ /* The following three fields are not part of the JSON record */
+ unsigned n_ref;
+ UserRecordMask mask;
+ bool incomplete; /* incomplete due to security restrictions. */
+
+ char *user_name;
+ char *realm;
+ char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */
+ char *real_name;
+ char *email_address;
+ char *password_hint;
+ char *icon_name;
+ char *location;
+
+ UserDisposition disposition;
+ uint64_t last_change_usec;
+ uint64_t last_password_change_usec;
+
+ char *shell;
+ mode_t umask;
+ char **environment;
+ char *time_zone;
+ char *preferred_language;
+ int nice_level;
+ struct rlimit *rlimits[_RLIMIT_MAX];
+
+ int locked; /* prohibit activation in general */
+ uint64_t not_before_usec; /* prohibit activation before this unix time */
+ uint64_t not_after_usec; /* prohibit activation after this unix time */
+
+ UserStorage storage;
+ uint64_t disk_size;
+ uint64_t disk_size_relative; /* Disk size, relative to the free bytes of the medium, normalized to UINT32_MAX = 100% */
+ char *skeleton_directory;
+ mode_t access_mode;
+
+ uint64_t tasks_max;
+ uint64_t memory_high;
+ uint64_t memory_max;
+ uint64_t cpu_weight;
+ uint64_t io_weight;
+
+ bool nosuid;
+ bool nodev;
+ bool noexec;
+
+ char **hashed_password;
+ char **ssh_authorized_keys;
+ char **password;
+
+ void *fscrypt_salt;
+ size_t fscrypt_salt_size;
+
+ char *cifs_domain;
+ char *cifs_user_name;
+ char *cifs_service;
+
+ char *image_path;
+ char *image_path_auto; /* when none is configured explicitly, this is where we place the implicit image */
+ char *home_directory;
+ char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */
+
+ uid_t uid;
+ gid_t gid;
+
+ char **member_of;
+
+ char *file_system_type;
+ sd_id128_t partition_uuid;
+ sd_id128_t luks_uuid;
+ sd_id128_t file_system_uuid;
+
+ int luks_discard;
+ char *luks_cipher;
+ char *luks_cipher_mode;
+ uint64_t luks_volume_key_size;
+ char *luks_pbkdf_hash_algorithm;
+ char *luks_pbkdf_type;
+ uint64_t luks_pbkdf_time_cost_usec;
+ uint64_t luks_pbkdf_memory_cost;
+ uint64_t luks_pbkdf_parallel_threads;
+
+ uint64_t disk_usage;
+ uint64_t disk_free;
+ uint64_t disk_ceiling;
+ uint64_t disk_floor;
+
+ char *state;
+ char *service;
+ int signed_locally;
+
+ uint64_t good_authentication_counter;
+ uint64_t bad_authentication_counter;
+ uint64_t last_good_authentication_usec;
+ uint64_t last_bad_authentication_usec;
+
+ uint64_t ratelimit_begin_usec;
+ uint64_t ratelimit_count;
+ uint64_t ratelimit_interval_usec;
+ uint64_t ratelimit_burst;
+
+ int removable;
+ int enforce_password_policy;
+ int auto_login;
+
+ uint64_t stop_delay_usec; /* How long to leave systemd --user around on log-out */
+ int kill_processes; /* Whether to kill user processes forcibly on log-out */
+
+ /* The following exist mostly so that we can cover the full /etc/shadow set of fields, we currently
+ * do not actually make use of these in the systemd codebase */
+ uint64_t password_change_min_usec; /* maps to .sp_min */
+ uint64_t password_change_max_usec; /* maps to .sp_max */
+ uint64_t password_change_warn_usec; /* maps to .sp_warn */
+ uint64_t password_change_inactive_usec; /* maps to .sp_inact */
+ int password_change_now; /* Require a password change immediately on next login (.sp_lstchg = 0) */
+
+ JsonVariant *json;
+} UserRecord;
+
+UserRecord* user_record_new(void);
+UserRecord* user_record_ref(UserRecord *h);
+UserRecord* user_record_unref(UserRecord *h);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UserRecord*, user_record_unref);
+
+int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags flags);
+int user_record_build(UserRecord **ret, ...);
+
+const char *user_record_user_name_and_realm(UserRecord *h);
+UserStorage user_record_storage(UserRecord *h);
+const char *user_record_file_system_type(UserRecord *h);
+const char *user_record_skeleton_directory(UserRecord *h);
+mode_t user_record_access_mode(UserRecord *h);
+const char *user_record_home_directory(UserRecord *h);
+const char *user_record_image_path(UserRecord *h);
+unsigned long user_record_mount_flags(UserRecord *h);
+const char *user_record_cifs_user_name(UserRecord *h);
+const char *user_record_shell(UserRecord *h);
+const char *user_record_real_name(UserRecord *h);
+bool user_record_luks_discard(UserRecord *h);
+const char *user_record_luks_cipher(UserRecord *h);
+const char *user_record_luks_cipher_mode(UserRecord *h);
+uint64_t user_record_luks_volume_key_size(UserRecord *h);
+const char* user_record_luks_pbkdf_type(UserRecord *h);
+usec_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h);
+uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h);
+uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h);
+const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h);
+gid_t user_record_gid(UserRecord *h);
+UserDisposition user_record_disposition(UserRecord *h);
+int user_record_removable(UserRecord *h);
+usec_t user_record_ratelimit_interval_usec(UserRecord *h);
+uint64_t user_record_ratelimit_burst(UserRecord *h);
+
+bool user_record_equal(UserRecord *a, UserRecord *b);
+bool user_record_compatible(UserRecord *a, UserRecord *b);
+int user_record_compare_last_change(UserRecord *a, UserRecord *b);
+
+usec_t user_record_ratelimit_next_try(UserRecord *h);
+
+int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret);
+int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask);
+
+int user_record_test_blocked(UserRecord *h);
+
+/* The following six are user by group-record.c, that's why we export them here */
+int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
+
+int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags);
+int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags);
+int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask);
+
+const char* user_storage_to_string(UserStorage t) _const_;
+UserStorage user_storage_from_string(const char *s) _pure_;
+
+const char* user_disposition_to_string(UserDisposition t) _const_;
+UserDisposition user_disposition_from_string(const char *s) _pure_;
diff --git a/src/shared/userdb.c b/src/shared/userdb.c
new file mode 100644
index 0000000000000..eef435d5a5eae
--- /dev/null
+++ b/src/shared/userdb.c
@@ -0,0 +1,1347 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "dirent-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "group-record-nss.h"
+#include "missing_syscall.h"
+#include "parse-util.h"
+#include "set.h"
+#include "socket-util.h"
+#include "strv.h"
+#include "user-record-nss.h"
+#include "user-util.h"
+#include "userdb.h"
+#include "varlink.h"
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, Varlink, varlink_unref);
+
+typedef enum LookupWhat {
+ LOOKUP_USER,
+ LOOKUP_GROUP,
+ LOOKUP_MEMBERSHIP,
+ _LOOKUP_WHAT_MAX,
+} LookupWhat;
+
+struct UserDBIterator {
+ LookupWhat what;
+ Set *links;
+ bool nss_covered:1;
+ bool nss_iterating:1;
+ bool synthesize_root:1;
+ bool synthesize_nobody:1;
+ int error;
+ int nss_lock;
+ unsigned n_found;
+ sd_event *event;
+ UserRecord *found_user; /* when .what == LOOKUP_USER */
+ GroupRecord *found_group; /* when .what == LOOKUP_GROUP */
+
+ char *found_user_name, *found_group_name; /* when .what == LOOKUP_MEMBERSHIP */
+ char **members_of_group;
+ size_t index_members_of_group;
+ char *filter_user_name;
+};
+
+UserDBIterator* userdb_iterator_free(UserDBIterator *iterator) {
+ if (!iterator)
+ return NULL;
+
+ set_free(iterator->links);
+
+ switch (iterator->what) {
+
+ case LOOKUP_USER:
+ user_record_unref(iterator->found_user);
+
+ if (iterator->nss_iterating)
+ endpwent();
+
+ break;
+
+ case LOOKUP_GROUP:
+ group_record_unref(iterator->found_group);
+
+ if (iterator->nss_iterating)
+ endgrent();
+
+ break;
+
+ case LOOKUP_MEMBERSHIP:
+ free(iterator->found_user_name);
+ free(iterator->found_group_name);
+ strv_free(iterator->members_of_group);
+ free(iterator->filter_user_name);
+
+ if (iterator->nss_iterating)
+ endgrent();
+
+ break;
+
+ default:
+ assert_not_reached("Unexpected state?");
+ }
+
+ sd_event_unref(iterator->event);
+ safe_close(iterator->nss_lock);
+
+ return mfree(iterator);
+}
+
+static UserDBIterator* userdb_iterator_new(LookupWhat what) {
+ UserDBIterator *i;
+
+ assert(what >= 0);
+ assert(what < _LOOKUP_WHAT_MAX);
+
+ i = new(UserDBIterator, 1);
+ if (!i)
+ return NULL;
+
+ *i = (UserDBIterator) {
+ .what = what,
+ .nss_lock = -1,
+ };
+
+ return i;
+}
+
+static int userdb_on_query_reply(
+ Varlink *link,
+ JsonVariant *parameters,
+ const char *error_id,
+ VarlinkReplyFlags flags,
+ void *userdata) {
+
+ UserDBIterator *iterator = userdata;
+ int r;
+
+ assert(iterator);
+
+ if (error_id) {
+ log_debug("Got lookup error: %s", error_id);
+
+ if (STR_IN_SET(error_id,
+ "io.systemd.UserDatabase.NoRecordFound",
+ "io.systemd.UserDatabase.ConflictingRecordFound"))
+ r = -ESRCH;
+ else if (streq(error_id, "io.systemd.UserDatabase.ServiceNotAvailable"))
+ r = -EHOSTDOWN;
+ else if (streq(error_id, VARLINK_ERROR_TIMEOUT))
+ r = -ETIMEDOUT;
+ else
+ r = -EIO;
+
+ goto finish;
+ }
+
+ switch (iterator->what) {
+
+ case LOOKUP_USER: {
+ struct user_data {
+ JsonVariant *record;
+ bool incomplete;
+ } user_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_data, record), 0 },
+ { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_data, incomplete), 0 },
+ {}
+ };
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+
+ assert_se(!iterator->found_user);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &user_data);
+ if (r < 0)
+ goto finish;
+
+ if (!user_data.record) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key");
+ goto finish;
+ }
+
+ hr = user_record_new();
+ if (!hr) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ r = user_record_load(hr, user_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ goto finish;
+
+ if (!hr->service) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "User record does not carry service information, refusing.");
+ goto finish;
+ }
+
+ hr->incomplete = user_data.incomplete;
+
+ /* We match the root user by the name since the name is our primary key. We match the nobody
+ * use by UID though, since the name might differ on OSes */
+ if (streq_ptr(hr->user_name, "root"))
+ iterator->synthesize_root = false;
+ if (hr->uid == UID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ iterator->found_user = TAKE_PTR(hr);
+ iterator->n_found++;
+
+ /* More stuff coming? then let's just exit cleanly here */
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ /* Otherwise, let's remove this link and exit cleanly then */
+ r = 0;
+ goto finish;
+ }
+
+ case LOOKUP_GROUP: {
+ struct group_data {
+ JsonVariant *record;
+ bool incomplete;
+ } group_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct group_data, record), 0 },
+ { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct group_data, incomplete), 0 },
+ {}
+ };
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+
+ assert_se(!iterator->found_group);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &group_data);
+ if (r < 0)
+ goto finish;
+
+ if (!group_data.record) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key");
+ goto finish;
+ }
+
+ g = group_record_new();
+ if (!g) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ r = group_record_load(g, group_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ goto finish;
+
+ if (!g->service) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Group record does not carry service information, refusing.");
+ goto finish;
+ }
+
+ g->incomplete = group_data.incomplete;
+
+ if (streq_ptr(g->group_name, "root"))
+ iterator->synthesize_root = false;
+ if (g->gid == GID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ iterator->found_group = TAKE_PTR(g);
+ iterator->n_found++;
+
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ r = 0;
+ goto finish;
+ }
+
+ case LOOKUP_MEMBERSHIP: {
+ struct membership_data {
+ const char *user_name;
+ const char *group_name;
+ } membership_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, user_name), JSON_SAFE },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, group_name), JSON_SAFE },
+ {}
+ };
+
+ assert(!iterator->found_user_name);
+ assert(!iterator->found_group_name);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &membership_data);
+ if (r < 0)
+ goto finish;
+
+ iterator->found_user_name = mfree(iterator->found_user_name);
+ iterator->found_group_name = mfree(iterator->found_group_name);
+
+ iterator->found_user_name = strdup(membership_data.user_name);
+ if (!iterator->found_user_name) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ iterator->found_group_name = strdup(membership_data.group_name);
+ if (!iterator->found_group_name) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ iterator->n_found++;
+
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ r = 0;
+ goto finish;
+ }
+
+ default:
+ assert_not_reached("unexpected lookup");
+ }
+
+finish:
+ /* If we got one ESRCH, let that win. This way when we do a wild dump we won't be tripped up by bad
+ * errors if at least one connection ended cleanly */
+ if (r == -ESRCH || iterator->error == 0)
+ iterator->error = -r;
+
+ assert_se(set_remove(iterator->links, link) == link);
+ link = varlink_unref(link);
+ return 0;
+}
+
+static int userdb_connect(
+ UserDBIterator *iterator,
+ const char *path,
+ const char *method,
+ bool more,
+ JsonVariant *query) {
+
+ _cleanup_(varlink_unrefp) Varlink *vl = NULL;
+ int r;
+
+ assert(iterator);
+ assert(path);
+ assert(method);
+
+ r = varlink_connect_address(&vl, path);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to connect to %s: %m", path);
+
+ varlink_set_userdata(vl, iterator);
+
+ if (!iterator->event) {
+ r = sd_event_new(&iterator->event);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to allocate event loop: %m");
+ }
+
+ r = varlink_attach_event(vl, iterator->event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+ (void) varlink_set_description(vl, path);
+
+ r = varlink_bind_reply(vl, userdb_on_query_reply);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to bind reply callback: %m");
+
+ if (more)
+ r = varlink_observe(vl, method, query);
+ else
+ r = varlink_invoke(vl, method, query);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to invoke varlink method: %m");
+
+ r = set_ensure_allocated(&iterator->links, &link_hash_ops);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to allocate set: %m");
+
+ r = set_put(iterator->links, vl);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add varlink connection to set: %m");
+
+ TAKE_PTR(vl);
+ return r;
+}
+
+static int userdb_start_query(
+ UserDBIterator *iterator,
+ const char *method,
+ bool more,
+ JsonVariant *query,
+ UserDBFlags flags) {
+
+ _cleanup_(strv_freep) char **except = NULL, **only = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ struct dirent *de;
+ const char *e;
+ int r, ret = 0;
+
+ assert(iterator);
+ assert(method);
+
+ e = getenv("SYSTEMD_BYPASS_USERDB");
+ if (e) {
+ r = parse_boolean(e);
+ if (r > 0)
+ return -ENOLINK;
+ if (r < 0) {
+ except = strv_split(e, ":");
+ if (!except)
+ return -ENOMEM;
+ }
+ }
+
+ e = getenv("SYSTEMD_ONLY_USERDB");
+ if (e) {
+ only = strv_split(e, ":");
+ if (!only)
+ return -ENOMEM;
+ }
+
+ /* First, let's talk to the multiplexer, if we can */
+ if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_AVOID_DYNAMIC_USER|USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE)) == 0 &&
+ !strv_contains(except, "io.systemd.Multiplexer") &&
+ (!only || strv_contains(only, "io.systemd.Multiplexer"))) {
+ _cleanup_(json_variant_unrefp) JsonVariant *patched_query = json_variant_ref(query);
+
+ r = json_variant_set_field_string(&patched_query, "service", "io.systemd.Multiplexer");
+ if (r < 0)
+ return log_debug_errno(r, "Unable to set service JSON field: %m");
+
+ r = userdb_connect(iterator, "/run/systemd/userdb/io.systemd.Multiplexer", method, more, patched_query);
+ if (r >= 0) {
+ iterator->nss_covered = true; /* The multiplexer does NSS */
+ return 0;
+ }
+ }
+
+ d = opendir("/run/systemd/userdb/");
+ if (!d) {
+ if (errno == ENOENT)
+ return -ESRCH;
+
+ return -errno;
+ }
+
+ FOREACH_DIRENT(de, d, return -errno) {
+ _cleanup_(json_variant_unrefp) JsonVariant *patched_query = NULL;
+ _cleanup_free_ char *p = NULL;
+ bool is_nss;
+
+ if (streq(de->d_name, "io.systemd.Multiplexer")) /* We already tried this above, don't try this again */
+ continue;
+
+ if (FLAGS_SET(flags, USERDB_AVOID_DYNAMIC_USER) &&
+ streq(de->d_name, "io.systemd.DynamicUser"))
+ continue;
+
+ /* Avoid NSS is this is requested. Note that we also skip NSS when we were asked to skip the
+ * multiplexer, since in that case it's safer to do NSS in the client side emulation below
+ * (and when we run as part of systemd-userdbd.service we don't want to talk to ourselves
+ * anyway). */
+ is_nss = streq(de->d_name, "io.systemd.NameServiceSwitch");
+ if ((flags & (USERDB_AVOID_NSS|USERDB_AVOID_MULTIPLEXER)) != 0 && is_nss)
+ continue;
+
+ if (strv_contains(except, de->d_name))
+ continue;
+
+ if (only && !strv_contains(only, de->d_name))
+ continue;
+
+ p = path_join("/run/systemd/userdb/", de->d_name);
+ if (!p)
+ return -ENOMEM;
+
+ patched_query = json_variant_ref(query);
+ r = json_variant_set_field_string(&patched_query, "service", de->d_name);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to set service JSON field: %m");
+
+ r = userdb_connect(iterator, p, method, more, patched_query);
+ if (is_nss && r >= 0) /* Turn off fallback NSS if we found the NSS service and could connect
+ * to it */
+ iterator->nss_covered = true;
+
+ if (ret == 0 && r < 0)
+ ret = r;
+ }
+
+ if (set_isempty(iterator->links))
+ return ret; /* propagate last error we saw if we couldn't connect to anything. */
+
+ /* We connected to some services, in this case, ignore the ones we failed on */
+ return 0;
+}
+
+static int userdb_process(
+ UserDBIterator *iterator,
+ UserRecord **ret_user_record,
+ GroupRecord **ret_group_record,
+ char **ret_user_name,
+ char **ret_group_name) {
+
+ int r;
+
+ assert(iterator);
+
+ for (;;) {
+ if (iterator->what == LOOKUP_USER && iterator->found_user) {
+ if (ret_user_record)
+ *ret_user_record = TAKE_PTR(iterator->found_user);
+ else
+ iterator->found_user = user_record_unref(iterator->found_user);
+
+ if (ret_group_record)
+ *ret_group_record = NULL;
+ if (ret_user_name)
+ *ret_user_name = NULL;
+ if (ret_group_name)
+ *ret_group_name = NULL;
+
+ return 0;
+ }
+
+ if (iterator->what == LOOKUP_GROUP && iterator->found_group) {
+ if (ret_group_record)
+ *ret_group_record = TAKE_PTR(iterator->found_group);
+ else
+ iterator->found_group = group_record_unref(iterator->found_group);
+
+ if (ret_user_record)
+ *ret_user_record = NULL;
+ if (ret_user_name)
+ *ret_user_name = NULL;
+ if (ret_group_name)
+ *ret_group_name = NULL;
+
+ return 0;
+ }
+
+ if (iterator->what == LOOKUP_MEMBERSHIP && iterator->found_user_name && iterator->found_group_name) {
+ if (ret_user_name)
+ *ret_user_name = TAKE_PTR(iterator->found_user_name);
+ else
+ iterator->found_user_name = mfree(iterator->found_user_name);
+
+ if (ret_group_name)
+ *ret_group_name = TAKE_PTR(iterator->found_group_name);
+ else
+ iterator->found_group_name = mfree(iterator->found_group_name);
+
+ if (ret_user_record)
+ *ret_user_record = NULL;
+ if (ret_group_record)
+ *ret_group_record = NULL;
+
+ return 0;
+ }
+
+ if (set_isempty(iterator->links)) {
+ if (iterator->error == 0)
+ return -ESRCH;
+
+ return -abs(iterator->error);
+ }
+
+ if (!iterator->event)
+ return -ESRCH;
+
+ r = sd_event_run(iterator->event, UINT64_MAX);
+ if (r < 0)
+ return r;
+ }
+}
+
+static int synthetic_root_user_build(UserRecord **ret) {
+ return user_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING("root")),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/root")),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+static int synthetic_nobody_user_build(UserRecord **ret) {
+ return user_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(NOBODY_USER_NAME)),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(UID_NOBODY)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)),
+ JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)),
+ JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ /* Make sure the NSS lookup doesn't recurse back to us. (EBUSY is fine here, it just means we
+ * already took the lock from our thread, which is totally OK.) */
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ /* Client-side NSS fallback */
+ r = nss_user_record_by_name(name, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (streq(name, "root"))
+ return synthetic_root_user_build(ret);
+
+ if (streq(name, NOBODY_USER_NAME) && synthesize_nobody())
+ return synthetic_nobody_user_build(ret);
+ }
+
+ return r;
+}
+
+int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!uid_is_valid(uid))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ /* Client-side NSS fallback */
+ r = nss_user_record_by_uid(uid, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (uid == 0)
+ return synthetic_root_user_build(ret);
+
+ if (uid == UID_NOBODY && synthesize_nobody())
+ return synthetic_nobody_user_build(ret);
+ }
+
+ return r;
+}
+
+int userdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE);
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", true, NULL, flags);
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) {
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setpwent();
+ iterator->nss_iterating = true;
+ goto finish;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE))
+ goto finish;
+
+ return r;
+
+finish:
+ *ret = TAKE_PTR(iterator);
+ return 0;
+}
+
+int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) {
+ int r;
+
+ assert(iterator);
+ assert(iterator->what == LOOKUP_USER);
+
+ if (iterator->nss_iterating) {
+ struct passwd *pw;
+
+ /* If NSS isn't covered elsewhere, let's iterate through it first, since it probably contains
+ * the more traditional sources, which are probably good to show first. */
+
+ pw = getpwent();
+ if (pw) {
+ _cleanup_free_ char *buffer = NULL;
+ bool incomplete = false;
+ struct spwd spwd;
+
+ if (streq_ptr(pw->pw_name, "root"))
+ iterator->synthesize_root = false;
+ if (pw->pw_uid == UID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ r = nss_spwd_for_passwd(pw, &spwd, &buffer);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, ret);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ (*ret)->incomplete = incomplete;
+ return r;
+ }
+
+ if (errno != 0)
+ log_debug_errno(errno, "Failure to iterate NSS user database, ignoring: %m");
+
+ iterator->nss_iterating = false;
+ endpwent();
+ }
+
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+
+ if (r < 0) {
+ if (iterator->synthesize_root) {
+ iterator->synthesize_root = false;
+ iterator->n_found++;
+ return synthetic_root_user_build(ret);
+ }
+
+ if (iterator->synthesize_nobody) {
+ iterator->synthesize_nobody = false;
+ iterator->n_found++;
+ return synthetic_nobody_user_build(ret);
+ }
+ }
+
+ /* if we found at least one entry, then ignore errors and indicate that we reached the end */
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+static int synthetic_root_group_build(GroupRecord **ret) {
+ return group_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING("root")),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+static int synthetic_nobody_group_build(GroupRecord **ret) {
+ return group_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(NOBODY_GROUP_NAME)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ r = nss_group_record_by_name(name, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (streq(name, "root"))
+ return synthetic_root_group_build(ret);
+
+ if (streq(name, NOBODY_GROUP_NAME) && synthesize_nobody())
+ return synthetic_nobody_group_build(ret);
+ }
+
+ return r;
+}
+
+int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!gid_is_valid(gid))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ r = nss_group_record_by_gid(gid, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (gid == 0)
+ return synthetic_root_group_build(ret);
+
+ if (gid == GID_NOBODY && synthesize_nobody())
+ return synthetic_nobody_group_build(ret);
+ }
+
+ return r;
+}
+
+int groupdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE);
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", true, NULL, flags);
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) {
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setgrent();
+ iterator->nss_iterating = true;
+ goto finish;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE))
+ goto finish;
+
+ return r;
+
+finish:
+ *ret = TAKE_PTR(iterator);
+ return 0;
+}
+
+int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) {
+ int r;
+
+ assert(iterator);
+ assert(iterator->what == LOOKUP_GROUP);
+
+ if (iterator->nss_iterating) {
+ struct group *gr;
+
+ errno = 0;
+ gr = getgrent();
+ if (gr) {
+ _cleanup_free_ char *buffer = NULL;
+ bool incomplete = false;
+ struct sgrp sgrp;
+
+ if (streq_ptr(gr->gr_name, "root"))
+ iterator->synthesize_root = false;
+ if (gr->gr_gid == GID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ r = nss_sgrp_for_group(gr, &sgrp, &buffer);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", gr->gr_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, ret);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ (*ret)->incomplete = incomplete;
+ return r;
+ }
+
+ if (errno != 0)
+ log_debug_errno(errno, "Failure to iterate NSS group database, ignoring: %m");
+
+ iterator->nss_iterating = false;
+ endgrent();
+ }
+
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+
+ if (r < 0) {
+ if (iterator->synthesize_root) {
+ iterator->synthesize_root = false;
+ iterator->n_found++;
+ return synthetic_root_group_build(ret);
+ }
+
+ if (iterator->synthesize_nobody) {
+ iterator->synthesize_nobody = false;
+ iterator->n_found++;
+ return synthetic_nobody_group_build(ret);
+ }
+ }
+
+ /* if we found at least one entry, then ignore errors and indicate that we reached the end */
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ assert(ret);
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ iterator->filter_user_name = strdup(name);
+ if (!iterator->filter_user_name)
+ return -ENOMEM;
+
+ setgrent();
+ iterator->nss_iterating = true;
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+ return r;
+}
+
+int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+ int r;
+
+ assert(ret);
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
+
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ /* We ignore all errors here, since the group might be defined by a userdb native service, and we queried them already above. */
+ (void) nss_group_record_by_name(name, &gr);
+ if (gr) {
+ iterator->members_of_group = strv_copy(gr->members);
+ if (!iterator->members_of_group)
+ return -ENOMEM;
+
+ iterator->index_members_of_group = 0;
+
+ iterator->found_group_name = strdup(name);
+ if (!iterator->found_group_name)
+ return -ENOMEM;
+ }
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+
+ return r;
+}
+
+int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags);
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setgrent();
+ iterator->nss_iterating = true;
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+
+ return r;
+}
+
+int membershipdb_iterator_get(
+ UserDBIterator *iterator,
+ char **ret_user,
+ char **ret_group) {
+
+ int r;
+
+ assert(iterator);
+
+ for (;;) {
+ /* If we are iteratring through NSS acquire a new group entry if we haven't acquired one yet. */
+ if (!iterator->members_of_group) {
+ struct group *g;
+
+ if (!iterator->nss_iterating)
+ break;
+
+ assert(!iterator->found_user_name);
+ do {
+ errno = 0;
+ g = getgrent();
+ if (!g) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failure during NSS group iteration, ignoring: %m");
+ break;
+ }
+
+ } while (iterator->filter_user_name ? !strv_contains(g->gr_mem, iterator->filter_user_name) :
+ strv_isempty(g->gr_mem));
+
+ if (g) {
+ r = free_and_strdup(&iterator->found_group_name, g->gr_name);
+ if (r < 0)
+ return r;
+
+ if (iterator->filter_user_name)
+ iterator->members_of_group = strv_new(iterator->filter_user_name);
+ else
+ iterator->members_of_group = strv_copy(g->gr_mem);
+ if (!iterator->members_of_group)
+ return -ENOMEM;
+
+ iterator->index_members_of_group = 0;
+ } else {
+ iterator->nss_iterating = false;
+ endgrent();
+ break;
+ }
+ }
+
+ assert(iterator->found_group_name);
+ assert(iterator->members_of_group);
+ assert(!iterator->found_user_name);
+
+ if (iterator->members_of_group[iterator->index_members_of_group]) {
+ _cleanup_free_ char *cu = NULL, *cg = NULL;
+
+ if (ret_user) {
+ cu = strdup(iterator->members_of_group[iterator->index_members_of_group]);
+ if (!cu)
+ return -ENOMEM;
+ }
+
+ if (ret_group) {
+ cg = strdup(iterator->found_group_name);
+ if (!cg)
+ return -ENOMEM;
+ }
+
+ if (ret_user)
+ *ret_user = TAKE_PTR(cu);
+
+ if (ret_group)
+ *ret_group = TAKE_PTR(cg);
+
+ iterator->index_members_of_group++;
+ return 0;
+ }
+
+ iterator->members_of_group = strv_free(iterator->members_of_group);
+ iterator->found_group_name = mfree(iterator->found_group_name);
+ }
+
+ r = userdb_process(iterator, NULL, NULL, ret_user, ret_group);
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_strv_free_ char **members = NULL;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ r = membershipdb_by_group(name, flags, &iterator);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_free_ char *user_name = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user_name, NULL);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&members, TAKE_PTR(user_name));
+ if (r < 0)
+ return r;
+ }
+
+ strv_sort(members);
+ strv_uniq(members);
+
+ *ret = TAKE_PTR(members);
+ return 0;
+}
+
+static int userdb_thread_sockaddr(struct sockaddr_un *ret_sa, socklen_t *ret_salen) {
+ static const uint8_t
+ k1[16] = { 0x35, 0xc1, 0x1f, 0x41, 0x59, 0xc6, 0xa0, 0xf9, 0x33, 0x4b, 0x17, 0x3d, 0xb9, 0xf6, 0x14, 0xd9 },
+ k2[16] = { 0x6a, 0x11, 0x4c, 0x37, 0xe5, 0xa3, 0x8c, 0xa6, 0x93, 0x55, 0x64, 0x8c, 0x93, 0xee, 0xa1, 0x7b };
+
+ struct siphash sh;
+ uint64_t x, y;
+ pid_t tid;
+ void *p;
+
+ assert(ret_sa);
+ assert(ret_salen);
+
+ /* This calculates an AF_UNIX socket address in the abstract namespace whose existance works as an
+ * indicator whether to emulate NSS records for complex user records that are also available via the
+ * varlink protocol. The name of the socket is picked in a way so that:
+ *
+ * → it is per-thread (by hashing from the TID)
+ *
+ * → is not guessable for foreign processes (by hashing from the — hopefully secret — AT_RANDOM
+ * value every process gets passed from the kernel
+ *
+ * By using a socket the NSS emulation can be nicely turned off for limited amounts of time only,
+ * simply controlled by the lifetime of the fd itself. By using an AF_UNIX socket in the abstract
+ * namespace the lock is automatically cleaned up when the process dies abnormally.
+ *
+ */
+
+ p = ULONG_TO_PTR(getauxval(AT_RANDOM));
+ if (!p)
+ return -EIO;
+
+ tid = gettid();
+
+ siphash24_init(&sh, k1);
+ siphash24_compress(p, 16, &sh);
+ siphash24_compress(&tid, sizeof(tid), &sh);
+ x = siphash24_finalize(&sh);
+
+ siphash24_init(&sh, k2);
+ siphash24_compress(p, 16, &sh);
+ siphash24_compress(&tid, sizeof(tid), &sh);
+ y = siphash24_finalize(&sh);
+
+ *ret_sa = (struct sockaddr_un) {
+ .sun_family = AF_UNIX,
+ };
+
+ sprintf(ret_sa->sun_path + 1, "userdb-%016" PRIx64 "%016" PRIx64, x, y);
+ *ret_salen = offsetof(struct sockaddr_un, sun_path) + 1 + 7 + 32;
+
+ return 0;
+}
+
+int userdb_nss_compat_is_enabled(void) {
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ socklen_t salen;
+ int r;
+
+ /* Tests whether the NSS compatibility logic is currently turned on for the invoking thread. Returns
+ * true if NSS compatibility is turned on, i.e. whether NSS records shall be synthesized from complex
+ * user records. */
+
+ r = userdb_thread_sockaddr(&sa.un, &salen);
+ if (r < 0)
+ return r;
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -errno;
+
+ /* Try to connect(). This doesn't do anything really, except that it checks whether the socket
+ * address is bound at all. */
+ if (connect(fd, &sa.sa, salen) < 0) {
+ if (errno == ECONNREFUSED) /* the socket is not bound, hence NSS emulation shall be done */
+ return true;
+
+ return -errno;
+ }
+
+ return false;
+}
+
+int userdb_nss_compat_disable(void) {
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ socklen_t salen;
+ int r;
+
+ /* Turn off the NSS compatibility logic for the invoking thread. By default NSS records are
+ * synthesized for all complex user records looked up via NSS. If this call is invoked this is
+ * disabled for the invoking thread, but only for it. A caller that natively supports the varlink
+ * user record protocol may use that to turn off the compatibility for NSS lookups. */
+
+ r = userdb_thread_sockaddr(&sa.un, &salen);
+ if (r < 0)
+ return r;
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ return -errno;
+
+ if (bind(fd, &sa.sa, salen) < 0) {
+ if (errno == EADDRINUSE) /* lock already taken, convert this into a recognizable error */
+ return -EBUSY;
+
+ return -errno;
+ }
+
+ return TAKE_FD(fd);
+}
diff --git a/src/shared/userdb.h b/src/shared/userdb.h
new file mode 100644
index 0000000000000..4288b0ff95dfc
--- /dev/null
+++ b/src/shared/userdb.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#include "group-record.h"
+#include "user-record.h"
+
+/* Inquire local services for user/group records */
+
+typedef struct UserDBIterator UserDBIterator;
+
+UserDBIterator *userdb_iterator_free(UserDBIterator *iterator);
+DEFINE_TRIVIAL_CLEANUP_FUNC(UserDBIterator*, userdb_iterator_free);
+
+typedef enum UserDBFlags {
+ USERDB_AVOID_NSS = 1 << 0, /* don't do client-side nor server-side NSS */
+ USERDB_AVOID_DYNAMIC_USER = 1 << 1, /* exclude looking up in io.systemd.DynamicUser */
+ USERDB_AVOID_MULTIPLEXER = 1 << 2, /* exclude looking up via io.systemd.Multiplexer */
+ USERDB_DONT_SYNTHESIZE = 1 << 3, /* don't synthesize root/nobody */
+} UserDBFlags;
+
+int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret);
+int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret);
+int userdb_all(UserDBFlags flags, UserDBIterator **ret);
+int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret);
+
+int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret);
+int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret);
+int groupdb_all(UserDBFlags flags, UserDBIterator **ret);
+int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret);
+
+int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret);
+int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret);
+int membershipdb_all(UserDBFlags flags, UserDBIterator **ret);
+int membershipdb_iterator_get(UserDBIterator *iterator, char **user, char **group);
+int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret);
+
+int userdb_nss_compat_is_enabled(void);
+int userdb_nss_compat_disable(void);
diff --git a/src/shared/varlink.c b/src/shared/varlink.c
index 99343167f66c2..7f59fde4d8c2b 100644
--- a/src/shared/varlink.c
+++ b/src/shared/varlink.c
@@ -29,6 +29,7 @@ typedef enum VarlinkState {
/* Client side states */
VARLINK_IDLE_CLIENT,
VARLINK_AWAITING_REPLY,
+ VARLINK_AWAITING_REPLY_MORE,
VARLINK_CALLING,
VARLINK_CALLED,
VARLINK_PROCESSING_REPLY,
@@ -39,7 +40,6 @@ typedef enum VarlinkState {
VARLINK_PROCESSING_METHOD_MORE,
VARLINK_PROCESSING_METHOD_ONEWAY,
VARLINK_PROCESSED_METHOD,
- VARLINK_PROCESSED_METHOD_MORE,
VARLINK_PENDING_METHOD,
VARLINK_PENDING_METHOD_MORE,
@@ -63,6 +63,7 @@ typedef enum VarlinkState {
IN_SET(state, \
VARLINK_IDLE_CLIENT, \
VARLINK_AWAITING_REPLY, \
+ VARLINK_AWAITING_REPLY_MORE, \
VARLINK_CALLING, \
VARLINK_CALLED, \
VARLINK_PROCESSING_REPLY, \
@@ -71,7 +72,6 @@ typedef enum VarlinkState {
VARLINK_PROCESSING_METHOD_MORE, \
VARLINK_PROCESSING_METHOD_ONEWAY, \
VARLINK_PROCESSED_METHOD, \
- VARLINK_PROCESSED_METHOD_MORE, \
VARLINK_PENDING_METHOD, \
VARLINK_PENDING_METHOD_MORE)
@@ -185,6 +185,7 @@ struct VarlinkServer {
static const char* const varlink_state_table[_VARLINK_STATE_MAX] = {
[VARLINK_IDLE_CLIENT] = "idle-client",
[VARLINK_AWAITING_REPLY] = "awaiting-reply",
+ [VARLINK_AWAITING_REPLY_MORE] = "awaiting-reply-more",
[VARLINK_CALLING] = "calling",
[VARLINK_CALLED] = "called",
[VARLINK_PROCESSING_REPLY] = "processing-reply",
@@ -193,7 +194,6 @@ static const char* const varlink_state_table[_VARLINK_STATE_MAX] = {
[VARLINK_PROCESSING_METHOD_MORE] = "processing-method-more",
[VARLINK_PROCESSING_METHOD_ONEWAY] = "processing-method-oneway",
[VARLINK_PROCESSED_METHOD] = "processed-method",
- [VARLINK_PROCESSED_METHOD_MORE] = "processed-method-more",
[VARLINK_PENDING_METHOD] = "pending-method",
[VARLINK_PENDING_METHOD_MORE] = "pending-method-more",
[VARLINK_PENDING_DISCONNECT] = "pending-disconnect",
@@ -287,6 +287,8 @@ int varlink_connect_address(Varlink **ret, const char *address) {
if (v->fd < 0)
return -errno;
+ v->fd = fd_move_above_stdio(v->fd);
+
if (connect(v->fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) {
if (!IN_SET(errno, EAGAIN, EINPROGRESS))
return -errno;
@@ -405,7 +407,7 @@ static int varlink_test_disconnect(Varlink *v) {
goto disconnect;
/* If we are waiting for incoming data but the read side is shut down, disconnect. */
- if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected)
+ if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected)
goto disconnect;
/* Similar, if are a client that hasn't written anything yet but the write side is dead, also
@@ -478,7 +480,7 @@ static int varlink_read(Varlink *v) {
assert(v);
- if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER))
+ if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER))
return 0;
if (v->connecting) /* read() on a socket while we are in connect() will fail with EINVAL, hence exit early here */
return 0;
@@ -578,7 +580,7 @@ static int varlink_parse_message(Varlink *v) {
varlink_log(v, "New incoming message: %s", begin);
- r = json_parse(begin, &v->current, NULL, NULL);
+ r = json_parse(begin, 0, &v->current, NULL, NULL);
if (r < 0)
return r;
@@ -596,7 +598,7 @@ static int varlink_parse_message(Varlink *v) {
static int varlink_test_timeout(Varlink *v) {
assert(v);
- if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING))
+ if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING))
return 0;
if (v->timeout == USEC_INFINITY)
return 0;
@@ -673,7 +675,7 @@ static int varlink_dispatch_reply(Varlink *v) {
assert(v);
- if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING))
+ if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING))
return 0;
if (!v->current)
return 0;
@@ -715,6 +717,11 @@ static int varlink_dispatch_reply(Varlink *v) {
goto invalid;
}
+ /* Replies with 'continue' set are only OK we set 'more' when we initiated the method */
+ if (v->state != VARLINK_AWAITING_REPLY_MORE && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ goto invalid;
+
+ /* An error is final */
if (error && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
goto invalid;
@@ -722,7 +729,7 @@ static int varlink_dispatch_reply(Varlink *v) {
if (r < 0)
goto invalid;
- if (v->state == VARLINK_AWAITING_REPLY) {
+ if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE)) {
varlink_set_state(v, VARLINK_PROCESSING_REPLY);
if (v->reply_callback) {
@@ -734,17 +741,18 @@ static int varlink_dispatch_reply(Varlink *v) {
v->current = json_variant_unref(v->current);
if (v->state == VARLINK_PROCESSING_REPLY) {
+
assert(v->n_pending > 0);
- v->n_pending--;
- varlink_set_state(v, v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY);
+ if (!FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ v->n_pending--;
+
+ varlink_set_state(v,
+ FLAGS_SET(flags, VARLINK_REPLY_CONTINUES) ? VARLINK_AWAITING_REPLY_MORE :
+ v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY);
}
} else {
assert(v->state == VARLINK_CALLING);
-
- if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
- goto invalid;
-
varlink_set_state(v, VARLINK_CALLED);
}
@@ -878,7 +886,6 @@ static int varlink_dispatch_method(Varlink *v) {
varlink_set_state(v, VARLINK_PENDING_METHOD);
break;
- case VARLINK_PROCESSED_METHOD_MORE: /* One reply for a "more" message was sent, more to come */
case VARLINK_PROCESSING_METHOD_MORE: /* No reply for a "more" message was sent, more to come */
varlink_set_state(v, VARLINK_PENDING_METHOD_MORE);
break;
@@ -1073,7 +1080,7 @@ int varlink_get_events(Varlink *v) {
return EPOLLOUT;
if (!v->read_disconnected &&
- IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) &&
+ IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) &&
!v->current &&
v->input_buffer_unscanned <= 0)
ret |= EPOLLIN;
@@ -1091,7 +1098,7 @@ int varlink_get_timeout(Varlink *v, usec_t *ret) {
if (v->state == VARLINK_DISCONNECTED)
return -ENOTCONN;
- if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING) &&
+ if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING) &&
v->timeout != USEC_INFINITY) {
if (ret)
*ret = usec_add(v->timestamp, v->timeout);
@@ -1189,6 +1196,15 @@ int varlink_close(Varlink *v) {
return 1;
}
+Varlink* varlink_close_unref(Varlink *v) {
+
+ if (!v)
+ return NULL;
+
+ (void) varlink_close(v);
+ return varlink_unref(v);
+}
+
Varlink* varlink_flush_close_unref(Varlink *v) {
if (!v)
@@ -1196,7 +1212,6 @@ Varlink* varlink_flush_close_unref(Varlink *v) {
(void) varlink_flush(v);
(void) varlink_close(v);
-
return varlink_unref(v);
}
@@ -1259,6 +1274,8 @@ int varlink_send(Varlink *v, const char *method, JsonVariant *parameters) {
if (v->state == VARLINK_DISCONNECTED)
return -ENOTCONN;
+
+ /* We allow enqueuing multiple method calls at once! */
if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY))
return -EBUSY;
@@ -1308,6 +1325,8 @@ int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters) {
if (v->state == VARLINK_DISCONNECTED)
return -ENOTCONN;
+
+ /* We allow enqueing multiple method calls at once! */
if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY))
return -EBUSY;
@@ -1349,6 +1368,60 @@ int varlink_invokeb(Varlink *v, const char *method, ...) {
return varlink_invoke(v, method, parameters);
}
+int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters) {
+ _cleanup_(json_variant_unrefp) JsonVariant *m = NULL;
+ int r;
+
+ assert_return(v, -EINVAL);
+ assert_return(method, -EINVAL);
+
+ if (v->state == VARLINK_DISCONNECTED)
+ return -ENOTCONN;
+ /* Note that we don't allow enqueuing multiple method calls when we are in more/continues mode! We
+ * thus insist on an idle client here. */
+ if (v->state != VARLINK_IDLE_CLIENT)
+ return -EBUSY;
+
+ r = varlink_sanitize_parameters(¶meters);
+ if (r < 0)
+ return r;
+
+ r = json_build(&m, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)),
+ JSON_BUILD_PAIR("parameters", JSON_BUILD_VARIANT(parameters)),
+ JSON_BUILD_PAIR("more", JSON_BUILD_BOOLEAN(true))));
+ if (r < 0)
+ return r;
+
+ r = varlink_enqueue_json(v, m);
+ if (r < 0)
+ return r;
+
+
+ varlink_set_state(v, VARLINK_AWAITING_REPLY_MORE);
+ v->n_pending++;
+ v->timestamp = now(CLOCK_MONOTONIC);
+
+ return 0;
+}
+
+int varlink_observeb(Varlink *v, const char *method, ...) {
+ _cleanup_(json_variant_unrefp) JsonVariant *parameters = NULL;
+ va_list ap;
+ int r;
+
+ assert_return(v, -EINVAL);
+
+ va_start(ap, method);
+ r = json_buildv(¶meters, ap);
+ va_end(ap);
+
+ if (r < 0)
+ return r;
+
+ return varlink_observe(v, method, parameters);
+}
+
int varlink_call(
Varlink *v,
const char *method,
@@ -2157,6 +2230,8 @@ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t
if (fd < 0)
return -errno;
+ fd = fd_move_above_stdio(fd);
+
(void) sockaddr_un_unlink(&sockaddr.un);
RUN_WITH_UMASK(~m & 0777)
@@ -2339,17 +2414,18 @@ int varlink_server_bind_connect(VarlinkServer *s, VarlinkConnect callback) {
}
unsigned varlink_server_connections_max(VarlinkServer *s) {
- struct rlimit rl;
+ int dts;
/* If a server is specified, return the setting for that server, otherwise the default value */
if (s)
return s->connections_max;
- assert_se(getrlimit(RLIMIT_NOFILE, &rl) >= 0);
+ dts = getdtablesize();
+ assert_se(dts > 0);
/* Make sure we never use up more than ¾th of RLIMIT_NOFILE for IPC */
- if (VARLINK_DEFAULT_CONNECTIONS_MAX > rl.rlim_cur / 4 * 3)
- return rl.rlim_cur / 4 * 3;
+ if (VARLINK_DEFAULT_CONNECTIONS_MAX > (unsigned) dts / 4 * 3)
+ return dts / 4 * 3;
return VARLINK_DEFAULT_CONNECTIONS_MAX;
}
diff --git a/src/shared/varlink.h b/src/shared/varlink.h
index d96fa93619a7e..0d9617d40352f 100644
--- a/src/shared/varlink.h
+++ b/src/shared/varlink.h
@@ -73,6 +73,7 @@ int varlink_flush(Varlink *v);
int varlink_close(Varlink *v);
Varlink* varlink_flush_close_unref(Varlink *v);
+Varlink* varlink_close_unref(Varlink *v);
/* Enqueue method call, not expecting a reply */
int varlink_send(Varlink *v, const char *method, JsonVariant *parameters);
@@ -86,6 +87,10 @@ int varlink_callb(Varlink *v, const char *method, JsonVariant **ret_parameters,
int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters);
int varlink_invokeb(Varlink *v, const char *method, ...);
+/* Enqueue method call, expect a reply now, and possibly more later, which are all delivered to the reply callback */
+int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters);
+int varlink_observeb(Varlink *v, const char *method, ...);
+
/* Enqueue a final reply */
int varlink_reply(Varlink *v, JsonVariant *parameters);
int varlink_replyb(Varlink *v, ...);
@@ -148,6 +153,7 @@ int varlink_server_set_connections_max(VarlinkServer *s, unsigned m);
int varlink_server_set_description(VarlinkServer *s, const char *description);
DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_unref);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_close_unref);
DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_flush_close_unref);
DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref);
diff --git a/src/sleep/sleep.c b/src/sleep/sleep.c
index b9fe96635d22b..3b0f4f462424b 100644
--- a/src/sleep/sleep.c
+++ b/src/sleep/sleep.c
@@ -18,11 +18,12 @@
#include "sd-messages.h"
#include "btrfs-util.h"
+#include "bus-error.h"
#include "def.h"
#include "exec-util.h"
#include "fd-util.h"
-#include "format-util.h"
#include "fileio.h"
+#include "format-util.h"
#include "log.h"
#include "main-func.h"
#include "parse-util.h"
@@ -182,6 +183,49 @@ static int configure_hibernation(void) {
return write_hibernate_location_info();
}
+static int lock_all_homes(void) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ /* Let's synchronously lock all home directories managed by homed that have been marked for it. This
+ * way the key material required to access these volumes is hopefully removed from memory. */
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to connect to system bus, ignoring: %m");
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.home1",
+ "/org/freedesktop/home1",
+ "org.freedesktop.home1.Manager",
+ "LockAllHomes");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* If homed is not running it can't have any home directories active either. */
+ r = sd_bus_message_set_auto_start(m, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to disable auto-start of LockAllHomes() message: %m");
+
+ r = sd_bus_call(bus, m, DEFAULT_TIMEOUT_USEC, &error, NULL);
+ if (r < 0) {
+ if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) ||
+ sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) {
+ log_debug("systemd-homed is not running, skipping locking of home.");
+ return 0;
+ }
+
+ return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r));
+ }
+
+ log_debug("Successfully requested that all home directories shall be suspended.");
+ return 0;
+}
+
static int execute(char **modes, char **states) {
char *arguments[] = {
NULL,
@@ -217,6 +261,7 @@ static int execute(char **modes, char **states) {
}
(void) execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, arguments, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+ (void) lock_all_homes();
log_struct(LOG_INFO,
"MESSAGE_ID=" SD_MESSAGE_SLEEP_START_STR,
diff --git a/src/systemd/sd-bus-vtable.h b/src/systemd/sd-bus-vtable.h
index 0f43554d82519..95b5914236a47 100644
--- a/src/systemd/sd-bus-vtable.h
+++ b/src/systemd/sd-bus-vtable.h
@@ -43,6 +43,7 @@ enum {
SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE = 1ULL << 5,
SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION = 1ULL << 6,
SD_BUS_VTABLE_PROPERTY_EXPLICIT = 1ULL << 7,
+ SD_BUS_VTABLE_SENSITIVE = 1ULL << 8, /* covers both directions: method call + reply */
_SD_BUS_VTABLE_CAPABILITY_MASK = 0xFFFFULL << 40
};
diff --git a/src/systemd/sd-bus.h b/src/systemd/sd-bus.h
index 84ceb62dc79c7..3c9792e4975e2 100644
--- a/src/systemd/sd-bus.h
+++ b/src/systemd/sd-bus.h
@@ -328,6 +328,7 @@ int sd_bus_message_peek_type(sd_bus_message *m, char *type, const char **content
int sd_bus_message_verify_type(sd_bus_message *m, char type, const char *contents);
int sd_bus_message_at_end(sd_bus_message *m, int complete);
int sd_bus_message_rewind(sd_bus_message *m, int complete);
+int sd_bus_message_sensitive(sd_bus_message *m);
/* Bus management */
diff --git a/src/test/test-dissect-image.c b/src/test/test-dissect-image.c
index 7b32e8373f920..12685dad1373d 100644
--- a/src/test/test-dissect-image.c
+++ b/src/test/test-dissect-image.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include
+#include
#include
#include "dissect-image.h"
@@ -21,7 +22,7 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE;
}
- r = loop_device_make_by_path(argv[1], O_RDONLY, &d);
+ r = loop_device_make_by_path(argv[1], O_RDONLY, LO_FLAGS_PARTSCAN, &d);
if (r < 0) {
log_error_errno(r, "Failed to set up loopback device: %m");
return EXIT_FAILURE;
diff --git a/src/test/test-json.c b/src/test/test-json.c
index a6613043b9240..c72cd7427432c 100644
--- a/src/test/test-json.c
+++ b/src/test/test-json.c
@@ -82,7 +82,7 @@ static void test_variant(const char *data, Test test) {
_cleanup_free_ char *s = NULL;
int r;
- r = json_parse(data, &v, NULL, NULL);
+ r = json_parse(data, 0, &v, NULL, NULL);
assert_se(r == 0);
assert_se(v);
@@ -93,7 +93,7 @@ static void test_variant(const char *data, Test test) {
log_info("formatted normally: %s\n", s);
- r = json_parse(data, &w, NULL, NULL);
+ r = json_parse(data, JSON_PARSE_SENSITIVE, &w, NULL, NULL);
assert_se(r == 0);
assert_se(w);
assert_se(json_variant_has_type(v, json_variant_type(w)));
@@ -110,7 +110,7 @@ static void test_variant(const char *data, Test test) {
log_info("formatted prettily:\n%s", s);
- r = json_parse(data, &w, NULL, NULL);
+ r = json_parse(data, 0, &w, NULL, NULL);
assert_se(r == 0);
assert_se(w);
@@ -302,7 +302,7 @@ static void test_build(void) {
assert_se(json_variant_format(a, 0, &s) >= 0);
log_info("GOT: %s\n", s);
- assert_se(json_parse(s, &b, NULL, NULL) >= 0);
+ assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0);
assert_se(json_variant_equal(a, b));
a = json_variant_unref(a);
@@ -313,7 +313,7 @@ static void test_build(void) {
s = mfree(s);
assert_se(json_variant_format(a, 0, &s) >= 0);
log_info("GOT: %s\n", s);
- assert_se(json_parse(s, &b, NULL, NULL) >= 0);
+ assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0);
assert_se(json_variant_format(b, 0, &t) >= 0);
log_info("GOT: %s\n", t);
@@ -365,7 +365,7 @@ static void test_source(void) {
assert_se(f = fmemopen_unlocked((void*) data, strlen(data), "r"));
- assert_se(json_parse_file(f, "waldo", &v, NULL, NULL) >= 0);
+ assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) >= 0);
printf("--- non-pretty begin ---\n");
json_variant_dump(v, 0, stdout, NULL);
@@ -415,6 +415,93 @@ static void test_depth(void) {
fputs("\n", stdout);
}
+static void test_normalize(void) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_free_ char *t = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")),
+ JSON_BUILD_PAIR("c", JSON_BUILD_STRING("y")),
+ JSON_BUILD_PAIR("a", JSON_BUILD_STRING("z")))) >= 0);
+
+ assert_se(!json_variant_is_sorted(v));
+ assert_se(!json_variant_is_normalized(v));
+
+ assert_se(json_variant_format(v, 0, &t) >= 0);
+ assert_se(streq(t, "{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}"));
+ t = mfree(t);
+
+ assert_se(json_build(&w, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("bar", JSON_BUILD_STRING("zzz")),
+ JSON_BUILD_PAIR("foo", JSON_BUILD_VARIANT(v)))) >= 0);
+
+ assert_se(json_variant_is_sorted(w));
+ assert_se(!json_variant_is_normalized(w));
+
+ assert_se(json_variant_format(w, 0, &t) >= 0);
+ assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}}"));
+ t = mfree(t);
+
+ assert_se(json_variant_sort(&v) >= 0);
+ assert_se(json_variant_is_sorted(v));
+ assert_se(json_variant_is_normalized(v));
+
+ assert_se(json_variant_format(v, 0, &t) >= 0);
+ assert_se(streq(t, "{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}"));
+ t = mfree(t);
+
+ assert_se(json_variant_normalize(&w) >= 0);
+ assert_se(json_variant_is_sorted(w));
+ assert_se(json_variant_is_normalized(w));
+
+ assert_se(json_variant_format(w, 0, &t) >= 0);
+ assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}}"));
+ t = mfree(t);
+}
+
+static void test_bisect(void) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ char c;
+
+ /* Tests the bisection logic in json_variant_by_key() */
+
+ for (c = 'z'; c >= 'a'; c--) {
+
+ if ((c % 3) == 0)
+ continue;
+
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ assert_se(json_variant_new_stringn(&w, (char[4]) { '<', c, c, '>' }, 4) >= 0);
+ assert_se(json_variant_set_field(&v, (char[2]) { c, 0 }, w) >= 0);
+ }
+
+ json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ assert_se(!json_variant_is_sorted(v));
+ assert_se(!json_variant_is_normalized(v));
+ assert_se(json_variant_normalize(&v) >= 0);
+ assert_se(json_variant_is_sorted(v));
+ assert_se(json_variant_is_normalized(v));
+
+ json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ for (c = 'a'; c <= 'z'; c++) {
+ JsonVariant *k;
+ const char *z;
+
+ k = json_variant_by_key(v, (char[2]) { c, 0 });
+ assert_se(!k == ((c % 3) == 0));
+
+ if (!k)
+ continue;
+
+ assert_se(json_variant_is_string(k));
+
+ z = (char[5]){ '<', c, c, '>', 0};
+ assert_se(streq(json_variant_string(k), z));
+ }
+}
+
int main(int argc, char *argv[]) {
test_setup_logging(LOG_DEBUG);
@@ -462,10 +549,11 @@ int main(int argc, char *argv[]) {
test_variant("[ 0, -0, 0.0, -0.0, 0.000, -0.000, 0e0, -0e0, 0e+0, -0e-0, 0e-0, -0e000, 0e+000 ]", test_zeroes);
test_build();
-
test_source();
-
test_depth();
+ test_normalize();
+ test_bisect();
+
return 0;
}
diff --git a/src/userdb/meson.build b/src/userdb/meson.build
new file mode 100644
index 0000000000000..2f7e1accbf90d
--- /dev/null
+++ b/src/userdb/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_userwork_sources = files('''
+ userwork.c
+'''.split())
+
+systemd_userdbd_sources = files('''
+ userdbd-manager.c
+ userdbd-manager.h
+ userdbd.c
+'''.split())
+
+userdbctl_sources = files('''
+ userdbctl.c
+'''.split())
diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c
new file mode 100644
index 0000000000000..8b10503f15730
--- /dev/null
+++ b/src/userdb/userdbctl.c
@@ -0,0 +1,753 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "dirent-util.h"
+#include "errno-list.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "group-record-show.h"
+#include "main-func.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "socket-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "user-record-show.h"
+#include "user-util.h"
+#include "userdb.h"
+#include "verbs.h"
+
+static enum {
+ DISPLAY_CLASSIC,
+ DISPLAY_TABLE,
+ DISPLAY_FRIENDLY,
+ DISPLAY_JSON,
+ _DISPLAY_INVALID = -1
+} arg_display = _DISPLAY_INVALID;
+
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static char** arg_services = NULL;
+static UserDBFlags arg_userdb_flags = 0;
+
+STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep);
+
+static int show_user(UserRecord *ur, Table *table) {
+ int r;
+
+ assert(ur);
+
+ switch (arg_display) {
+
+ case DISPLAY_CLASSIC:
+ if (!uid_is_valid(ur->uid))
+ break;
+
+ printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n",
+ ur->user_name,
+ ur->uid,
+ user_record_gid(ur),
+ strempty(user_record_real_name(ur)),
+ user_record_home_directory(ur),
+ user_record_shell(ur));
+
+ break;
+
+ case DISPLAY_JSON:
+ json_variant_dump(ur->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0);
+ break;
+
+ case DISPLAY_FRIENDLY:
+ user_record_show(ur, true);
+
+ if (ur->incomplete) {
+ fflush(stdout);
+ log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
+ }
+
+ break;
+
+ case DISPLAY_TABLE:
+ assert(table);
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, ur->user_name,
+ TABLE_STRING, user_disposition_to_string(user_record_disposition(ur)),
+ TABLE_UID, ur->uid,
+ TABLE_GID, user_record_gid(ur),
+ TABLE_STRING, empty_to_null(ur->real_name),
+ TABLE_STRING, user_record_home_directory(ur),
+ TABLE_STRING, user_record_shell(ur),
+ TABLE_INT, (int) user_record_disposition(ur));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ break;
+
+ default:
+ assert_not_reached("Unexpected display mode");
+ }
+
+ return 0;
+}
+
+static int display_user(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ bool draw_separator = false;
+ int ret = 0, r;
+
+ if (arg_display < 0)
+ arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE;
+
+ if (arg_display == DISPLAY_TABLE) {
+ table = table_new("name", "disposition", "uid", "gid", "realname", "home", "shell", "disposition-numeric");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100);
+ (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
+ (void) table_set_empty_string(table, "-");
+ (void) table_set_sort(table, 7, 2, (size_t) -1);
+ (void) table_set_display(table, 0, 1, 2, 3, 4, 5, 6, (size_t) -1);
+ }
+
+ if (argc > 1) {
+ char **i;
+
+ STRV_FOREACH(i, argv + 1) {
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ uid_t uid;
+
+ if (parse_uid(*i, &uid) >= 0)
+ r = userdb_by_uid(uid, arg_userdb_flags, &ur);
+ else
+ r = userdb_by_name(*i, arg_userdb_flags, &ur);
+ if (r < 0) {
+ if (r == -ESRCH)
+ log_error_errno(r, "User %s does not exist.", *i);
+ else if (r == -EHOSTDOWN)
+ log_error_errno(r, "Selected user database service is not available for this request.");
+ else
+ log_error_errno(r, "Failed to find user %s: %m", *i);
+
+ if (ret >= 0)
+ ret = r;
+ } else {
+ if (draw_separator && arg_display == DISPLAY_FRIENDLY)
+ putchar('\n');
+
+ r = show_user(ur, table);
+ if (r < 0)
+ return r;
+
+ draw_separator = true;
+ }
+ }
+ } else {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ r = userdb_all(arg_userdb_flags, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate users: %m");
+
+ for (;;) {
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+
+ r = userdb_iterator_get(iterator, &ur);
+ if (r == -ESRCH)
+ break;
+ if (r == -EHOSTDOWN)
+ return log_error_errno(r, "Selected user database service is not available for this request.");
+ if (r < 0)
+ return log_error_errno(r, "Failed acquire next user: %m");
+
+ if (draw_separator && arg_display == DISPLAY_FRIENDLY)
+ putchar('\n');
+
+ r = show_user(ur, table);
+ if (r < 0)
+ return r;
+
+ draw_separator = true;
+ }
+ }
+
+ if (table) {
+ r = table_print(table, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show table: %m");
+ }
+
+ return ret;
+}
+
+static int show_group(GroupRecord *gr, Table *table) {
+ int r;
+
+ assert(gr);
+
+ switch (arg_display) {
+
+ case DISPLAY_CLASSIC: {
+ _cleanup_free_ char *m = NULL;
+
+ if (!gid_is_valid(gr->gid))
+ break;
+
+ m = strv_join(gr->members, ",");
+ if (!m)
+ return log_oom();
+
+ printf("%s:x:" GID_FMT ":%s\n",
+ gr->group_name,
+ gr->gid,
+ m);
+ break;
+ }
+
+ case DISPLAY_JSON:
+ json_variant_dump(gr->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0);
+ break;
+
+ case DISPLAY_FRIENDLY:
+ group_record_show(gr, true);
+
+ if (gr->incomplete) {
+ fflush(stdout);
+ log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name);
+ }
+
+ break;
+
+ case DISPLAY_TABLE:
+ assert(table);
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, gr->group_name,
+ TABLE_STRING, user_disposition_to_string(group_record_disposition(gr)),
+ TABLE_GID, gr->gid,
+ TABLE_INT, (int) group_record_disposition(gr));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ break;
+
+ default:
+ assert_not_reached("Unexpected disply mode");
+ }
+
+ return 0;
+}
+
+
+static int display_group(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ bool draw_separator = false;
+ int ret = 0, r;
+
+ if (arg_display < 0)
+ arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE;
+
+ if (arg_display == DISPLAY_TABLE) {
+ table = table_new("name", "disposition", "gid", "disposition-numeric");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100);
+ (void) table_set_sort(table, 3, 2, (size_t) -1);
+ (void) table_set_display(table, 0, 1, 2, (size_t) -1);
+ }
+
+ if (argc > 1) {
+ char **i;
+
+ STRV_FOREACH(i, argv + 1) {
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+ gid_t gid;
+
+ if (parse_gid(*i, &gid) >= 0)
+ r = groupdb_by_gid(gid, arg_userdb_flags, &gr);
+ else
+ r = groupdb_by_name(*i, arg_userdb_flags, &gr);
+ if (r < 0) {
+ if (r == -ESRCH)
+ log_error_errno(r, "Group %s does not exist.", *i);
+ else if (r == -EHOSTDOWN)
+ log_error_errno(r, "Selected group database service is not available for this request.");
+ else
+ log_error_errno(r, "Failed to find group %s: %m", *i);
+
+ if (ret >= 0)
+ ret = r;
+ } else {
+ if (draw_separator && arg_display == DISPLAY_FRIENDLY)
+ putchar('\n');
+
+ r = show_group(gr, table);
+ if (r < 0)
+ return r;
+
+ draw_separator = true;
+ }
+ }
+
+ } else {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ r = groupdb_all(arg_userdb_flags, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate groups: %m");
+
+ for (;;) {
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+
+ r = groupdb_iterator_get(iterator, &gr);
+ if (r == -ESRCH)
+ break;
+ if (r == -EHOSTDOWN)
+ return log_error_errno(r, "Selected group database service is not available for this request.");
+ if (r < 0)
+ return log_error_errno(r, "Failed acquire next group: %m");
+
+ if (draw_separator && arg_display == DISPLAY_FRIENDLY)
+ putchar('\n');
+
+ r = show_group(gr, table);
+ if (r < 0)
+ return r;
+
+ draw_separator = true;
+ }
+
+ }
+
+ if (table) {
+ r = table_print(table, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show table: %m");
+ }
+
+ return ret;
+}
+
+static int show_membership(const char *user, const char *group, Table *table) {
+ int r;
+
+ assert(user);
+ assert(group);
+
+ switch (arg_display) {
+
+ case DISPLAY_CLASSIC:
+ /* Strictly speaking there's no 'classic' output for this concept, but let's output it in
+ * similar style to the classic display for user/group info */
+
+ printf("%s:%s\n", user, group);
+ break;
+
+ case DISPLAY_JSON: {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ r = json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("user", JSON_BUILD_STRING(user)),
+ JSON_BUILD_PAIR("group", JSON_BUILD_STRING(group))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build JSON object: %m");
+
+ json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
+ break;
+ }
+
+ case DISPLAY_FRIENDLY:
+ /* Hmm, this is not particularly friendly, but not sure how we could do this better */
+ printf("%s: %s\n", group, user);
+ break;
+
+ case DISPLAY_TABLE:
+ assert(table);
+
+ r = table_add_many(
+ table,
+ TABLE_STRING, user,
+ TABLE_STRING, group);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ break;
+
+ default:
+ assert_not_reached("Unexpected display mode");
+ }
+
+
+ return 0;
+}
+
+static int display_memberships(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int ret = 0, r;
+
+ if (arg_display < 0)
+ arg_display = DISPLAY_TABLE;
+
+ if (arg_display == DISPLAY_TABLE) {
+ table = table_new("user", "group");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_sort(table, 0, 1, (size_t) -1);
+ }
+
+ if (argc > 1) {
+ char **i;
+
+ STRV_FOREACH(i, argv + 1) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ if (streq(argv[0], "users-in-group")) {
+ r = membershipdb_by_group(*i, arg_userdb_flags, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate users in group: %m");
+ } else if (streq(argv[0], "groups-of-user")) {
+ r = membershipdb_by_user(*i, arg_userdb_flags, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate groups of user: %m");
+ } else
+ assert_not_reached("Unexpected verb");
+
+ for (;;) {
+ _cleanup_free_ char *user = NULL, *group = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user, &group);
+ if (r == -ESRCH)
+ break;
+ if (r == -EHOSTDOWN)
+ return log_error_errno(r, "Selected membership database service is not available for this request.");
+ if (r < 0)
+ return log_error_errno(r, "Failed acquire next membership: %m");
+
+ r = show_membership(user, group, table);
+ if (r < 0)
+ return r;
+ }
+ }
+ } else {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ r = membershipdb_all(arg_userdb_flags, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate memberships: %m");
+
+ for (;;) {
+ _cleanup_free_ char *user = NULL, *group = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user, &group);
+ if (r == -ESRCH)
+ break;
+ if (r == -EHOSTDOWN)
+ return log_error_errno(r, "Selected membership database service is not available for this request.");
+ if (r < 0)
+ return log_error_errno(r, "Failed acquire next membership: %m");
+
+ r = show_membership(user, group, table);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (table) {
+ r = table_print(table, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show table: %m");
+ }
+
+ return ret;
+}
+
+static int display_services(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ struct dirent *de;
+ int r;
+
+ d = opendir("/run/systemd/userdb/");
+ if (!d) {
+ if (errno == ENOENT) {
+ log_info("No services.");
+ return 0;
+ }
+
+ return log_error_errno(errno, "Failed to open /run/systemd/userdb/: %m");
+ }
+
+ t = table_new("service", "connectible");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_sort(t, 0, (size_t) -1);
+
+ FOREACH_DIRENT(de, d, return -errno) {
+ _cleanup_free_ char *j = NULL, *no = NULL;
+ union sockaddr_union sockaddr;
+ _cleanup_close_ int fd = -1;
+
+ j = path_join("/run/systemd/userdb/", de->d_name);
+ if (!j)
+ return log_oom();
+
+ r = sockaddr_un_set_path(&sockaddr.un, j);
+ if (r < 0)
+ return log_error_errno(r, "Path %s does not fit in AF_UNIX socket address: %m", j);
+
+ fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ return log_error_errno(r, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
+
+ if (connect(fd, &sockaddr.un, SOCKADDR_UN_LEN(sockaddr.un)) < 0) {
+ no = strjoin("No (", errno_to_name(errno), ")");
+ if (!no)
+ return log_oom();
+ }
+
+ r = table_add_many(t,
+ TABLE_STRING, de->d_name,
+ TABLE_STRING, no ?: "yes",
+ TABLE_SET_COLOR, no ? ansi_highlight_red() : ansi_highlight_green());
+ if (r < 0)
+ return log_error_errno(r, "Failed to add table row: %m");
+ }
+
+ if (table_get_rows(t) <= 0) {
+ log_info("No services.");
+ return 0;
+ }
+
+ if (arg_display == DISPLAY_JSON)
+ table_print_json(t, NULL, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO);
+ else
+ table_print(t, NULL);
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ (void) pager_open(arg_pager_flags);
+
+ r = terminal_urlify_man("userdbctl", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Show user and group information.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --display=MODE Select display mode (classic, friendly, table, json)\n"
+ " -j Equivalent to --display=json\n"
+ " -s --service=SERVICE[:SERVICE…]\n"
+ " Query on specified service\n"
+ " --with-nss=BOOL Control whether to include glibc NSS data\n"
+ " -N Disable inclusion of glibc NSS data and disable synthesizing\n"
+ " (Same as --with-nss=no --synthesize=no)\n"
+ " --synthesize=BOOL Synthesize root/nobody user\n"
+ "\nCommands:\n"
+ " user [USER…] Inspect user\n"
+ " group [GROUP…] Inspect group\n"
+ " users-in-group [GROUP…] Show users that are members of specified group(s)\n"
+ " groups-of-user [USER…] Show groups the specified user(s) is a member of\n"
+ " services Show enabled database services\n"
+ "\nSee the %s for details.\n"
+ , program_invocation_short_name
+ , link
+ );
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_DISPLAY,
+ ARG_WITH_NSS,
+ ARG_SYNTHESIZE,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ { "display", required_argument, NULL, ARG_DISPLAY },
+ { "service", required_argument, NULL, 's' },
+ { "with-nss", required_argument, NULL, ARG_WITH_NSS },
+ { "synthesize", required_argument, NULL, ARG_SYNTHESIZE },
+ {}
+ };
+
+ const char *e;
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ /* We are going to update this environment variable with our own, hence let's first read what is already set */
+ e = getenv("SYSTEMD_ONLY_USERDB");
+ if (e) {
+ char **l;
+
+ l = strv_split(e, ":");
+ if (!l)
+ return log_oom();
+
+ strv_free(arg_services);
+ arg_services = l;
+ }
+
+ for (;;) {
+ int c;
+
+ c = getopt_long(argc, argv, "hjs:N", options, NULL);
+ if (c < 0)
+ break;
+
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case ARG_DISPLAY:
+ if (streq(optarg, "classic"))
+ arg_display = DISPLAY_CLASSIC;
+ else if (streq(optarg, "friendly"))
+ arg_display = DISPLAY_FRIENDLY;
+ else if (streq(optarg, "json"))
+ arg_display = DISPLAY_JSON;
+ else if (streq(optarg, "table"))
+ arg_display = DISPLAY_TABLE;
+ else if (streq(optarg, "help")) {
+ puts("classic\n"
+ "friendly\n"
+ "json");
+ return 0;
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --display= mode: %s", optarg);
+
+ break;
+
+ case 'j':
+ arg_display = DISPLAY_JSON;
+ break;
+
+ case 's':
+ if (isempty(optarg))
+ arg_services = strv_free(arg_services);
+ else {
+ char **l;
+
+ l = strv_split(optarg, ":");
+ if (!l)
+ return log_oom();
+
+ r = strv_extend_strv(&arg_services, l, true);
+ if (r < 0)
+ return log_oom();
+ }
+
+ break;
+
+ case 'N':
+ arg_userdb_flags |= USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE;
+ break;
+
+ case ARG_WITH_NSS:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --with-nss= parameter: %s", optarg);
+
+ SET_FLAG(arg_userdb_flags, USERDB_AVOID_NSS, !r);
+ break;
+
+ case ARG_SYNTHESIZE:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --synthesize= parameter: %s", optarg);
+
+ SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r);
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+ }
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "user", VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user },
+ { "group", VERB_ANY, VERB_ANY, 0, display_group },
+ { "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships },
+ { "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships },
+ { "services", VERB_ANY, 1, 0, display_services },
+ {}
+ };
+
+ int r;
+
+ log_show_color(true);
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (arg_services) {
+ _cleanup_free_ char *e = NULL;
+
+ e = strv_join(arg_services, ":");
+ if (!e)
+ return log_oom();
+
+ if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0)
+ return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m");
+
+ log_info("Enabled services: %s", e);
+ } else {
+ if (unsetenv("SYSTEMD_ONLY_USERDB") < 0)
+ return log_error_errno(r, "Failed to unset $SYSTEMD_ONLY_USERDB: %m");
+ }
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/userdb/userdbd-manager.c b/src/userdb/userdbd-manager.c
new file mode 100644
index 0000000000000..a5f6b767238ee
--- /dev/null
+++ b/src/userdb/userdbd-manager.c
@@ -0,0 +1,301 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "sd-daemon.h"
+
+#include "fd-util.h"
+#include "mkdir.h"
+#include "process-util.h"
+#include "set.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "stdio-util.h"
+#include "umask-util.h"
+#include "userdbd-manager.h"
+#include "fs-util.h"
+
+#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC)
+
+static int start_workers(Manager *m, bool explicit_request);
+
+static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = userdata;
+
+ assert(s);
+ assert(m);
+
+ for (;;) {
+ siginfo_t siginfo = {};
+ bool removed = false;
+
+ if (waitid(P_ALL, 0, &siginfo, WNOHANG|WEXITED) < 0) {
+ if (errno == ECHILD)
+ break;
+
+ log_warning_errno(errno, "Failed to invoke waitid(): %m");
+ break;
+ }
+ if (siginfo.si_pid == 0)
+ break;
+
+ if (set_remove(m->workers_dynamic, PID_TO_PTR(siginfo.si_pid)))
+ removed = true;
+ if (set_remove(m->workers_fixed, PID_TO_PTR(siginfo.si_pid)))
+ removed = true;
+
+ if (!removed) {
+ log_warning("Weird, got SIGCHLD for unknown child " PID_FMT ", ignoring.", siginfo.si_pid);
+ continue;
+ }
+
+ if (siginfo.si_code == CLD_EXITED) {
+ if (siginfo.si_status == EXIT_SUCCESS)
+ log_debug("Worker " PID_FMT " exited successfully.", siginfo.si_pid);
+ else
+ log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", siginfo.si_pid, siginfo.si_status);
+ } else if (siginfo.si_code == CLD_KILLED)
+ log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status));
+ else if (siginfo.si_code == CLD_DUMPED)
+ log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status));
+ else
+ log_warning("Can't handle SIGCHLD of this type");
+ }
+
+ (void) start_workers(m, false); /* Fill up workers again if we fell below the low watermark */
+ return 0;
+}
+
+static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = userdata;
+
+ assert(s);
+ assert(m);
+
+ (void) start_workers(m, true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */
+ return 0;
+}
+
+int manager_new(Manager **ret) {
+ Manager *m;
+ int r;
+
+ m = new(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ *m = (Manager) {
+ .listen_fd = -1,
+ };
+
+ r = sd_event_new(&m->event);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_set_watchdog(m->event, true);
+
+ m->workers_fixed = set_new(NULL);
+ m->workers_dynamic = set_new(NULL);
+
+ if (!m->workers_fixed || !m->workers_dynamic)
+ return -ENOMEM;
+
+ r = sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, on_sigusr2, m);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, &m->sigchld_event_source, SIGCHLD, on_sigchld, m);
+ if (r < 0)
+ return r;
+
+ RATELIMIT_INIT(m->worker_ratelimit, 5 * USEC_PER_SEC, 50);
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+Manager* manager_free(Manager *m) {
+ if (!m)
+ return NULL;
+
+ set_free(m->workers_fixed);
+ set_free(m->workers_dynamic);
+
+ sd_event_source_disable_unref(m->sigusr2_event_source);
+ sd_event_source_disable_unref(m->sigchld_event_source);
+
+ sd_event_unref(m->event);
+
+ return mfree(m);
+}
+
+static size_t manager_current_workers(Manager *m) {
+ assert(m);
+
+ return set_size(m->workers_fixed) + set_size(m->workers_dynamic);
+}
+
+static int start_one_worker(Manager *m) {
+ bool fixed;
+ pid_t pid;
+ int r;
+
+ assert(m);
+
+ fixed = set_size(m->workers_fixed) < USERDB_WORKERS_MIN;
+
+ r = safe_fork("(sd-worker)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to fork new worker child: %m");
+ if (r == 0) {
+ char pids[DECIMAL_STR_MAX(pid_t)];
+ /* Child */
+
+ log_close();
+
+ r = close_all_fds(&m->listen_fd, 1);
+ if (r < 0) {
+ log_error_errno(r, "Failed to close fds in child: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ log_open();
+
+ if (m->listen_fd == 3) {
+ r = fd_cloexec(3, false);
+ if (r < 0) {
+ log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m");
+ _exit(EXIT_FAILURE);
+ }
+ } else {
+ if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */
+ log_error_errno(errno, "Failed to move listen fd to 3: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ safe_close(m->listen_fd);
+ }
+
+ xsprintf(pids, PID_FMT, pid);
+ if (setenv("LISTEN_PID", pids, 1) < 0) {
+ log_error_errno(errno, "Failed to set $LISTEN_PID: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (setenv("LISTEN_FDS", "1", 1) < 0) {
+ log_error_errno(errno, "Failed to set $LISTEN_FDS: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+
+ if (setenv("USERDB_FIXED_WORKER", one_zero(fixed), 1) < 0) {
+ log_error_errno(errno, "Failed to set $USERDB_FIXED_WORKER: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ // FIXME drop this line
+ execl("/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */
+ //execl("/usr/bin/valgrind", "valgrind", "/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */
+
+ execl(SYSTEMD_USERWORK_PATH, "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */
+ log_error_errno(errno, "Failed start worker process: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ if (fixed)
+ r = set_put(m->workers_fixed, PID_TO_PTR(pid));
+ else
+ r = set_put(m->workers_dynamic, PID_TO_PTR(pid));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add child process to set: %m");
+
+ return 0;
+}
+
+static int start_workers(Manager *m, bool explicit_request) {
+ int r;
+
+ assert(m);
+
+ for (;;) {
+ size_t n;
+
+ n = manager_current_workers(m);
+ if (n >= USERDB_WORKERS_MIN && (!explicit_request || n >= USERDB_WORKERS_MAX))
+ break;
+
+ if (!ratelimit_below(&m->worker_ratelimit)) {
+ /* If we keep starting workers too often, let's fail the whole daemon, something is wrong */
+ sd_event_exit(m->event, EXIT_FAILURE);
+
+ return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong.");
+ }
+
+ r = start_one_worker(m);
+ if (r < 0)
+ return r;
+
+ explicit_request = false;
+ }
+
+ return 0;
+}
+
+int manager_startup(Manager *m) {
+ struct timeval ts;
+ int n, r;
+
+ assert(m);
+ assert(m->listen_fd < 0);
+
+ n = sd_listen_fds(false);
+ if (n < 0)
+ return log_error_errno(n, "Failed to determine number of passed file descriptors: %m");
+ if (n > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n);
+ if (n == 1)
+ m->listen_fd = SD_LISTEN_FDS_START;
+ else {
+ union sockaddr_union sockaddr;
+
+ r = sockaddr_un_set_path(&sockaddr.un, "/run/systemd/userdb/io.systemd.NameServiceSwitch");
+ if (r < 0)
+ return log_error_errno(r, "Cannot assign socket path to socket address: %m");
+
+ r = mkdir_p("/run/systemd/userdb", 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create /run/systemd/userdb: %m");
+
+ m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+ if (m->listen_fd < 0)
+ return log_error_errno(errno, "Failed to bind on socket: %m");
+
+ (void) sockaddr_un_unlink(&sockaddr.un);
+
+ RUN_WITH_UMASK(0000)
+ if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0)
+ return log_error_errno(errno, "Failed to bind socket: %m");
+
+ r = symlink_idempotent("io.systemd.NameServiceSwitch", "/run/systemd/userdb/io.systemd.Multiplexer", false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind io.systemd.Multiplexer: %m");
+
+ if (listen(m->listen_fd, SOMAXCONN) < 0)
+ return log_error_errno(errno, "Failed to listen on socket: %m");
+ }
+
+ /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be
+ * GC'ed on idle */
+ if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, timeval_store(&ts, LISTEN_TIMEOUT_USEC), sizeof(ts)) < 0)
+ return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m");
+
+ return start_workers(m, false);
+}
diff --git a/src/userdb/userdbd-manager.h b/src/userdb/userdbd-manager.h
new file mode 100644
index 0000000000000..0bf67fe55b443
--- /dev/null
+++ b/src/userdb/userdbd-manager.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+#include "sd-event.h"
+
+typedef struct Manager Manager;
+
+#include "hashmap.h"
+#include "varlink.h"
+#include "ratelimit.h"
+
+#define USERDB_WORKERS_MIN 3
+#define USERDB_WORKERS_MAX 4096
+
+struct Manager {
+ sd_event *event;
+
+ Set *workers_fixed; /* Workers 0…USERDB_WORKERS_MIN */
+ Set *workers_dynamic; /* Workers USERD_WORKERS_MIN+1…USERDB_WORKERS_MAX */
+
+ sd_event_source *sigusr2_event_source;
+ sd_event_source *sigchld_event_source;
+
+ int listen_fd;
+
+ RateLimit worker_ratelimit;
+};
+
+int manager_new(Manager **ret);
+Manager* manager_free(Manager *m);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+int manager_startup(Manager *m);
diff --git a/src/userdb/userdbd.c b/src/userdb/userdbd.c
new file mode 100644
index 0000000000000..c7cf0e9485aeb
--- /dev/null
+++ b/src/userdb/userdbd.c
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "daemon-util.h"
+#include "userdbd-manager.h"
+#include "log.h"
+#include "main-func.h"
+#include "signal-util.h"
+
+/* This service offers two Varlink services, both implementing io.systemd.UserDatabase:
+ *
+ * → io.systemd.NameServiceSwitch: this is a compatibility interface for glibc NSS: it response to
+ * name lookups by checking the classic NSS interfaces and responding that.
+ *
+ * → io.systemd.Multiplexer: this multiplexes lookup requests to all Varlink services that have a
+ * socket in /run/systemd/userdb/. It's supposed to simplify clients that don't want to implement
+ * the full iterative logic on their own.
+ */
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ log_setup_service();
+
+ // FIXME
+ log_set_max_level(LOG_DEBUG);
+
+ umask(0022);
+
+ if (argc != 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NameServiceSwitch:io.systemd.Multiplexer", 1) < 0)
+ return log_error_errno(errno, "Failed to se $SYSTEMD_BYPASS_USERDB: %m");
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGUSR2, -1) >= 0);
+
+ r = manager_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Could not create manager: %m");
+
+ r = manager_startup(m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start up daemon: %m");
+
+ notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
+
+ r = sd_event_loop(m->event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c
new file mode 100644
index 0000000000000..8b9fbb5974640
--- /dev/null
+++ b/src/userdb/userwork.c
@@ -0,0 +1,785 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "sd-daemon.h"
+
+#include "env-util.h"
+#include "fd-util.h"
+#include "group-record-nss.h"
+#include "group-record.h"
+#include "main-func.h"
+#include "process-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "user-record-nss.h"
+#include "user-record.h"
+#include "user-util.h"
+#include "userdb.h"
+#include "varlink.h"
+
+#define ITERATIONS_MAX 64U
+#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE)
+#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC)
+#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC)
+#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC)
+
+typedef struct LookupParameters {
+ const char *user_name;
+ const char *group_name;
+ union {
+ uid_t uid;
+ gid_t gid;
+ };
+ const char *service;
+} LookupParameters;
+
+static int add_nss_service(JsonVariant **v) {
+ _cleanup_(json_variant_unrefp) JsonVariant *status = NULL, *z = NULL;
+ char buf[SD_ID128_STRING_MAX];
+ sd_id128_t mid;
+ int r;
+
+ assert(v);
+
+ /* Patch in service field if it's missing. The assumption here is that this field is unset only for
+ * NSS records */
+
+ if (json_variant_by_key(*v, "service"))
+ return 0;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return r;
+
+ status = json_variant_ref(json_variant_by_key(*v, "status"));
+ z = json_variant_ref(json_variant_by_key(status, sd_id128_to_string(mid, buf)));
+
+ if (json_variant_by_key(z, "service"))
+ return 0;
+
+ r = json_variant_set_field_string(&z, "service", "io.systemd.NameServiceSwitch");
+ if (r < 0)
+ return r;
+
+ r = json_variant_set_field(&status, buf, z);
+ if (r < 0)
+ return r;
+
+ return json_variant_set_field(v, "status", status);
+}
+
+static int build_user_json(Varlink *link, UserRecord *ur, JsonVariant **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *stripped = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ UserRecordLoadFlags flags;
+ uid_t peer_uid;
+ bool trusted;
+ int r;
+
+ assert(ur);
+ assert(ret);
+
+ r = varlink_get_peer_uid(link, &peer_uid);
+ if (r < 0) {
+ log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
+ trusted = false;
+ } else
+ trusted = peer_uid == 0 || peer_uid == ur->uid;
+
+ flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+ if (trusted)
+ flags |= USER_RECORD_ALLOW_PRIVILEGED;
+ else
+ flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+ r = user_record_clone(ur, flags, &stripped);
+ if (r < 0)
+ return r;
+
+ stripped->incomplete =
+ ur->incomplete ||
+ (FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED) &&
+ !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED));
+
+ v = json_variant_ref(stripped->json);
+ r = add_nss_service(&v);
+ if (r < 0)
+ return r;
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)),
+ JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete))));
+}
+
+static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 },
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ LookupParameters p = {
+ .uid = UID_INVALID,
+ };
+ int r;
+
+ assert(parameters);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
+ if (uid_is_valid(p.uid))
+ r = nss_user_record_by_uid(p.uid, &hr);
+ else if (p.user_name)
+ r = nss_user_record_by_name(p.user_name, &hr);
+ else {
+ _cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
+
+ setpwent();
+
+ for (;;) {
+ _cleanup_(user_record_unrefp) UserRecord *z = NULL;
+ _cleanup_free_ char *sbuf = NULL;
+ struct passwd *pw;
+ struct spwd spwd;
+
+ errno = 0;
+ pw = getpwent();
+ if (!pw) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failure while iterating through NSS user database, ignoring: %m");
+
+ break;
+ }
+
+ r = nss_spwd_for_passwd(pw, &spwd, &sbuf);
+ if (r < 0)
+ log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name);
+
+ r = nss_passwd_to_user_record(pw, NULL, &z);
+ if (r < 0) {
+ endpwent();
+ return r;
+ }
+
+ if (last) {
+ r = varlink_notify(link, last);
+ if (r < 0) {
+ endpwent();
+ return r;
+ }
+
+ last = json_variant_unref(last);
+ }
+
+ r = build_user_json(link, z, &last);
+ if (r < 0) {
+ endpwent();
+ return r;
+ }
+ }
+
+ endpwent();
+
+ if (!last)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, last);
+ }
+
+ } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
+
+ if (uid_is_valid(p.uid))
+ r = userdb_by_uid(p.uid, USERDB_AVOID_MULTIPLEXER, &hr);
+ else if (p.user_name)
+ r = userdb_by_name(p.user_name, USERDB_AVOID_MULTIPLEXER, &hr);
+ else {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
+
+ r = userdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_(user_record_unrefp) UserRecord *z = NULL;
+
+ r = userdb_iterator_get(iterator, &z);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return r;
+
+ if (last) {
+ r = varlink_notify(link, last);
+ if (r < 0)
+ return r;
+
+ last = json_variant_unref(last);
+ }
+
+ r = build_user_json(link, z, &last);
+ if (r < 0)
+ return r;
+ }
+
+ if (!last)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, last);
+ }
+ } else
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+ if (r == -ESRCH)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ if (r < 0) {
+ log_debug_errno(r, "User lookup failed abnormally: %m");
+ return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL);
+ }
+
+ if ((uid_is_valid(p.uid) && hr->uid != p.uid) ||
+ (p.user_name && !streq(hr->user_name, p.user_name)))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_user_json(link, hr, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+static int build_group_json(Varlink *link, GroupRecord *gr, JsonVariant **ret) {
+ _cleanup_(group_record_unrefp) GroupRecord *stripped = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ UserRecordLoadFlags flags;
+ uid_t peer_uid;
+ bool trusted;
+ int r;
+
+ assert(gr);
+ assert(ret);
+
+ r = varlink_get_peer_uid(link, &peer_uid);
+ if (r < 0) {
+ log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
+ trusted = false;
+ } else
+ trusted = peer_uid == 0;
+
+ flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
+ if (trusted)
+ flags |= USER_RECORD_ALLOW_PRIVILEGED;
+ else
+ flags |= USER_RECORD_STRIP_PRIVILEGED;
+
+ r = group_record_clone(gr, flags, &stripped);
+ if (r < 0)
+ return r;
+
+ stripped->incomplete =
+ gr->incomplete ||
+ (FLAGS_SET(gr->mask, USER_RECORD_PRIVILEGED) &&
+ !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED));
+
+ v = json_variant_ref(gr->json);
+ r = add_nss_service(&v);
+ if (r < 0)
+ return r;
+
+ return json_build(ret, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)),
+ JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete))));
+}
+
+static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ static const JsonDispatch dispatch_table[] = {
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ LookupParameters p = {
+ .gid = GID_INVALID,
+ };
+ int r;
+
+ assert(parameters);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
+
+ if (gid_is_valid(p.gid))
+ r = nss_group_record_by_gid(p.gid, &g);
+ else if (p.group_name)
+ r = nss_group_record_by_name(p.group_name, &g);
+ else {
+ _cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
+
+ setgrent();
+
+ for (;;) {
+ _cleanup_(group_record_unrefp) GroupRecord *z = NULL;
+ _cleanup_free_ char *sbuf = NULL;
+ struct group *grp;
+ struct sgrp sgrp;
+
+ errno = 0;
+ grp = getgrent();
+ if (!grp) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m");
+
+ break;
+ }
+
+ r = nss_sgrp_for_group(grp, &sgrp, &sbuf);
+ if (r < 0)
+ log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", grp->gr_name);
+
+ r = nss_group_to_group_record(grp, r >= 0 ? &sgrp : NULL, &z);
+ if (r < 0) {
+ endgrent();
+ return r;
+ }
+
+ if (last) {
+ r = varlink_notify(link, last);
+ if (r < 0) {
+ endgrent();
+ return r;
+ }
+
+ last = json_variant_unref(last);
+ }
+
+ r = build_group_json(link, z, &last);
+ if (r < 0) {
+ endgrent();
+ return r;
+ }
+ }
+
+ endgrent();
+
+ if (!last)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, last);
+ }
+
+ } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
+
+ if (gid_is_valid(p.gid))
+ r = groupdb_by_gid(p.gid, USERDB_AVOID_MULTIPLEXER, &g);
+ else if (p.group_name)
+ r = groupdb_by_name(p.group_name, USERDB_AVOID_MULTIPLEXER, &g);
+ else {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
+
+ r = groupdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_(group_record_unrefp) GroupRecord *z = NULL;
+
+ r = groupdb_iterator_get(iterator, &z);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return r;
+
+ if (last) {
+ r = varlink_notify(link, last);
+ if (r < 0)
+ return r;
+
+ last = json_variant_unref(last);
+ }
+
+ r = build_group_json(link, z, &last);
+ if (r < 0)
+ return r;
+ }
+
+ if (!last)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_reply(link, last);
+ }
+ } else
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+ if (r == -ESRCH)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ if (r < 0) {
+ log_debug_errno(r, "Group lookup failed abnormally: %m");
+ return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL);
+ }
+
+ if ((uid_is_valid(p.gid) && g->gid != p.gid) ||
+ (p.group_name && !streq(g->group_name, p.group_name)))
+ return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
+
+ r = build_group_json(link, g, &v);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, v);
+}
+
+static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ static const JsonDispatch dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
+ {}
+ };
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ LookupParameters p = {};
+ int r;
+
+ assert(parameters);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+ if (r < 0)
+ return r;
+
+ if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
+
+ if (p.group_name) {
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ const char *last = NULL;
+ char **i;
+
+ r = nss_group_record_by_name(p.group_name, &g);
+ if (r == -ESRCH)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(i, g->members) {
+
+ if (p.user_name && !streq_ptr(p.user_name, *i))
+ continue;
+
+ if (last) {
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name))));
+ if (r < 0)
+ return r;
+ }
+
+ last = *i;
+ }
+
+ if (!last)
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name))));
+ } else {
+ _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL;
+
+ setgrent();
+
+ for (;;) {
+ struct group *grp;
+ const char* two[2], **users, **i;
+
+ errno = 0;
+ grp = getgrent();
+ if (!grp) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m");
+
+ break;
+ }
+
+ if (p.user_name) {
+ if (!strv_contains(grp->gr_mem, p.user_name))
+ continue;
+
+ two[0] = p.user_name;
+ two[1] = NULL;
+
+ users = two;
+ } else
+ users = (const char**) grp->gr_mem;
+
+ STRV_FOREACH(i, users) {
+
+ if (last_user_name) {
+ assert(last_group_name);
+
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+ if (r < 0) {
+ endgrent();
+ return r;
+ }
+
+ free(last_user_name);
+ free(last_group_name);
+ }
+
+ last_user_name = strdup(*i);
+ last_group_name = strdup(grp->gr_name);
+ if (!last_user_name || !last_group_name) {
+ endgrent();
+ return -ENOMEM;
+ }
+ }
+ }
+
+ endgrent();
+
+ if (!last_user_name) {
+ assert(!last_group_name);
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ }
+
+ assert(last_group_name);
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+ }
+
+ } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
+
+ _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL;
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+
+ if (p.group_name)
+ r = membershipdb_by_group(p.group_name, USERDB_AVOID_MULTIPLEXER, &iterator);
+ else if (p.user_name)
+ r = membershipdb_by_user(p.user_name, USERDB_AVOID_MULTIPLEXER, &iterator);
+ else
+ r = membershipdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_free_ char *user_name = NULL, *group_name = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user_name, &group_name);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return r;
+
+ /* If both group + user are specified do a-posteriori filtering */
+ if (p.group_name && p.user_name && !streq(group_name, p.group_name))
+ continue;
+
+ if (last_user_name) {
+ assert(last_group_name);
+
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+ if (r < 0)
+ return r;
+
+ free(last_user_name);
+ free(last_group_name);
+ }
+
+ last_user_name = TAKE_PTR(user_name);
+ last_group_name = TAKE_PTR(group_name);
+ }
+
+ if (!last_user_name) {
+ assert(!last_group_name);
+ return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
+ }
+
+ assert(last_group_name);
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
+ }
+
+ return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
+}
+
+static int process_connection(VarlinkServer *server, int fd) {
+ _cleanup_(varlink_close_unrefp) Varlink *vl = NULL;
+ int r;
+
+ r = varlink_server_add_connection(server, fd, &vl);
+ if (r < 0) {
+ fd = safe_close(fd);
+ return log_error_errno(r, "Failed to add connection: %m");
+ }
+
+ vl = varlink_ref(vl);
+
+ for (;;) {
+ r = varlink_process(vl);
+ if (r == -ENOTCONN) {
+ log_debug("Connection terminated.");
+ break;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to process connection: %m");
+ if (r > 0)
+ continue;
+
+ r = varlink_wait(vl, CONNECTION_IDLE_USEC);
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait for connection events: %m");
+ if (r == 0)
+ break;
+ }
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY;
+ _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL;
+ _cleanup_close_ int lock = -1;
+ unsigned n_iterations = 0;
+ int m, listen_fd, r;
+
+ log_setup_service();
+
+ // FIXME
+ log_set_max_level(LOG_DEBUG);
+
+ m = sd_listen_fds(false);
+ if (m < 0)
+ return log_error_errno(m, "Failed to determine number of listening fds: %m");
+ if (m == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received.");
+ if (m > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time.");
+
+ listen_fd = SD_LISTEN_FDS_START;
+
+ r = fd_nonblock(listen_fd, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m");
+
+ r = varlink_server_new(&server, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate server: %m");
+
+ r = varlink_server_bind_method_many(
+ server,
+ "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record,
+ "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
+ "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind methods: %m");
+
+ r = getenv_bool("USERDB_FIXED_WORKER");
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse USERDB_FIXED_WORKER: %m");
+ listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC;
+
+ lock = userdb_nss_compat_disable();
+ if (lock < 0)
+ return log_error_errno(r, "Failed to disable userdb NSS compatibility: %m");
+
+ start_time = now(CLOCK_MONOTONIC);
+
+ for (;;) {
+ _cleanup_close_ int fd = -1;
+ usec_t n;
+
+ /* Exit the worker in regular intervals, to flush out all memory use */
+ if (n_iterations++ > ITERATIONS_MAX) {
+ log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations);
+ break;
+ }
+
+ n = now(CLOCK_MONOTONIC);
+ if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ log_debug("Exiting worker, ran for %s, that's enough.", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, start_time), 0));
+ break;
+ }
+
+ if (last_busy_usec == USEC_INFINITY)
+ last_busy_usec = n;
+ else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) {
+ char buf[FORMAT_TIMESPAN_MAX];
+ log_debug("Exiting worker, been idle for %s, .", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, last_busy_usec), 0));
+ break;
+ }
+
+ (void) rename_process("systemd-userwork: waiting...");
+
+ fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC);
+
+ (void) rename_process("systemd-userwork: processing...");
+
+ if (fd < 0) {
+ if (errno == EAGAIN)
+ continue; /* The listening socket as SO_RECVTIMEO set, hence a time-out is
+ * expected after a while, let's check if it's time to exit
+ * though. */
+
+ if (errno == EINTR)
+ continue; /* Might be that somebody attached via strace, let's just continue
+ * in that case */
+
+ return log_error_errno(errno, "Failed to accept() from listening socket: %m");
+ }
+
+ if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) {
+ struct pollfd pfd = {
+ .fd = listen_fd,
+ .events = POLLIN,
+ };
+
+ /* We only slept a very short time? If so, let's see if there are more sockets
+ * pending, and if so, let's ask our parent for more workers */
+
+ if (poll(&pfd, 1, 0) < 0)
+ return log_error_errno(errno, "Failed to test for POLLIN on listening socket: %m");
+
+ if (FLAGS_SET(pfd.revents, POLLIN)) {
+ pid_t parent;
+
+ parent = getppid();
+ if (parent <= 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parent already died?");
+
+ if (kill(parent, SIGUSR1) < 0)
+ return log_error_errno(errno, "Failed to kill our own parent.");
+ }
+ }
+
+ (void) process_connection(server, TAKE_FD(fd));
+ last_busy_usec = USEC_INFINITY;
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/test/TEST-36-HOMED/Makefile b/test/TEST-36-HOMED/Makefile
new file mode 120000
index 0000000000000..e9f93b1104cd2
--- /dev/null
+++ b/test/TEST-36-HOMED/Makefile
@@ -0,0 +1 @@
+../TEST-01-BASIC/Makefile
\ No newline at end of file
diff --git a/test/TEST-36-HOMED/test.sh b/test/TEST-36-HOMED/test.sh
new file mode 100755
index 0000000000000..8fee9e585616f
--- /dev/null
+++ b/test/TEST-36-HOMED/test.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+set -e
+TEST_DESCRIPTION="testing homed"
+TEST_NO_QEMU=1
+
+. $TEST_BASE_DIR/test-functions
+
+test_setup() {
+ create_empty_image
+ mkdir -p $TESTDIR/root
+ mount ${LOOPDEV}p1 $TESTDIR/root
+
+ (
+ LOG_LEVEL=5
+ eval $(udevadm info --export --query=env --name=${LOOPDEV}p2)
+
+ setup_basic_environment
+
+ # mask some services that we do not want to run in these tests
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-hwdb-update.service
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-journal-catalog-update.service
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.service
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.socket
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-resolved.service
+ ln -fs /dev/null $initdir/etc/systemd/system/systemd-machined.service
+
+ # setup the testsuite service
+ cat >$initdir/etc/systemd/system/testsuite.service < /testok
+
+exit 0
diff --git a/units/meson.build b/units/meson.build
index cf4fe2e7bfb80..2ced8abc7273e 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -94,6 +94,8 @@ units = [
'sockets.target.wants/'],
['systemd-journald.socket', '',
'sockets.target.wants/'],
+ ['systemd-userdbd.socket', 'ENABLE_USERDB',
+ 'sockets.target.wants/'],
['systemd-networkd.socket', 'ENABLE_NETWORKD'],
['systemd-poweroff.service', ''],
['systemd-reboot.service', ''],
@@ -180,6 +182,9 @@ in_units = [
['systemd-nspawn@.service', ''],
['systemd-portabled.service', 'ENABLE_PORTABLED',
'dbus-org.freedesktop.portable1.service'],
+ ['systemd-userdbd.service', 'ENABLE_USERDB'],
+ ['systemd-homed.service', 'ENABLE_HOMED',
+ 'multi-user.target.wants/ dbus-org.freedesktop.home1.service'],
['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'],
['systemd-random-seed.service', 'ENABLE_RANDOMSEED',
'sysinit.target.wants/'],
diff --git a/units/systemd-homed.service.in b/units/systemd-homed.service.in
new file mode 100644
index 0000000000000..cb5edcb30d8e6
--- /dev/null
+++ b/units/systemd-homed.service.in
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Home Manager
+Documentation=man:systemd-homed.service(8)
+RequiresMountsFor=/home
+
+[Service]
+ExecStart=@rootlibexecdir@/systemd-homed
+BusName=org.freedesktop.home1
+WatchdogSec=3min
+#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD
+MemoryDenyWriteExecute=yes
+ProtectHostname=yes
+RestrictRealtime=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 AF_ALG
+SystemCallFilter=@system-service @mount
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+LockPersonality=yes
+IPAddressDeny=any
+StateDirectory=systemd/home
diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in
new file mode 100644
index 0000000000000..d451ec60243f5
--- /dev/null
+++ b/units/systemd-userdbd.service.in
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=User Database Manager
+Documentation=man:systemd-userdbd.service(8)
+Requires=systemd-userdbd.socket
+After=systemd-userdbd.socket
+Before=sysinit.target
+DefaultDependencies=no
+
+[Service]
+ExecStart=@rootlibexecdir@/systemd-userdbd
+Type=notify
+WatchdogSec=3min
+#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+ProtectHostname=yes
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+SystemCallFilter=@system-service
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+IPAddressDeny=any
+LimitNOFILE=@HIGH_RLIMIT_NOFILE@
diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket
new file mode 100644
index 0000000000000..1c749ea1d2321
--- /dev/null
+++ b/units/systemd-userdbd.socket
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=User Database Manager Socket
+Documentation=man:systemd-userdbd.service(8)
+DefaultDependencies=no
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/userdb/io.systemd.Multiplexer
+Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch
+SocketMode=0666