summaryrefslogtreecommitdiffstats
path: root/src/home
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
commitfc53809803cd2bc2434e312b19a18fa36776da12 (patch)
treeb4b43bd6538f51965ce32856e9c053d0f90919c8 /src/home
parentAdding upstream version 255.5. (diff)
downloadsystemd-fc53809803cd2bc2434e312b19a18fa36776da12.tar.xz
systemd-fc53809803cd2bc2434e312b19a18fa36776da12.zip
Adding upstream version 256.upstream/256
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/home')
-rw-r--r--src/home/home-util.c7
-rw-r--r--src/home/home-util.h11
-rw-r--r--src/home/homectl-fido2.c1
-rw-r--r--src/home/homectl-pkcs11.c183
-rw-r--r--src/home/homectl-pkcs11.h6
-rw-r--r--src/home/homectl.c1094
-rw-r--r--src/home/homed-bus.c70
-rw-r--r--src/home/homed-bus.h4
-rw-r--r--src/home/homed-conf.c9
-rw-r--r--src/home/homed-home-bus.c187
-rw-r--r--src/home/homed-home-bus.h3
-rw-r--r--src/home/homed-home.c349
-rw-r--r--src/home/homed-home.h21
-rw-r--r--src/home/homed-manager-bus.c195
-rw-r--r--src/home/homed-manager.c71
-rw-r--r--src/home/homed-operation.h1
-rw-r--r--src/home/homed.c2
-rw-r--r--src/home/homework-blob.c301
-rw-r--r--src/home/homework-blob.h9
-rw-r--r--src/home/homework-cifs.c4
-rw-r--r--src/home/homework-directory.c15
-rw-r--r--src/home/homework-fscrypt.c137
-rw-r--r--src/home/homework-fscrypt.h2
-rw-r--r--src/home/homework-luks.c280
-rw-r--r--src/home/homework-password-cache.c42
-rw-r--r--src/home/homework-password-cache.h12
-rw-r--r--src/home/homework-quota.c4
-rw-r--r--src/home/homework.c340
-rw-r--r--src/home/homework.h2
-rw-r--r--src/home/meson.build10
-rw-r--r--src/home/org.freedesktop.home1.conf28
-rw-r--r--src/home/org.freedesktop.home1.policy9
-rw-r--r--src/home/pam_systemd_home.c333
-rw-r--r--src/home/test-homed-regression-31896.c35
-rw-r--r--src/home/user-record-sign.c4
-rw-r--r--src/home/user-record-util.c123
-rw-r--r--src/home/user-record-util.h7
37 files changed, 2957 insertions, 954 deletions
diff --git a/src/home/home-util.c b/src/home/home-util.c
index c777d7b..9735236 100644
--- a/src/home/home-util.c
+++ b/src/home/home-util.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "dns-domain.h"
+#include "fd-util.h"
#include "home-util.h"
#include "libcrypt-util.h"
#include "memory-util.h"
@@ -9,6 +10,8 @@
#include "strv.h"
#include "user-util.h"
+DEFINE_HASH_OPS_FULL(blob_fd_hash_ops, char, path_hash_func, path_compare, free, void, close_fd_ptr);
+
bool suitable_user_name(const char *name) {
/* Checks whether the specified name is suitable for management via homed. Note that client-side
@@ -137,3 +140,7 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) {
const char *home_record_dir(void) {
return secure_getenv("SYSTEMD_HOME_RECORD_DIR") ?: "/var/lib/systemd/home/";
}
+
+const char *home_system_blob_dir(void) {
+ return secure_getenv("SYSTEMD_HOME_SYSTEM_BLOB_DIR") ?: "/var/cache/systemd/home/";
+}
diff --git a/src/home/home-util.h b/src/home/home-util.h
index 36b301d..42131b9 100644
--- a/src/home/home-util.h
+++ b/src/home/home-util.h
@@ -5,9 +5,17 @@
#include "sd-bus.h"
+#include "hash-funcs.h"
#include "time-util.h"
#include "user-record.h"
+/* Flags supported by UpdateEx() */
+#define SD_HOMED_UPDATE_OFFLINE (UINT64_C(1) << 0)
+#define SD_HOMED_UPDATE_FLAGS_ALL (SD_HOMED_UPDATE_OFFLINE)
+
+/* Flags supported by CreateHomeEx() */
+#define SD_HOMED_CREATE_FLAGS_ALL (0)
+
/* Put 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)
@@ -20,6 +28,8 @@
/* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */
assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U);
+extern const struct hash_ops blob_fd_hash_ops;
+
bool suitable_user_name(const char *name);
int suitable_realm(const char *realm);
int suitable_image_path(const char *path);
@@ -35,3 +45,4 @@ int bus_message_append_secret(sd_bus_message *m, UserRecord *secret);
#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
const char *home_record_dir(void);
+const char *home_system_blob_dir(void);
diff --git a/src/home/homectl-fido2.c b/src/home/homectl-fido2.c
index 3cbdf91..384461a 100644
--- a/src/home/homectl-fido2.c
+++ b/src/home/homectl-fido2.c
@@ -167,6 +167,7 @@ int identity_add_fido2_parameters(
/* user_display_name= */ rn ? json_variant_string(rn) : NULL,
/* user_icon_name= */ NULL,
/* askpw_icon_name= */ "user-home",
+ /* askpw_credential= */ "home.token-pin",
lock_with,
cred_alg,
&cid, &cid_size,
diff --git a/src/home/homectl-pkcs11.c b/src/home/homectl-pkcs11.c
index 2539af0..bb582d7 100644
--- a/src/home/homectl-pkcs11.c
+++ b/src/home/homectl-pkcs11.c
@@ -8,62 +8,55 @@
#include "memory-util.h"
#include "openssl-util.h"
#include "pkcs11-util.h"
-#include "random-util.h"
#include "strv.h"
-static int add_pkcs11_encrypted_key(
- JsonVariant **v,
- const char *uri,
- const void *encrypted_key, size_t encrypted_key_size,
- const void *decrypted_key, size_t decrypted_key_size) {
-
- _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
- _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
- ssize_t base64_encoded_size;
+int identity_add_token_pin(JsonVariant **v, const char *pin) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
+ _cleanup_strv_free_erase_ char **pins = NULL;
int r;
assert(v);
- assert(uri);
- assert(encrypted_key);
- assert(encrypted_key_size > 0);
- assert(decrypted_key);
- assert(decrypted_key_size > 0);
- /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
- * expect a NUL terminated string, and we use a binary key */
- base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
- if (base64_encoded_size < 0)
- return log_error_errno(base64_encoded_size, "Failed to base64 encode secret key: %m");
+ if (isempty(pin))
+ return 0;
- r = hash_password(base64_encoded, &hashed);
+ w = json_variant_ref(json_variant_by_key(*v, "secret"));
+ l = json_variant_ref(json_variant_by_key(w, "tokenPin"));
+
+ r = json_variant_strv(l, &pins);
if (r < 0)
- return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
+ return log_error_errno(r, "Failed to convert PIN array: %m");
- r = json_build(&e, JSON_BUILD_OBJECT(
- JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)),
- JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)),
- JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
+ if (strv_contains(pins, pin))
+ return 0;
+
+ r = strv_extend(&pins, pin);
if (r < 0)
- return log_error_errno(r, "Failed to build encrypted JSON key object: %m");
+ return log_oom();
- w = json_variant_ref(json_variant_by_key(*v, "privileged"));
- l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey"));
+ strv_uniq(pins);
- r = json_variant_append_array(&l, e);
+ l = json_variant_unref(l);
+
+ r = json_variant_new_array_strv(&l, pins);
if (r < 0)
- return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m");
+ return log_error_errno(r, "Failed to allocate new PIN array JSON: %m");
- r = json_variant_set_field(&w, "pkcs11EncryptedKey", l);
+ json_variant_sensitive(l);
+
+ r = json_variant_set_field(&w, "tokenPin", l);
if (r < 0)
- return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m");
+ return log_error_errno(r, "Failed to update PIN field: %m");
- r = json_variant_set_field(v, "privileged", w);
+ r = json_variant_set_field(v, "secret", w);
if (r < 0)
- return log_error_errno(r, "Failed to update privileged field: %m");
+ return log_error_errno(r, "Failed to update secret object: %m");
- return 0;
+ return 1;
}
+#if HAVE_P11KIT
+
static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) {
_cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
_cleanup_strv_free_ char **l = NULL;
@@ -98,100 +91,82 @@ static int add_pkcs11_token_uri(JsonVariant **v, const char *uri) {
return 0;
}
-int identity_add_token_pin(JsonVariant **v, const char *pin) {
- _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL;
- _cleanup_strv_free_erase_ char **pins = NULL;
+static int add_pkcs11_encrypted_key(
+ JsonVariant **v,
+ const char *uri,
+ const void *encrypted_key, size_t encrypted_key_size,
+ const void *decrypted_key, size_t decrypted_key_size) {
+
+ _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
+ _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
+ ssize_t base64_encoded_size;
int r;
assert(v);
+ assert(uri);
+ assert(encrypted_key);
+ assert(encrypted_key_size > 0);
+ assert(decrypted_key);
+ assert(decrypted_key_size > 0);
- if (isempty(pin))
- return 0;
-
- w = json_variant_ref(json_variant_by_key(*v, "secret"));
- l = json_variant_ref(json_variant_by_key(w, "tokenPin"));
+ /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
+ * expect a NUL terminated string, and we use a binary key */
+ base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded);
+ if (base64_encoded_size < 0)
+ return log_error_errno(base64_encoded_size, "Failed to base64 encode secret key: %m");
- r = json_variant_strv(l, &pins);
+ r = hash_password(base64_encoded, &hashed);
if (r < 0)
- return log_error_errno(r, "Failed to convert PIN array: %m");
-
- if (strv_contains(pins, pin))
- return 0;
+ return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
- r = strv_extend(&pins, pin);
+ r = json_build(&e, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("uri", JSON_BUILD_STRING(uri)),
+ JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(encrypted_key, encrypted_key_size)),
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed))));
if (r < 0)
- return log_oom();
-
- strv_uniq(pins);
+ return log_error_errno(r, "Failed to build encrypted JSON key object: %m");
- l = json_variant_unref(l);
+ w = json_variant_ref(json_variant_by_key(*v, "privileged"));
+ l = json_variant_ref(json_variant_by_key(w, "pkcs11EncryptedKey"));
- r = json_variant_new_array_strv(&l, pins);
+ r = json_variant_append_array(&l, e);
if (r < 0)
- return log_error_errno(r, "Failed to allocate new PIN array JSON: %m");
-
- json_variant_sensitive(l);
+ return log_error_errno(r, "Failed append PKCS#11 encrypted key: %m");
- r = json_variant_set_field(&w, "tokenPin", l);
+ r = json_variant_set_field(&w, "pkcs11EncryptedKey", l);
if (r < 0)
- return log_error_errno(r, "Failed to update PIN field: %m");
+ return log_error_errno(r, "Failed to set PKCS#11 encrypted key: %m");
- r = json_variant_set_field(v, "secret", w);
+ r = json_variant_set_field(v, "privileged", w);
if (r < 0)
- return log_error_errno(r, "Failed to update secret object: %m");
-
- return 1;
-}
+ return log_error_errno(r, "Failed to update privileged field: %m");
-static int acquire_pkcs11_certificate(
- const char *uri,
- const char *askpw_friendly_name,
- const char *askpw_icon_name,
- X509 **ret_cert,
- char **ret_pin_used) {
-#if HAVE_P11KIT
- return pkcs11_acquire_certificate(uri, askpw_friendly_name, askpw_icon_name, ret_cert, ret_pin_used);
-#else
- return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
- "PKCS#11 tokens not supported on this build.");
-#endif
+ return 0;
}
int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) {
- _cleanup_(erase_and_freep) void *decrypted_key = NULL, *encrypted_key = NULL;
+ _cleanup_(erase_and_freep) void *decrypted_key = NULL, *saved_key = NULL;
_cleanup_(erase_and_freep) char *pin = NULL;
- size_t decrypted_key_size, encrypted_key_size;
- _cleanup_(X509_freep) X509 *cert = NULL;
- EVP_PKEY *pkey;
+ size_t decrypted_key_size, saved_key_size;
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
int r;
assert(v);
- r = acquire_pkcs11_certificate(uri, "home directory operation", "user-home", &cert, &pin);
+ r = pkcs11_acquire_public_key(
+ uri,
+ "home directory operation",
+ "user-home",
+ "home.token-pin",
+ /* askpw_flags= */ 0,
+ &pkey,
+ &pin);
if (r < 0)
return r;
- pkey = X509_get0_pubkey(cert);
- if (!pkey)
- return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to extract public key from X.509 certificate.");
-
- r = rsa_pkey_to_suitable_key_size(pkey, &decrypted_key_size);
+ r = pkey_generate_volume_keys(pkey, &decrypted_key, &decrypted_key_size, &saved_key, &saved_key_size);
if (r < 0)
- return log_error_errno(r, "Failed to extract RSA key size from X509 certificate.");
-
- log_debug("Generating %zu bytes random key.", decrypted_key_size);
-
- decrypted_key = malloc(decrypted_key_size);
- if (!decrypted_key)
- return log_oom();
-
- r = crypto_random_bytes(decrypted_key, decrypted_key_size);
- if (r < 0)
- return log_error_errno(r, "Failed to generate random key: %m");
-
- r = rsa_encrypt_bytes(pkey, decrypted_key, decrypted_key_size, &encrypted_key, &encrypted_key_size);
- if (r < 0)
- return log_error_errno(r, "Failed to encrypt key: %m");
+ return log_error_errno(r, "Failed to generate volume keys: %m");
/* Add the token URI to the public part of the record. */
r = add_pkcs11_token_uri(v, uri);
@@ -202,7 +177,7 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) {
r = add_pkcs11_encrypted_key(
v,
uri,
- encrypted_key, encrypted_key_size,
+ saved_key, saved_key_size,
decrypted_key, decrypted_key_size);
if (r < 0)
return r;
@@ -216,3 +191,5 @@ int identity_add_pkcs11_key_data(JsonVariant **v, const char *uri) {
return 0;
}
+
+#endif
diff --git a/src/home/homectl-pkcs11.h b/src/home/homectl-pkcs11.h
index 5c30fee..424777f 100644
--- a/src/home/homectl-pkcs11.h
+++ b/src/home/homectl-pkcs11.h
@@ -5,7 +5,13 @@
int identity_add_token_pin(JsonVariant **v, const char *pin);
+#if HAVE_P11KIT
int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri);
+#else
+static inline int identity_add_pkcs11_key_data(JsonVariant **v, const char *token_uri) {
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "PKCS#11 tokens not supported on this build.");
+}
+#endif
int list_pkcs11_tokens(void);
int find_pkcs11_token_auto(char **ret);
diff --git a/src/home/homectl.c b/src/home/homectl.c
index a6951c8..d9321a2 100644
--- a/src/home/homectl.c
+++ b/src/home/homectl.c
@@ -12,6 +12,9 @@
#include "cap-list.h"
#include "capability-util.h"
#include "cgroup-util.h"
+#include "copy.h"
+#include "creds-util.h"
+#include "dirent-util.h"
#include "dns-domain.h"
#include "env-util.h"
#include "fd-util.h"
@@ -19,6 +22,7 @@
#include "format-table.h"
#include "fs-util.h"
#include "glyph-util.h"
+#include "hashmap.h"
#include "home-util.h"
#include "homectl-fido2.h"
#include "homectl-pkcs11.h"
@@ -35,16 +39,21 @@
#include "percent-util.h"
#include "pkcs11-util.h"
#include "pretty-print.h"
+#include "proc-cmdline.h"
#include "process-util.h"
+#include "recurse-dir.h"
#include "rlimit-util.h"
+#include "rm-rf.h"
#include "spawn-polkit-agent.h"
#include "terminal-util.h"
-#include "uid-alloc-range.h"
+#include "tmpfile-util.h"
+#include "uid-classification.h"
#include "user-record.h"
#include "user-record-password-quality.h"
#include "user-record-show.h"
#include "user-record-util.h"
#include "user-util.h"
+#include "userdb.h"
#include "verbs.h"
static PagerFlags arg_pager_flags = 0;
@@ -52,6 +61,7 @@ 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 bool arg_offline = false;
static const char *arg_identity = NULL;
static JsonVariant *arg_identity_extra = NULL;
static JsonVariant *arg_identity_extra_privileged = NULL;
@@ -80,6 +90,10 @@ static enum {
} arg_export_format = EXPORT_FORMAT_FULL;
static uint64_t arg_capability_bounding_set = UINT64_MAX;
static uint64_t arg_capability_ambient_set = UINT64_MAX;
+static bool arg_prompt_new_user = false;
+static char *arg_blob_dir = NULL;
+static bool arg_blob_clear = false;
+static Hashmap *arg_blob_files = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp);
STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp);
@@ -89,6 +103,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_token_uri, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_fido2_device, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_dir, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_blob_files, hashmap_freep);
static const BusLocator *bus_mgr;
@@ -102,7 +118,10 @@ static bool identity_properties_specified(void) {
!strv_isempty(arg_identity_filter) ||
!strv_isempty(arg_identity_filter_rlimits) ||
!strv_isempty(arg_pkcs11_token_uri) ||
- !strv_isempty(arg_fido2_device);
+ !strv_isempty(arg_fido2_device) ||
+ arg_blob_dir ||
+ arg_blob_clear ||
+ !hashmap_isempty(arg_blob_files);
}
static int acquire_bus(sd_bus **bus) {
@@ -184,7 +203,7 @@ static int list_homes(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_parse_error(r);
- if (table_get_rows(table) > 1 || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ if (!table_isempty(table) || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
r = table_set_sort(table, (size_t) 0);
if (r < 0)
return table_log_sort_error(r);
@@ -194,11 +213,11 @@ static int list_homes(int argc, char *argv[], void *userdata) {
return r;
}
- if (arg_legend && (arg_json_format_flags & JSON_FORMAT_OFF)) {
- if (table_get_rows(table) > 1)
- printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
- else
+ if (arg_legend && !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) {
+ if (table_isempty(table))
printf("No home areas.\n");
+ else
+ printf("\n%zu home areas listed.\n", table_get_rows(table) - 1);
}
return 0;
@@ -243,14 +262,14 @@ static int acquire_existing_password(
user_name) < 0)
return log_oom();
- r = ask_password_auto(question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.password",
- USEC_INFINITY,
- flags,
- &password);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-password",
+ .credential = "home.password",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &password);
if (r == -EUNATCH) { /* EUNATCH is returned if no password was found and asking interactively was
* disabled via the flags. Not an error for us. */
log_debug_errno(r, "No passwords acquired.");
@@ -301,14 +320,14 @@ static int acquire_recovery_key(
if (asprintf(&question, "Please enter recovery key for user %s:", user_name) < 0)
return log_oom();
- r = ask_password_auto(question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-recovery-key",
- /* credential_name= */ "home.recovery-key",
- USEC_INFINITY,
- flags,
- &recovery_key);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-recovery-key",
+ .credential = "home.recovery-key",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &recovery_key);
if (r == -EUNATCH) { /* EUNATCH is returned if no recovery key was found and asking interactively was
* disabled via the flags. Not an error for us. */
log_debug_errno(r, "No recovery keys acquired.");
@@ -355,15 +374,14 @@ static int acquire_token_pin(
if (asprintf(&question, "Please enter security token PIN for user %s:", user_name) < 0)
return log_oom();
- r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "token-pin",
- /* credential_name= */ "home.token-pin",
- USEC_INFINITY,
- flags,
- &pin);
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "token-pin",
+ .credential = "home.token-pin",
+ };
+
+ r = ask_password_auto(&req, USEC_INFINITY, flags, &pin);
if (r == -EUNATCH) { /* EUNATCH is returned if no PIN was found and asking interactively was disabled
* via the flags. Not an error for us. */
log_debug_errno(r, "No security token PINs acquired.");
@@ -735,7 +753,6 @@ static int inspect_home(int argc, char *argv[], void *userdata) {
r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", *i);
} else
r = bus_call_method(bus, bus_mgr, "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 == 0)
@@ -1092,7 +1109,7 @@ static int add_disposition(JsonVariant **v) {
return 1;
}
-static int acquire_new_home_record(UserRecord **ret) {
+static int acquire_new_home_record(JsonVariant *input, UserRecord **ret) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
@@ -1102,12 +1119,16 @@ static int acquire_new_home_record(UserRecord **ret) {
if (arg_identity) {
unsigned line, column;
+ if (input)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Two identity records specified, refusing.");
+
r = json_parse_file(
streq(arg_identity, "-") ? stdin : NULL,
streq(arg_identity, "-") ? "<stdin>" : 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);
- }
+ } else
+ v = json_variant_ref(input);
r = apply_identity_changes(&v);
if (r < 0)
@@ -1146,7 +1167,18 @@ static int acquire_new_home_record(UserRecord **ret) {
if (!hr)
return log_oom();
- 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|USER_RECORD_PERMISSIVE);
+ 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_STRIP_BINDING|
+ USER_RECORD_STRIP_STATUS|
+ USER_RECORD_STRIP_SIGNATURE|
+ USER_RECORD_LOG|
+ USER_RECORD_PERMISSIVE);
if (r < 0)
return r;
@@ -1191,19 +1223,22 @@ static int acquire_new_password(
_cleanup_free_ char *question = NULL;
if (--i == 0)
- return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:");
+ 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();
+ AskPasswordRequest req = {
+ .message = question,
+ .icon = "user-home",
+ .keyring = "home-password",
+ .credential = "home.new-password",
+ };
+
r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.new-password",
+ &req,
USEC_INFINITY,
- 0, /* no caching, we want to collect a new password here after all */
+ /* flags= */ 0, /* no caching, we want to collect a new password here after all */
&first);
if (r < 0)
return log_error_errno(r, "Failed to acquire password: %m");
@@ -1212,14 +1247,12 @@ static int acquire_new_password(
if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0)
return log_oom();
+ req.message = question;
+
r = ask_password_auto(
- question,
- /* icon= */ "user-home",
- NULL,
- /* key_name= */ "home-password",
- /* credential_name= */ "home.new-password",
+ &req,
USEC_INFINITY,
- 0, /* no caching */
+ /* flags= */ 0, /* no caching */
&second);
if (r < 0)
return log_error_errno(r, "Failed to acquire password: %m");
@@ -1247,47 +1280,145 @@ static int acquire_new_password(
}
}
-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;
+static int acquire_merged_blob_dir(UserRecord *hr, bool existing, Hashmap **ret) {
+ _cleanup_free_ char *sys_blob_path = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ const char *src_blob_path, *filename;
+ void *fd_ptr;
int r;
- r = acquire_bus(&bus);
- if (r < 0)
- return r;
+ assert(ret);
- (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+ HASHMAP_FOREACH_KEY(fd_ptr, filename, arg_blob_files) {
+ _cleanup_free_ char *filename_dup = NULL;
+ _cleanup_close_ int fd_dup = -EBADF;
- if (argc >= 2) {
- /* If a username was specified, use it */
+ filename_dup = strdup(filename);
+ if (!filename_dup)
+ return log_oom();
- if (valid_user_group_name(argv[1], 0))
- r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]);
+ if (PTR_TO_FD(fd_ptr) != -EBADF) {
+ fd_dup = fcntl(PTR_TO_FD(fd_ptr), F_DUPFD_CLOEXEC, 3);
+ if (fd_dup < 0)
+ return log_error_errno(errno, "Failed to duplicate fd of %s: %m", filename);
+ }
+
+ r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, filename_dup, FD_TO_PTR(fd_dup));
+ if (r < 0)
+ return r;
+ TAKE_PTR(filename_dup); /* Ownership transferred to hashmap */
+ TAKE_FD(fd_dup);
+ }
+
+ if (arg_blob_dir)
+ src_blob_path = arg_blob_dir;
+ else if (existing && !arg_blob_clear) {
+ if (hr->blob_directory)
+ src_blob_path = hr->blob_directory;
else {
- _cleanup_free_ char *un = NULL, *rr = NULL;
+ /* This isn't technically a correct thing to do for generic user records,
+ * so anyone looking at this code for reference shouldn't replicate it.
+ * However, since homectl is tied to homed, this is OK. This adds robustness
+ * for situations where the user record is coming directly from the CLI and
+ * thus doesn't have a blobDirectory set */
+
+ sys_blob_path = path_join(home_system_blob_dir(), hr->user_name);
+ if (!sys_blob_path)
+ return log_oom();
- /* 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]);
+ src_blob_path = sys_blob_path;
+ }
+ } else
+ goto nodir; /* Shortcut: no dir to merge with, so just return copy of arg_blob_files */
- 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");
- }
+ d = opendir(src_blob_path);
+ if (!d)
+ return log_error_errno(errno, "Failed to open %s: %m", src_blob_path);
- r = json_variant_set_field_string(&arg_identity_extra, "userName", un);
+ FOREACH_DIRENT_ALL(de, d, return log_error_errno(errno, "Failed to read %s: %m", src_blob_path)) {
+ _cleanup_free_ char *name = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (hashmap_contains(blobs, de->d_name))
+ continue; /* arg_blob_files should override the base dir */
+
+ if (!suitable_blob_filename(de->d_name)) {
+ log_warning("File %s in blob directory %s has an invalid filename. Skipping.", de->d_name, src_blob_path);
+ continue;
}
+
+ name = strdup(de->d_name);
+ if (!name)
+ return log_oom();
+
+ fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s in %s: %m", de->d_name, src_blob_path);
+
+ r = fd_verify_regular(fd);
+ if (r < 0) {
+ log_warning_errno(r, "Entry %s in blob directory %s is not a regular file. Skipping.", de->d_name, src_blob_path);
+ continue;
+ }
+
+ r = hashmap_ensure_put(&blobs, &blob_fd_hash_ops, name, FD_TO_PTR(fd));
if (r < 0)
- return 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.");
+ return r;
+ TAKE_PTR(name); /* Ownership transferred to hashmap */
+ TAKE_FD(fd);
+ }
+
+nodir:
+ *ret = TAKE_PTR(blobs);
+ return 0;
+}
+
+static int bus_message_append_blobs(sd_bus_message *m, Hashmap *blobs) {
+ const char *filename;
+ void *fd_ptr;
+ int r;
+
+ assert(m);
+
+ r = sd_bus_message_open_container(m, 'a', "{sh}");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH_KEY(fd_ptr, filename, blobs) {
+ int fd = PTR_TO_FD(fd_ptr);
+
+ if (fd == -EBADF) /* File marked for deletion */
+ continue;
+
+ r = sd_bus_message_append(m, "{sh}", filename, fd);
+ if (r < 0)
+ return r;
}
- r = acquire_new_home_record(&hr);
+ return sd_bus_message_close_container(m);
+}
+
+static int create_home_common(JsonVariant *input) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = acquire_new_home_record(input, &hr);
+ if (r < 0)
+ return r;
+
+ r = acquire_merged_blob_dir(hr, false, &blobs);
if (r < 0)
return r;
@@ -1337,7 +1468,7 @@ static int create_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return log_error_errno(r, "Failed to format user record: %m");
- r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHome");
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "CreateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1347,6 +1478,14 @@ static int create_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_create_error(r);
+ r = bus_message_append_blobs(m, blobs);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "t", UINT64_C(0));
+ if (r < 0)
+ return bus_log_create_error(r);
+
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0) {
if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) {
@@ -1374,6 +1513,41 @@ static int create_home(int argc, char *argv[], void *userdata) {
return 0;
}
+static int create_home(int argc, char *argv[], void *userdata) {
+ int r;
+
+ if (argc >= 2) {
+ /* If a username was specified, use it */
+
+ if (valid_user_group_name(argv[1], 0))
+ 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.", 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.");
+ }
+
+ return create_home_common(/* input= */ NULL);
+}
+
static int remove_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
int r, ret = 0;
@@ -1467,7 +1641,7 @@ static int acquire_updated_home_record(
reply = sd_bus_message_unref(reply);
- r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature"));
+ r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature", "blobManifest"));
if (r < 0)
return log_error_errno(r, "Failed to strip binding and status from record to update: %m");
}
@@ -1537,7 +1711,9 @@ static int update_home(int argc, char *argv[], void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL, *secret = NULL;
_cleanup_free_ char *buffer = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
const char *username;
+ uint64_t flags = 0;
int r;
if (argc >= 2)
@@ -1570,18 +1746,25 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return r;
+ r = acquire_merged_blob_dir(hr, true, &blobs);
+ if (r < 0)
+ return r;
+
/* If we do multiple operations, let's output things more verbosely, since otherwise the repeated
* authentication might be confusing. */
if (arg_and_resize || arg_and_change_password)
log_info("Updating home directory.");
+ if (arg_offline)
+ flags |= SD_HOMED_UPDATE_OFFLINE;
+
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 = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHome");
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "UpdateHomeEx");
if (r < 0)
return bus_log_create_error(r);
@@ -1595,6 +1778,14 @@ static int update_home(int argc, char *argv[], void *userdata) {
if (r < 0)
return bus_log_create_error(r);
+ r = bus_message_append_blobs(m, blobs);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "t", flags);
+ if (r < 0)
+ return bus_log_create_error(r);
+
r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0) {
if (arg_and_change_password &&
@@ -2131,6 +2322,190 @@ static int rebalance(int argc, char *argv[], void *userdata) {
return 0;
}
+static int create_from_credentials(void) {
+ _cleanup_close_ int fd = -EBADF;
+ int ret = 0, n_created = 0, r;
+
+ fd = open_credentials_dir();
+ if (IN_SET(fd, -ENXIO, -ENOENT)) /* Credential env var not set, or dir doesn't exist. */
+ return 0;
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open credentials directory: %m");
+
+ _cleanup_free_ DirectoryEntries *des = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate credentials: %m");
+
+ FOREACH_ARRAY(i, des->entries, des->n_entries) {
+ _cleanup_(json_variant_unrefp) JsonVariant *identity = NULL;
+ struct dirent *de = *i;
+ const char *e;
+
+ if (de->d_type != DT_REG)
+ continue;
+
+ e = startswith(de->d_name, "home.create.");
+ if (!e)
+ continue;
+
+ if (!valid_user_group_name(e, 0)) {
+ log_notice("Skipping over credential with name that is not a suitable user name: %s", de->d_name);
+ continue;
+ }
+
+ r = json_parse_file_at(
+ /* f= */ NULL,
+ fd,
+ de->d_name,
+ /* flags= */ 0,
+ &identity,
+ /* ret_line= */ NULL,
+ /* ret_column= */ NULL);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to parse user record in credential '%s', ignoring: %m", de->d_name);
+ continue;
+ }
+
+ JsonVariant *un;
+ un = json_variant_by_key(identity, "userName");
+ if (un) {
+ if (!json_variant_is_string(un)) {
+ log_warning("User record from credential '%s' contains 'userName' field of invalid type, ignoring.", de->d_name);
+ continue;
+ }
+
+ if (!streq(json_variant_string(un), e)) {
+ log_warning("User record from credential '%s' contains 'userName' field (%s) that doesn't match credential name (%s), ignoring.", de->d_name, json_variant_string(un), e);
+ continue;
+ }
+ } else {
+ r = json_variant_set_field_string(&identity, "userName", e);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to set userName field: %m");
+ }
+
+ log_notice("Processing user '%s' from credentials.", e);
+
+ r = create_home_common(identity);
+ if (r >= 0)
+ n_created++;
+
+ RET_GATHER(ret, r);
+ }
+
+ return ret < 0 ? ret : n_created;
+}
+
+static int has_regular_user(void) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create user enumerator: %m");
+
+ for (;;) {
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+
+ r = userdb_iterator_get(iterator, &ur);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate users: %m");
+
+ if (user_record_disposition(ur) == USER_REGULAR)
+ return true;
+ }
+
+ return false;
+}
+
+static int create_interactively(void) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *username = NULL;
+ int r;
+
+ if (!arg_prompt_new_user) {
+ log_debug("Prompting for user creation was not requested.");
+ return 0;
+ }
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ (void) reset_terminal_fd(STDIN_FILENO, /* switch_to_text= */ false);
+
+ for (;;) {
+ username = mfree(username);
+
+ r = ask_string(&username,
+ "%s Please enter user name to create (empty to skip): ",
+ special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET));
+ if (r < 0)
+ return log_error_errno(r, "Failed to query user for username: %m");
+
+ if (isempty(username)) {
+ log_info("No data entered, skipping.");
+ return 0;
+ }
+
+ if (!valid_user_group_name(username, /* flags= */ 0)) {
+ log_notice("Specified user name is not a valid UNIX user name, try again: %s", username);
+ continue;
+ }
+
+ r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if specified user '%s' already exists: %m", username);
+
+ log_notice("Specified user '%s' exists already, try again.", username);
+ }
+
+ 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");
+
+ return create_home_common(/* input= */ NULL);
+}
+
+static int verb_firstboot(int argc, char *argv[], void *userdata) {
+ int r;
+
+ /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot
+ * tool. */
+
+ bool enabled;
+ r = proc_cmdline_get_bool("systemd.firstboot", /* flags = */ 0, &enabled);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m");
+ if (r > 0 && !enabled) {
+ log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts.");
+ arg_prompt_new_user = false;
+ }
+
+ r = create_from_credentials();
+ if (r < 0)
+ return r;
+ if (r > 0) /* Already created users from credentials */
+ return 0;
+
+ r = has_regular_user();
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ log_info("Regular user already present in user database, skipping user creation.");
+ return 0;
+ }
+
+ return create_interactively();
+}
+
static int drop_from_identity(const char *field) {
int r;
@@ -2187,12 +2562,14 @@ static int help(int argc, char *argv[], void *userdata) {
" deactivate-all Deactivate all active home areas\n"
" rebalance Rebalance free space between home areas\n"
" with USER [COMMAND…] Run shell or command with access to a home area\n"
+ " firstboot Run first-boot home area creation wizard\n"
"\n%4$sOptions:%5$s\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"
+ " --offline Don't update record embedded in home directory\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"
@@ -2205,6 +2582,8 @@ static int help(int argc, char *argv[], void *userdata) {
" -E When specified once equals -j --export-format=\n"
" stripped, when specified twice equals\n"
" -j --export-format=minimal\n"
+ " --prompt-new-user firstboot: Query user interactively for user\n"
+ " to create\n"
"\n%4$sGeneral User Record Properties:%5$s\n"
" -c --real-name=REALNAME Real name for user\n"
" --realm=REALM Realm to create user in\n"
@@ -2222,7 +2601,7 @@ static int help(int argc, char *argv[], void *userdata) {
" --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"
+ " --language=LOCALE Set preferred languages\n"
" --ssh-authorized-keys=KEYS\n"
" Specify SSH public keys\n"
" --pkcs11-token-uri=URI URI to PKCS#11 security token containing\n"
@@ -2239,7 +2618,13 @@ static int help(int argc, char *argv[], void *userdata) {
" Whether to require user verification to unlock\n"
" the account\n"
" --recovery-key=BOOL Add a recovery key\n"
- "\n%4$sAccount Management User Record Properties:%5$s\n"
+ "\n%4$sBlob Directory User Record Properties:%5$s\n"
+ " -b --blob=[FILENAME=]PATH\n"
+ " Path to a replacement blob directory, or replace\n"
+ " an individual files in the blob directory.\n"
+ " --avatar=PATH Path to user avatar picture\n"
+ " --login-background=PATH Path to user login background picture\n"
+ "\n%4$sAccount Management User Record Properties:%5$s\n"
" --locked=BOOL Set locked account state\n"
" --not-before=TIMESTAMP Do not allow logins before\n"
" --not-after=TIMESTAMP Do not allow logins after\n"
@@ -2322,6 +2707,9 @@ static int help(int argc, char *argv[], void *userdata) {
" --kill-processes=BOOL Whether to kill user processes when sessions\n"
" terminate\n"
" --auto-login=BOOL Try to log this user in automatically\n"
+ " --session-launcher=LAUNCHER\n"
+ " Preferred session launcher file\n"
+ " --session-type=TYPE Preferred session type\n"
"\nSee the %6$s for details.\n",
program_invocation_short_name,
ansi_highlight(),
@@ -2334,12 +2722,14 @@ static int help(int argc, char *argv[], void *userdata) {
}
static int parse_argv(int argc, char *argv[]) {
+ _cleanup_strv_free_ char **arg_languages = NULL;
enum {
ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_NO_ASK_PASSWORD,
+ ARG_OFFLINE,
ARG_REALM,
ARG_EMAIL_ADDRESS,
ARG_DISK_SIZE,
@@ -2397,6 +2787,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_PASSWORD_CHANGE_INACTIVE,
ARG_EXPORT_FORMAT,
ARG_AUTO_LOGIN,
+ ARG_SESSION_LAUNCHER,
+ ARG_SESSION_TYPE,
ARG_PKCS11_TOKEN_URI,
ARG_FIDO2_DEVICE,
ARG_FIDO2_WITH_PIN,
@@ -2412,98 +2804,108 @@ static int parse_argv(int argc, char *argv[]) {
ARG_FIDO2_CRED_ALG,
ARG_CAPABILITY_BOUNDING_SET,
ARG_CAPABILITY_AMBIENT_SET,
+ ARG_PROMPT_NEW_USER,
+ ARG_AVATAR,
+ ARG_LOGIN_BACKGROUND,
};
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-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_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-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS },
- { "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 },
- { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE },
- { "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 },
- { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS },
- { "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 },
- { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW },
- { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN },
- { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX },
- { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN },
- { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE },
- { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN },
- { "json", required_argument, NULL, ARG_JSON },
- { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
- { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
- { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
- { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
- { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
- { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
- { "fido2-with-user-verification",required_argument, NULL, ARG_FIDO2_WITH_UV },
- { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY },
- { "and-resize", required_argument, NULL, ARG_AND_RESIZE },
- { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
- { "drop-caches", required_argument, NULL, ARG_DROP_CACHES },
- { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS },
- { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE },
- { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT },
- { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET },
- { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET },
+ { "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 },
+ { "offline", no_argument, NULL, ARG_OFFLINE },
+ { "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-offline-discard", required_argument, NULL, ARG_LUKS_OFFLINE_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-force-iterations", required_argument, NULL, ARG_LUKS_PBKDF_FORCE_ITERATIONS },
+ { "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 },
+ { "luks-sector-size", required_argument, NULL, ARG_LUKS_SECTOR_SIZE },
+ { "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 },
+ { "cifs-extra-mount-options", required_argument, NULL, ARG_CIFS_EXTRA_MOUNT_OPTIONS },
+ { "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 },
+ { "password-change-now", required_argument, NULL, ARG_PASSWORD_CHANGE_NOW },
+ { "password-change-min", required_argument, NULL, ARG_PASSWORD_CHANGE_MIN },
+ { "password-change-max", required_argument, NULL, ARG_PASSWORD_CHANGE_MAX },
+ { "password-change-warn", required_argument, NULL, ARG_PASSWORD_CHANGE_WARN },
+ { "password-change-inactive", required_argument, NULL, ARG_PASSWORD_CHANGE_INACTIVE },
+ { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN },
+ { "session-launcher", required_argument, NULL, ARG_SESSION_LAUNCHER, },
+ { "session-type", required_argument, NULL, ARG_SESSION_TYPE, },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT },
+ { "pkcs11-token-uri", required_argument, NULL, ARG_PKCS11_TOKEN_URI },
+ { "fido2-credential-algorithm", required_argument, NULL, ARG_FIDO2_CRED_ALG },
+ { "fido2-device", required_argument, NULL, ARG_FIDO2_DEVICE },
+ { "fido2-with-client-pin", required_argument, NULL, ARG_FIDO2_WITH_PIN },
+ { "fido2-with-user-presence", required_argument, NULL, ARG_FIDO2_WITH_UP },
+ { "fido2-with-user-verification", required_argument, NULL, ARG_FIDO2_WITH_UV },
+ { "recovery-key", required_argument, NULL, ARG_RECOVERY_KEY },
+ { "and-resize", required_argument, NULL, ARG_AND_RESIZE },
+ { "and-change-password", required_argument, NULL, ARG_AND_CHANGE_PASSWORD },
+ { "drop-caches", required_argument, NULL, ARG_DROP_CACHES },
+ { "luks-extra-mount-options", required_argument, NULL, ARG_LUKS_EXTRA_MOUNT_OPTIONS },
+ { "auto-resize-mode", required_argument, NULL, ARG_AUTO_RESIZE_MODE },
+ { "rebalance-weight", required_argument, NULL, ARG_REBALANCE_WEIGHT },
+ { "capability-bounding-set", required_argument, NULL, ARG_CAPABILITY_BOUNDING_SET },
+ { "capability-ambient-set", required_argument, NULL, ARG_CAPABILITY_AMBIENT_SET },
+ { "prompt-new-user", no_argument, NULL, ARG_PROMPT_NEW_USER },
+ { "blob", required_argument, NULL, 'b' },
+ { "avatar", required_argument, NULL, ARG_AVATAR },
+ { "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND },
{}
};
@@ -2515,7 +2917,7 @@ static int parse_argv(int argc, char *argv[]) {
for (;;) {
int c;
- c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL);
+ c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL);
if (c < 0)
break;
@@ -2539,6 +2941,10 @@ static int parse_argv(int argc, char *argv[]) {
arg_ask_password = false;
break;
+ case ARG_OFFLINE:
+ arg_offline = true;
+ break;
+
case 'H':
arg_transport = BUS_TRANSPORT_REMOTE;
arg_host = optarg;
@@ -2609,7 +3015,7 @@ static int parse_argv(int argc, char *argv[]) {
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);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain.", optarg);
r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg);
if (r < 0)
@@ -2622,7 +3028,9 @@ static int parse_argv(int argc, char *argv[]) {
case ARG_CIFS_USER_NAME:
case ARG_CIFS_DOMAIN:
case ARG_CIFS_EXTRA_MOUNT_OPTIONS:
- case ARG_LUKS_EXTRA_MOUNT_OPTIONS: {
+ case ARG_LUKS_EXTRA_MOUNT_OPTIONS:
+ case ARG_SESSION_LAUNCHER:
+ case ARG_SESSION_TYPE: {
const char *field =
c == ARG_EMAIL_ADDRESS ? "emailAddress" :
@@ -2632,6 +3040,8 @@ static int parse_argv(int argc, char *argv[]) {
c == ARG_CIFS_DOMAIN ? "cifsDomain" :
c == ARG_CIFS_EXTRA_MOUNT_OPTIONS ? "cifsExtraMountOptions" :
c == ARG_LUKS_EXTRA_MOUNT_OPTIONS ? "luksExtraMountOptions" :
+ c == ARG_SESSION_LAUNCHER ? "preferredSessionLauncher" :
+ c == ARG_SESSION_TYPE ? "preferredSessionType" :
NULL;
assert(field);
@@ -2754,15 +3164,15 @@ static int parse_argv(int argc, char *argv[]) {
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);
+ return log_error_errno(r, "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");
+ return log_error_errno(r, "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");
+ return log_error_errno(r, "Failed to allocate maximum integer: %m");
t = strjoin("RLIMIT_", rlimit_to_string(l));
if (!t)
@@ -2906,26 +3316,46 @@ static int parse_argv(int argc, char *argv[]) {
break;
- case ARG_LANGUAGE:
- if (isempty(optarg)) {
- r = drop_from_identity("language");
+ case ARG_LANGUAGE: {
+ const char *p = optarg;
+
+ if (isempty(p)) {
+ r = drop_from_identity("preferredLanguage");
+ if (r < 0)
+ return r;
+
+ r = drop_from_identity("additionalLanguages");
if (r < 0)
return r;
+ arg_languages = strv_free(arg_languages);
break;
}
- if (!locale_is_valid(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg);
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
- if (locale_is_installed(optarg) <= 0)
- log_warning("Locale '%s' is not installed, accepting anyway.", optarg);
+ r = extract_first_word(&p, &word, ",:", 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse locale list: %m");
+ if (r == 0)
+ break;
- 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");
+ if (!locale_is_valid(word))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", word);
+
+ if (locale_is_installed(word) <= 0)
+ log_warning("Locale '%s' is not installed, accepting anyway.", word);
+
+ r = strv_consume(&arg_languages, TAKE_PTR(word));
+ if (r < 0)
+ return log_oom();
+
+ strv_uniq(arg_languages);
+ }
break;
+ }
case ARG_NOSUID:
case ARG_NODEV:
@@ -3788,6 +4218,82 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ case ARG_PROMPT_NEW_USER:
+ arg_prompt_new_user = true;
+ break;
+
+ case 'b':
+ case ARG_AVATAR:
+ case ARG_LOGIN_BACKGROUND: {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *path = NULL, *filename = NULL;
+
+ if (c == 'b') {
+ char *eq;
+
+ if (isempty(optarg)) { /* --blob= deletes everything, including existing blob dirs */
+ hashmap_clear(arg_blob_files);
+ arg_blob_dir = mfree(arg_blob_dir);
+ arg_blob_clear = true;
+ break;
+ }
+
+ eq = strrchr(optarg, '=');
+ if (!eq) { /* --blob=/some/path replaces the blob dir */
+ r = parse_path_argument(optarg, false, &arg_blob_dir);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+ break;
+ }
+
+ /* --blob=filename=/some/path replaces the file "filename" with /some/path */
+ filename = strndup(optarg, eq - optarg);
+ if (!filename)
+ return log_oom();
+
+ if (isempty(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse blob file assignment: %s", optarg);
+ if (!suitable_blob_filename(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid blob filename: %s", filename);
+
+ r = parse_path_argument(eq + 1, false, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", eq + 1);
+ } else {
+ const char *well_known_filename =
+ c == ARG_AVATAR ? "avatar" :
+ c == ARG_LOGIN_BACKGROUND ? "login-background" :
+ NULL;
+ assert(well_known_filename);
+
+ filename = strdup(well_known_filename);
+ if (!filename)
+ return log_oom();
+
+ r = parse_path_argument(optarg, false, &path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse path %s: %m", optarg);
+ }
+
+ if (path) {
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s: %m", path);
+
+ if (fd_verify_regular(fd) < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provided blob is not a regular file: %s", path);
+ } else
+ fd = -EBADF; /* Delete the file */
+
+ r = hashmap_ensure_put(&arg_blob_files, &blob_fd_hash_ops, filename, FD_TO_PTR(fd));
+ if (r < 0)
+ return log_error_errno(r, "Failed to map %s to %s in blob directory: %m", path, filename);
+ TAKE_PTR(filename); /* hashmap takes ownership */
+ TAKE_FD(fd);
+
+ break;
+ }
+
case '?':
return -EINVAL;
@@ -3802,6 +4308,25 @@ static int parse_argv(int argc, char *argv[]) {
if (arg_disk_size != UINT64_MAX || arg_disk_size_relative != UINT64_MAX)
arg_and_resize = true;
+ if (!strv_isempty(arg_languages)) {
+ char **additional;
+
+ r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", arg_languages[0]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update preferred language: %m");
+
+ additional = strv_skip(arg_languages, 1);
+ if (!strv_isempty(additional)) {
+ r = json_variant_set_field_strv(&arg_identity_extra, "additionalLanguages", additional);
+ if (r < 0)
+ return log_error_errno(r, "Failed to update additional language list: %m");
+ } else {
+ r = drop_from_identity("additionalLanguages");
+ if (r < 0)
+ return r;
+ }
+ }
+
return 1;
}
@@ -3835,6 +4360,197 @@ static int redirect_bus_mgr(void) {
return 0;
}
+static bool is_fallback_shell(const char *p) {
+ const char *q;
+
+ if (!p)
+ return false;
+
+ if (p[0] == '-') {
+ /* Skip over login shell dash */
+ p++;
+
+ if (streq(p, "ystemd-home-fallback-shell")) /* maybe the dash was used to override the binary name? */
+ return true;
+ }
+
+ q = strrchr(p, '/'); /* Skip over path */
+ if (q)
+ p = q + 1;
+
+ return streq(p, "systemd-home-fallback-shell");
+}
+
+static int fallback_shell(int argc, char *argv[]) {
+ _cleanup_(user_record_unrefp) UserRecord *secret = NULL, *hr = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ char *argv0 = NULL;
+ const char *json, *hd, *shell;
+ int r, incomplete;
+
+ /* So here's the deal: if users log into a system via ssh, and their homed-managed home directory
+ * wasn't activated yet, SSH will permit the access but the home directory isn't actually available
+ * yet. SSH doesn't allow us to ask authentication questions from the PAM session stack, and doesn't
+ * run the PAM authentication stack (because it authenticates via its own key management, after
+ * all). So here's our way to support this: homectl can be invoked as a multi-call binary under the
+ * name "systemd-home-fallback-shell". If so, it will chainload a login shell, but first try to
+ * unlock the home directory of the user it is invoked as. systemd-homed will then override the shell
+ * listed in user records whose home directory is not activated yet with this pseudo-shell. Net
+ * effect: one SSH auth succeeds this pseudo shell gets invoked, which will unlock the homedir
+ * (possibly asking for a passphrase) and then chainload the regular shell. Once the login is
+ * complete the user record will look like any other. */
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ for (unsigned n_tries = 0;; n_tries++) {
+ _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;
+
+ if (n_tries >= 5)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to activate home dir, even after %u tries.", n_tries);
+
+ /* Let's start by checking if this all is even necessary, i.e. if the useFallback boolean field is actually set. */
+ r = bus_call_method(bus, bus_mgr, "GetUserRecordByName", &error, &reply, "s", NULL); /* empty user string means: our calling user */
+ if (r < 0)
+ return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r));
+
+ 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 identity: %m");
+
+ hr = user_record_new();
+ if (!hr)
+ return log_oom();
+
+ r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ return r;
+
+ if (!hr->use_fallback) /* Nice! We are done, fallback logic not necessary */
+ break;
+
+ if (!secret) {
+ r = acquire_passed_secrets(hr->user_name, &secret);
+ if (r < 0)
+ return r;
+ }
+
+ for (;;) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = bus_message_new_method_call(bus, &m, bus_mgr, "ActivateHomeIfReferenced");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", NULL); /* empty user string means: our calling user */
+ 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_HOME_NOT_REFERENCED))
+ return log_error_errno(r, "Called without reference on home taken, can't operate.");
+
+ r = handle_generic_user_record_error(hr->user_name, secret, &error, r, false);
+ if (r < 0)
+ return r;
+
+ sd_bus_error_free(&error);
+ } else
+ break;
+ }
+
+ /* Try again */
+ hr = user_record_unref(hr);
+ }
+
+ incomplete = getenv_bool("XDG_SESSION_INCOMPLETE"); /* pam_systemd_home reports this state via an environment variable to us. */
+ if (incomplete < 0 && incomplete != -ENXIO)
+ return log_error_errno(incomplete, "Failed to parse $XDG_SESSION_INCOMPLETE environment variable: %m");
+ if (incomplete > 0) {
+ /* We are still in an "incomplete" session here. Now upgrade it to a full one. This will make logind
+ * start the user@.service instance for us. */
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.login1",
+ "/org/freedesktop/login1/session/self",
+ "org.freedesktop.login1.Session",
+ "SetClass",
+ &error,
+ /* ret_reply= */ NULL,
+ "s",
+ "user");
+ if (r < 0)
+ return log_error_errno(r, "Failed to upgrade session: %s", bus_error_message(&error, r));
+
+ if (setenv("XDG_SESSION_CLASS", "user", /* overwrite= */ true) < 0) /* Update the XDG_SESSION_CLASS environment variable to match the above */
+ return log_error_errno(errno, "Failed to set $XDG_SESSION_CLASS: %m");
+
+ if (unsetenv("XDG_SESSION_INCOMPLETE") < 0) /* Unset the 'incomplete' env var */
+ return log_error_errno(errno, "Failed to unset $XDG_SESSION_INCOMPLETE: %m");
+ }
+
+ /* We are going to invoke execv() soon. Let's be extra accurate and flush/close our bus connection
+ * first, just to make sure anything queued is flushed out (though there shouldn't be anything) */
+ bus = sd_bus_flush_close_unref(bus);
+
+ assert(!hr->use_fallback);
+ assert_se(shell = user_record_shell(hr));
+ assert_se(hd = user_record_home_directory(hr));
+
+ /* Extra protection: avoid loops */
+ if (is_fallback_shell(shell))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Primary shell of '%s' is fallback shell, refusing loop.", hr->user_name);
+
+ if (chdir(hd) < 0)
+ return log_error_errno(errno, "Failed to change directory to home directory '%s': %m", hd);
+
+ if (setenv("SHELL", shell, /* overwrite= */ true) < 0)
+ return log_error_errno(errno, "Failed to set $SHELL: %m");
+
+ if (setenv("HOME", hd, /* overwrite= */ true) < 0)
+ return log_error_errno(errno, "Failed to set $HOME: %m");
+
+ /* Paranoia: in case the client passed some passwords to us to help us unlock, unlock things now */
+ FOREACH_STRING(ue, "PASSWORD", "NEWPASSWORD", "PIN")
+ if (unsetenv(ue) < 0)
+ return log_error_errno(errno, "Failed to unset $%s: %m", ue);
+
+ r = path_extract_filename(shell, &argv0);
+ if (r < 0)
+ return log_error_errno(r, "Unable to extract file name from '%s': %m", shell);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Shell '%s' is a path to a directory, refusing.", shell);
+
+ /* Invoke this as login shell, by setting argv[0][0] to '-' (unless we ourselves weren't called as login shell) */
+ if (!argv || isempty(argv[0]) || argv[0][0] == '-')
+ argv0[0] = '-';
+
+ l = strv_new(argv0);
+ if (!l)
+ return log_oom();
+
+ if (strv_extend_strv(&l, strv_skip(argv, 1), /* filter_duplicates= */ false) < 0)
+ return log_oom();
+
+ execv(shell, l);
+ return log_error_errno(errno, "Failed to execute shell '%s': %m", shell);
+}
+
static int run(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, VERB_ANY, 0, help },
@@ -3854,6 +4570,7 @@ static int run(int argc, char *argv[]) {
{ "lock-all", VERB_ANY, 1, 0, lock_all_homes },
{ "deactivate-all", VERB_ANY, 1, 0, deactivate_all_homes },
{ "rebalance", VERB_ANY, 1, 0, rebalance },
+ { "firstboot", VERB_ANY, 1, 0, verb_firstboot },
{}
};
@@ -3865,6 +4582,9 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
+ if (is_fallback_shell(argv[0]))
+ return fallback_shell(argc, argv);
+
r = parse_argv(argc, argv);
if (r <= 0)
return r;
diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c
index 24b421a..a6f26fe 100644
--- a/src/home/homed-bus.c
+++ b/src/home/homed-bus.c
@@ -1,6 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "fd-util.h"
+#include "home-util.h"
#include "homed-bus.h"
+#include "stat-util.h"
#include "strv.h"
int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) {
@@ -64,3 +67,70 @@ int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, U
*ret = TAKE_PTR(hr);
return 0;
}
+
+int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error) {
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ int r;
+
+ assert(m);
+ assert(ret);
+
+ /* We want to differentiate between blobs being NULL (not passed at all)
+ * and empty (passed from dbus, but it was empty) */
+ r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_enter_container(m, 'a', "{sh}");
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ const char *_filename = NULL;
+ int _fd;
+
+ r = sd_bus_message_read(m, "{sh}", &_filename, &_fd);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ filename = strdup(_filename);
+ if (!filename)
+ return -ENOMEM;
+
+ fd = fcntl(_fd, F_DUPFD_CLOEXEC, 3);
+ if (fd < 0)
+ return -errno;
+
+ r = suitable_blob_filename(filename);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid blob directory filename: %s", filename);
+
+ r = fd_verify_regular(fd);
+ if (r < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for '%s' is not a regular file", filename);
+
+ r = fd_verify_safe_flags(fd);
+ if (r == -EREMOTEIO)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
+ "FD for '%s' has unexpected flags set", filename);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(blobs, filename, FD_TO_PTR(fd));
+ if (r < 0)
+ return r;
+ TAKE_PTR(filename); /* Ownership transferred to hashmap */
+ TAKE_FD(fd);
+ }
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(blobs);
+ return 0;
+}
diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h
index 977679b..0660a59 100644
--- a/src/home/homed-bus.h
+++ b/src/home/homed-bus.h
@@ -3,8 +3,10 @@
#include "sd-bus.h"
-#include "user-record.h"
+#include "hashmap.h"
#include "json.h"
+#include "user-record.h"
int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error);
int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error);
+int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error);
diff --git a/src/home/homed-conf.c b/src/home/homed-conf.c
index ffa4bb3..3f74096 100644
--- a/src/home/homed-conf.c
+++ b/src/home/homed-conf.c
@@ -9,9 +9,12 @@ int manager_parse_config_file(Manager *m) {
assert(m);
- return config_parse_config_file("homed.conf", "Home\0",
- config_item_perf_lookup, homed_gperf_lookup,
- CONFIG_PARSE_WARN, m);
+ return config_parse_standard_file_with_dropins(
+ "systemd/homed.conf",
+ "Home\0",
+ config_item_perf_lookup, homed_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ m);
}
DEFINE_CONFIG_PARSE_ENUM(config_parse_default_storage, user_storage, UserStorage, "Failed to parse default storage setting");
diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c
index a47f4d8..23578fe 100644
--- a/src/home/homed-home-bus.c
+++ b/src/home/homed-home-bus.c
@@ -5,6 +5,8 @@
#include "bus-common-errors.h"
#include "bus-polkit.h"
#include "fd-util.h"
+#include "format-util.h"
+#include "home-util.h"
#include "homed-bus.h"
#include "homed-home-bus.h"
#include "homed-home.h"
@@ -74,6 +76,34 @@ int bus_home_client_is_trusted(Home *h, sd_bus_message *message) {
return euid == 0 || h->uid == euid;
}
+static int home_verify_polkit_async(
+ Home *h,
+ sd_bus_message *message,
+ const char *action,
+ uid_t good_uid,
+ sd_bus_error *error) {
+
+ assert(h);
+ assert(message);
+ assert(action);
+ assert(error);
+
+ const char *details[] = {
+ "uid", FORMAT_UID(h->uid),
+ "username", h->user_name,
+ NULL
+ };
+
+ return bus_verify_polkit_async_full(
+ message,
+ action,
+ details,
+ good_uid,
+ /* flags= */ 0,
+ &h->manager->polkit_registry,
+ error);
+}
+
int bus_home_get_record_json(
Home *h,
sd_bus_message *message,
@@ -144,15 +174,31 @@ int bus_home_method_activate(
_cleanup_(user_record_unrefp) UserRecord *secret = NULL;
Home *h = ASSERT_PTR(userdata);
+ bool if_referenced;
int r;
assert(message);
+ if_referenced = endswith(sd_bus_message_get_member(message), "IfReferenced");
+
+ r = bus_verify_polkit_async_full(
+ message,
+ "org.freedesktop.home1.activate-home",
+ /* details= */ NULL,
+ h->uid,
+ /* flags= */ 0,
+ &h->manager->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
r = bus_message_read_secret(message, &secret, error);
if (r < 0)
return r;
- r = home_activate(h, secret, error);
+ r = home_activate(h, if_referenced, secret, error);
if (r < 0)
return r;
@@ -201,14 +247,11 @@ int bus_home_method_unregister(
assert(message);
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.remove-home",
- NULL,
- true,
UID_INVALID,
- &h->manager->polkit_registry,
error);
if (r < 0)
return r;
@@ -241,21 +284,18 @@ int bus_home_method_realize(
if (r < 0)
return r;
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
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);
+ r = home_create(h, secret, NULL, 0, error);
if (r < 0)
return r;
@@ -281,14 +321,11 @@ int bus_home_method_remove(
assert(message);
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.remove-home",
- NULL,
- true,
UID_INVALID,
- &h->manager->polkit_registry,
error);
if (r < 0)
return r;
@@ -354,14 +391,11 @@ int bus_home_method_authenticate(
if (r < 0)
return r;
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.authenticate-home",
- NULL,
- true,
h->uid,
- &h->manager->polkit_registry,
error);
if (r < 0)
return r;
@@ -382,7 +416,13 @@ int bus_home_method_authenticate(
return 1;
}
-int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) {
+int bus_home_update_record(
+ Home *h,
+ sd_bus_message *message,
+ UserRecord *hr,
+ Hashmap *blobs,
+ uint64_t flags,
+ sd_bus_error *error) {
int r;
assert(h);
@@ -393,21 +433,21 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *
if (r < 0)
return r;
- r = bus_verify_polkit_async(
+ if ((flags & ~SD_HOMED_UPDATE_FLAGS_ALL) != 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags provided.");
+
+ r = home_verify_polkit_async(
+ h,
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);
+ r = home_update(h, hr, blobs, flags, error);
if (r < 0)
return r;
@@ -418,6 +458,8 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *
if (r < 0)
return r;
+ h->current_operation->call_flags = flags;
+
return 1;
}
@@ -427,16 +469,28 @@ int bus_home_method_update(
sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ uint64_t flags = 0;
Home *h = ASSERT_PTR(userdata);
int r;
assert(message);
- 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|USER_RECORD_PERMISSIVE, &hr, error);
+ r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_PERMISSIVE, &hr, error);
if (r < 0)
return r;
- return bus_home_method_update_record(h, message, hr, error);
+ if (endswith(sd_bus_message_get_member(message), "Ex")) {
+ r = bus_message_read_blobs(message, &blobs, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "t", &flags);
+ if (r < 0)
+ return r;
+ }
+
+ return bus_home_update_record(h, message, hr, blobs, flags, error);
}
int bus_home_method_resize(
@@ -459,21 +513,18 @@ int bus_home_method_resize(
if (r < 0)
return r;
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
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, /* automatic= */ false, error);
+ r = home_resize(h, sz, secret, error);
if (r < 0)
return r;
@@ -506,14 +557,11 @@ int bus_home_method_change_password(
if (r < 0)
return r;
- r = bus_verify_polkit_async(
+ r = home_verify_polkit_async(
+ h,
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.passwd-home",
- NULL,
- true,
h->uid,
- &h->manager->polkit_registry,
error);
if (r < 0)
return r;
@@ -637,30 +685,38 @@ int bus_home_method_ref(
_cleanup_close_ int fd = -EBADF;
Home *h = ASSERT_PTR(userdata);
- HomeState state;
int please_suspend, r;
+ bool unrestricted;
assert(message);
+ /* In unrestricted mode we'll add a reference to the home even if it's not active */
+ unrestricted = strstr(sd_bus_message_get_member(message), "Unrestricted");
+
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:
- case HOME_DIRTY:
- 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;
+ if (!unrestricted) {
+ HomeState state;
+
+ state = home_get_state(h);
- return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ 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:
+ case HOME_DIRTY:
+ 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);
@@ -784,7 +840,12 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_ARGS("s", secret),
SD_BUS_NO_RESULT,
bus_home_method_activate,
- SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("ActivateIfReferenced",
+ SD_BUS_ARGS("s", secret),
+ SD_BUS_NO_RESULT,
+ bus_home_method_activate,
+ SD_BUS_VTABLE_UNPRIVILEGED|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_WITH_ARGS("Realize",
@@ -809,6 +870,11 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_NO_RESULT,
bus_home_method_update,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("UpdateEx",
+ SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
+ SD_BUS_NO_RESULT,
+ bus_home_method_update,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("Resize",
SD_BUS_ARGS("t", size, "s", secret),
SD_BUS_NO_RESULT,
@@ -835,6 +901,11 @@ const sd_bus_vtable home_vtable[] = {
SD_BUS_RESULT("h", send_fd),
bus_home_method_ref,
0),
+ SD_BUS_METHOD_WITH_ARGS("RefUnrestricted",
+ SD_BUS_ARGS("b", please_suspend),
+ SD_BUS_RESULT("h", send_fd),
+ bus_home_method_ref,
+ 0),
SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0),
SD_BUS_VTABLE_END
};
diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h
index 5522178..1644bc8 100644
--- a/src/home/homed-home-bus.h
+++ b/src/home/homed-home-bus.h
@@ -17,7 +17,6 @@ int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error
int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error);
-int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error);
int bus_home_method_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);
@@ -26,6 +25,8 @@ int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_erro
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);
+int bus_home_update_record(Home *home, sd_bus_message *message, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
+
extern const BusObjectImplementation home_object;
int bus_home_path(Home *h, char **ret);
diff --git a/src/home/homed-home.c b/src/home/homed-home.c
index 37b3270..757881c 100644
--- a/src/home/homed-home.c
+++ b/src/home/homed-home.c
@@ -10,6 +10,7 @@
#include "blockdev-util.h"
#include "btrfs-util.h"
+#include "build-path.h"
#include "bus-common-errors.h"
#include "bus-locator.h"
#include "data-fd-util.h"
@@ -33,12 +34,13 @@
#include "process-util.h"
#include "quota-util.h"
#include "resize-fs.h"
+#include "rm-rf.h"
#include "set.h"
#include "signal-util.h"
#include "stat-util.h"
#include "string-table.h"
#include "strv.h"
-#include "uid-alloc-range.h"
+#include "uid-classification.h"
#include "user-record-password-quality.h"
#include "user-record-sign.h"
#include "user-record-util.h"
@@ -54,7 +56,13 @@
assert_cc(HOME_UID_MIN <= HOME_UID_MAX);
assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1));
-static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret);
+static int home_start_work(
+ Home *h,
+ const char *verb,
+ UserRecord *hr,
+ UserRecord *secret,
+ Hashmap *blobs,
+ uint64_t flags);
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref);
@@ -96,7 +104,7 @@ static int suitable_home_record(UserRecord *hr) {
int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
_cleanup_(home_freep) Home *home = NULL;
- _cleanup_free_ char *nm = NULL, *ns = NULL;
+ _cleanup_free_ char *nm = NULL, *ns = NULL, *blob = NULL;
int r;
assert(m);
@@ -162,6 +170,13 @@ int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) {
if (r < 0)
return r;
+ blob = path_join(home_system_blob_dir(), hr->user_name);
+ if (!blob)
+ return -ENOMEM;
+ r = mkdir_safe(blob, 0755, 0, 0, MKDIR_IGNORE_EXISTING);
+ if (r < 0)
+ log_warning_errno(r, "Failed to create blob dir for user '%s': %m", home->user_name);
+
(void) bus_manager_emit_auto_login_changed(m);
(void) bus_home_emit_change(home);
(void) manager_schedule_rebalance(m, /* immediately= */ false);
@@ -322,7 +337,9 @@ int home_save_record(Home *h) {
}
int home_unlink_record(Home *h) {
+ _cleanup_free_ char *blob = NULL;
const char *fn;
+ int r;
assert(h);
@@ -334,6 +351,13 @@ int home_unlink_record(Home *h) {
if (unlink(fn) < 0 && errno != ENOENT)
return -errno;
+ blob = path_join(home_system_blob_dir(), h->user_name);
+ if (!blob)
+ return -ENOMEM;
+ r = rm_rf(blob, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
+ if (r < 0)
+ return r;
+
return 0;
}
@@ -402,11 +426,9 @@ static void home_maybe_stop_retry_deactivate(Home *h, HomeState state) {
/* Free the deactivation retry event source if we won't need it anymore. Specifically, we'll free the
* event source whenever the home directory is already deactivated (and we thus where successful) or
* if we start executing an operation that indicates that the home directory is going to be used or
- * operated on again. Also, if the home is referenced again stop the timer */
+ * operated on again. Also, if the home is referenced again stop the timer. */
- if (HOME_STATE_MAY_RETRY_DEACTIVATE(state) &&
- !h->ref_event_source_dont_suspend &&
- !h->ref_event_source_please_suspend)
+ if (HOME_STATE_MAY_RETRY_DEACTIVATE(state) && !home_is_referenced(h))
return;
h->retry_deactivate_event_source = sd_event_source_disable_unref(h->retry_deactivate_event_source);
@@ -454,7 +476,7 @@ static void home_start_retry_deactivate(Home *h) {
return;
/* If the home directory is being used now don't start the timer */
- if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+ if (home_is_referenced(h))
return;
r = sd_event_add_time_relative(
@@ -650,11 +672,17 @@ static int convert_worker_errno(Home *h, int e, sd_bus_error *error) {
return 0;
}
-static void home_count_bad_authentication(Home *h, bool save) {
+static void home_count_bad_authentication(Home *h, int error, bool save) {
int r;
assert(h);
+ if (!IN_SET(error,
+ -ENOKEY, /* Password incorrect */
+ -EBADSLT, /* Password incorrect and no token */
+ -EREMOTEIO)) /* Recovery key incorrect */
+ return;
+
r = user_record_bad_authentication(h->record);
if (r < 0) {
log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m");
@@ -680,8 +708,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
secret = TAKE_PTR(h->secret); /* Take possession */
if (ret < 0) {
- if (ret == -ENOKEY)
- (void) home_count_bad_authentication(h, false);
+ (void) home_count_bad_authentication(h, ret, /* save= */ false);
(void) convert_worker_errno(h, ret, &error);
r = log_error_errno(ret, "Fixation failed: %m");
@@ -717,7 +744,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) {
if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) {
- r = home_start_work(h, "activate", h->record, secret);
+ r = home_start_work(h, "activate", h->record, secret, NULL, 0);
if (r < 0) {
h->current_operation = operation_result_unref(h->current_operation, r, NULL);
home_set_state(h, _HOME_STATE_INVALID);
@@ -743,6 +770,27 @@ fail:
home_set_state(h, HOME_UNFIXATED);
}
+static bool error_is_bad_password(int ret) {
+ /* Tests for the various cases of bad passwords. We generally don't want to log so loudly about
+ * these, since everyone types in a bad password now and then. Moreover we usually try to start out
+ * with an empty set of passwords, so the first authentication will frequently fail, if not token is
+ * inserted. */
+
+ return IN_SET(ret,
+ -ENOKEY, /* Bad password, or insufficient */
+ -EBADSLT, /* Bad password, and no token */
+ -EREMOTEIO, /* Bad recovery key */
+ -ENOANO, /* PIN for security token needed */
+ -ERFKILL, /* "Protected Authentication Path" for token needed */
+ -EMEDIUMTYPE, /* Presence confirmation on token needed */
+ -ENOCSI, /* User verification on token needed */
+ -ENOSTR, /* Token action timeout */
+ -EOWNERDEAD, /* PIN locked of security token */
+ -ENOLCK, /* Bad PIN of security token */
+ -ETOOMANYREFS, /* Bad PIN and few tries left */
+ -EUCLEAN); /* Bad PIN and one try left */
+}
+
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;
@@ -751,11 +799,11 @@ static void home_activate_finish(Home *h, int ret, UserRecord *hr) {
assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
if (ret < 0) {
- if (ret == -ENOKEY)
- home_count_bad_authentication(h, true);
+ (void) home_count_bad_authentication(h, ret, /* save= */ true);
(void) convert_worker_errno(h, ret, &error);
- r = log_error_errno(ret, "Activation failed: %m");
+ r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR,
+ ret, "Activation failed: %s", bus_error_message(&error, ret));
goto finish;
}
@@ -907,31 +955,39 @@ static void home_create_finish(Home *h, int ret, UserRecord *hr) {
static void home_change_finish(Home *h, int ret, UserRecord *hr) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ uint64_t flags;
int r;
assert(h);
+ flags = h->current_operation ? h->current_operation->call_flags : 0;
+
if (ret < 0) {
- if (ret == -ENOKEY)
- (void) home_count_bad_authentication(h, true);
+ (void) home_count_bad_authentication(h, ret, /* save= */ true);
(void) convert_worker_errno(h, ret, &error);
- r = log_error_errno(ret, "Change operation failed: %m");
+ r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR,
+ ret, "Change operation failed: %s", bus_error_message(&error, ret));
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 {
+ if (!FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)) {
r = user_record_good_authentication(h->record);
if (r < 0)
log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m");
+ }
+ r = home_set_record(h, hr);
+ if (r >= 0)
r = home_save_record(h);
- if (r < 0)
- log_warning_errno(r, "Failed to write home record to disk, ignoring: %m");
+ if (r < 0) {
+ if (FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)) {
+ log_error_errno(r, "Failed to update home record and write it to disk: %m");
+ sd_bus_error_set(&error, SD_BUS_ERROR_FAILED, "Failed to cache changes to home record");
+ goto finish;
+ } else
+ log_warning_errno(r, "Failed to update home record, ignoring: %m");
}
}
@@ -982,11 +1038,11 @@ static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) {
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) home_count_bad_authentication(h, ret, /* save= */ true);
(void) convert_worker_errno(h, ret, &error);
- r = log_error_errno(ret, "Unlocking operation failed: %m");
+ r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR,
+ ret, "Unlocking operation failed: %s", bus_error_message(&error, ret));
/* Revert to locked state */
home_set_state(h, HOME_LOCKED);
@@ -1018,11 +1074,11 @@ static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) {
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) home_count_bad_authentication(h, ret, /* save= */ true);
(void) convert_worker_errno(h, ret, &error);
- r = log_error_errno(ret, "Authentication failed: %m");
+ r = log_full_errno(error_is_bad_password(ret) ? LOG_NOTICE : LOG_ERR,
+ ret, "Authentication failed: %s", bus_error_message(&error, ret));
goto finish;
}
@@ -1136,10 +1192,17 @@ static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void
return 0;
}
-static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) {
- _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+static int home_start_work(
+ Home *h,
+ const char *verb,
+ UserRecord *hr,
+ UserRecord *secret,
+ Hashmap *blobs,
+ uint64_t flags) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *fdmap = NULL;
_cleanup_(erase_and_freep) char *formatted = NULL;
_cleanup_close_ int stdin_fd = -EBADF, stdout_fd = -EBADF;
+ _cleanup_free_ int *blob_fds = NULL;
pid_t pid = 0;
int r;
@@ -1167,11 +1230,42 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
return r;
}
+ if (blobs) {
+ const char *blob_filename = NULL;
+ void *fd_ptr;
+ size_t i = 0;
+
+ blob_fds = new(int, hashmap_size(blobs));
+ if (!blob_fds)
+ return -ENOMEM;
+
+ /* homework needs to be able to tell the difference between blobs being null
+ * (the fdmap field is completely missing) and it being empty (the field is an
+ * empty object) */
+ r = json_variant_new_object(&fdmap, NULL, 0);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH_KEY(fd_ptr, blob_filename, blobs) {
+ blob_fds[i] = PTR_TO_FD(fd_ptr);
+
+ r = json_variant_set_field_integer(&fdmap, blob_filename, i);
+ if (r < 0)
+ return r;
+
+ i++;
+ }
+
+ r = json_variant_set_field(&v, HOMEWORK_BLOB_FDMAP_FIELD, fdmap);
+ if (r < 0)
+ return r;
+ }
+
r = json_variant_format(v, 0, &formatted);
if (r < 0)
return r;
- stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0);
+ stdin_fd = acquire_data_fd(formatted);
if (stdin_fd < 0)
return stdin_fd;
@@ -1183,13 +1277,14 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
r = safe_fork_full("(sd-homework)",
(int[]) { stdin_fd, stdout_fd, STDERR_FILENO },
- NULL, 0,
- FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
+ blob_fds, hashmap_size(blobs),
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_CLOEXEC_OFF|FORK_PACK_FDS|FORK_DEATHSIG_SIGTERM|
+ FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
if (r < 0)
return r;
if (r == 0) {
_cleanup_free_ char *joined = NULL;
- const char *homework, *suffix, *unix_path;
+ const char *suffix, *unix_path;
/* Child */
@@ -1225,16 +1320,21 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord
_exit(EXIT_FAILURE);
}
+ if (setenv("SYSTEMD_HOMEWORK_UPDATE_OFFLINE", one_zero(FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE)), 1) < 0) {
+ log_error_errno(errno, "Failed to set $SYSTEMD_HOMEWORK_UPDATE_OFFLINE: %m");
+ _exit(EXIT_FAILURE);
+ }
+
r = setenv_systemd_exec_pid(true);
if (r < 0)
log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m");
- /* Allow overriding the homework path via an environment variable, to make debugging
- * easier. */
- homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH;
+ r = setenv_systemd_log_level();
+ if (r < 0)
+ log_warning_errno(r, "Failed to update $SYSTEMD_LOG_LEVEL, ignoring: %m");
- execl(homework, homework, verb, NULL);
- log_error_errno(errno, "Failed to invoke %s: %m", homework);
+ r = invoke_callout_binary(SYSTEMD_HOMEWORK_PATH, STRV_MAKE(SYSTEMD_HOMEWORK_PATH, verb));
+ log_error_errno(r, "Failed to invoke %s: %m", SYSTEMD_HOMEWORK_PATH);
_exit(EXIT_FAILURE);
}
@@ -1298,9 +1398,10 @@ static int home_fixate_internal(
int r;
assert(h);
+ assert(secret);
assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE));
- r = home_start_work(h, "inspect", h->record, secret);
+ r = home_start_work(h, "inspect", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1318,6 +1419,7 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) {
int r;
assert(h);
+ assert(secret);
switch (home_get_state(h)) {
case HOME_ABSENT:
@@ -1345,9 +1447,10 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta
int r;
assert(h);
+ assert(secret);
assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE));
- r = home_start_work(h, "activate", h->record, secret);
+ r = home_start_work(h, "activate", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1355,10 +1458,14 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta
return 0;
}
-int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) {
+int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error) {
int r;
assert(h);
+ assert(secret);
+
+ if (if_referenced && !home_is_referenced(h))
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_REFERENCED, "Home %s is currently not referenced.", h->user_name);
switch (home_get_state(h)) {
case HOME_UNFIXATED:
@@ -1392,9 +1499,10 @@ static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for
int r;
assert(h);
+ assert(secret);
assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE));
- r = home_start_work(h, "inspect", h->record, secret);
+ r = home_start_work(h, "inspect", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1407,6 +1515,7 @@ int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) {
int r;
assert(h);
+ assert(secret);
state = home_get_state(h);
switch (state) {
@@ -1438,7 +1547,7 @@ static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) {
home_unpin(h); /* unpin so that we can deactivate */
- r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL);
+ r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL, NULL, 0);
if (r < 0)
/* Operation failed before it even started, reacquire pin fd, if state still dictates so */
home_update_pin_fd(h, _HOME_STATE_INVALID);
@@ -1475,10 +1584,11 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) {
return home_deactivate_internal(h, force, error);
}
-int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
+int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error) {
int r;
assert(h);
+ assert(secret);
switch (home_get_state(h)) {
case HOME_INACTIVE: {
@@ -1517,7 +1627,7 @@ int home_create(Home *h, UserRecord *secret, sd_bus_error *error) {
return r;
}
- r = home_start_work(h, "create", h->record, secret);
+ r = home_start_work(h, "create", h->record, secret, blobs, flags);
if (r < 0)
return r;
@@ -1547,7 +1657,7 @@ int home_remove(Home *h, sd_bus_error *error) {
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name);
}
- r = home_start_work(h, "remove", h->record, NULL);
+ r = home_start_work(h, "remove", h->record, NULL, NULL, 0);
if (r < 0)
return r;
@@ -1591,6 +1701,8 @@ static int home_update_internal(
const char *verb,
UserRecord *hr,
UserRecord *secret,
+ Hashmap *blobs,
+ uint64_t flags,
sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL;
@@ -1614,6 +1726,15 @@ static int home_update_internal(
secret = saved_secret;
}
+ if (blobs) {
+ const char *failed = NULL;
+ r = user_record_ensure_blob_manifest(hr, blobs, &failed);
+ if (r == -EINVAL)
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
+ }
+
r = manager_verify_user_record(h->manager, hr);
switch (r) {
@@ -1656,14 +1777,14 @@ static int home_update_internal(
return sd_bus_error_set(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing.");
}
- r = home_start_work(h, verb, new_hr, secret);
+ r = home_start_work(h, verb, new_hr, secret, blobs, flags);
if (r < 0)
return r;
return 0;
}
-int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
+int home_update(Home *h, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error) {
HomeState state;
int r;
@@ -1675,7 +1796,9 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
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);
+ if (!FLAGS_SET(flags, SD_HOMED_UPDATE_OFFLINE))
+ return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name);
+ break; /* offline updates are compatible w/ an absent home area */
case HOME_LOCKED:
return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name);
case HOME_INACTIVE:
@@ -1691,7 +1814,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
if (r < 0)
return r;
- r = home_update_internal(h, "update", hr, NULL, error);
+ r = home_update_internal(h, "update", hr, NULL, blobs, flags, error);
if (r < 0)
return r;
@@ -1702,7 +1825,6 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) {
int home_resize(Home *h,
uint64_t disk_size,
UserRecord *secret,
- bool automatic,
sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *c = NULL;
@@ -1778,7 +1900,7 @@ int home_resize(Home *h,
c = TAKE_PTR(signed_c);
}
- r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, error);
+ r = home_update_internal(h, "resize", c, secret, NULL, 0, error);
if (r < 0)
return r;
@@ -1815,6 +1937,8 @@ int home_passwd(Home *h,
int r;
assert(h);
+ assert(new_secret);
+ assert(old_secret);
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);
@@ -1892,7 +2016,7 @@ int home_passwd(Home *h,
return r;
}
- r = home_update_internal(h, "passwd", signed_c, merged_secret, error);
+ r = home_update_internal(h, "passwd", signed_c, merged_secret, NULL, 0, error);
if (r < 0)
return r;
@@ -1949,7 +2073,7 @@ int home_lock(Home *h, sd_bus_error *error) {
return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
}
- r = home_start_work(h, "lock", h->record, NULL);
+ r = home_start_work(h, "lock", h->record, NULL, NULL, 0);
if (r < 0)
return r;
@@ -1961,9 +2085,10 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state
int r;
assert(h);
+ assert(secret);
assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE));
- r = home_start_work(h, "unlock", h->record, secret);
+ r = home_start_work(h, "unlock", h->record, secret, NULL, 0);
if (r < 0)
return r;
@@ -1973,7 +2098,9 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state
int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) {
int r;
+
assert(h);
+ assert(secret);
r = home_ratelimit(h, error);
if (r < 0)
@@ -2085,23 +2212,11 @@ int home_killall(Home *h) {
if (r < 0)
return r;
if (r == 0) {
- gid_t gid;
-
/* Child */
- gid = user_record_gid(h->record);
- if (setresgid(gid, gid, gid) < 0) {
- log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid);
- _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);
+ r = fully_set_uid_gid(h->uid, user_record_gid(h->record), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0);
+ if (r < 0) {
+ log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", h->uid, user_record_gid(h->record));
_exit(EXIT_FAILURE);
}
@@ -2542,6 +2657,9 @@ int home_augment_status(
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))),
JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.Home")),
+ JSON_BUILD_PAIR("useFallback", JSON_BUILD_BOOLEAN(!HOME_STATE_IS_ACTIVE(state))),
+ JSON_BUILD_PAIR("fallbackShell", JSON_BUILD_CONST_STRING(BINDIR "/systemd-home-fallback-shell")),
+ JSON_BUILD_PAIR("fallbackHomeDirectory", JSON_BUILD_CONST_STRING("/")),
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)),
@@ -2602,7 +2720,7 @@ static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *u
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)
+ if (home_is_referenced(h))
return 0;
log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name);
@@ -2652,7 +2770,9 @@ int home_create_fifo(Home *h, bool please_suspend) {
(void) sd_event_source_set_description(*ss, "acquire-ref");
- r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1);
+ /* We need to notice dropped refs before we process new bus requests (which
+ * might try to obtain new refs) */
+ r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_NORMAL-10);
if (r < 0)
return r;
@@ -2690,7 +2810,8 @@ static int home_dispatch_acquire(Home *h, Operation *o) {
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);
- goto check;
+ operation_result(o, r, &error);
+ return 1;
case HOME_INACTIVE:
case HOME_DIRTY:
@@ -2721,7 +2842,6 @@ static int home_dispatch_acquire(Home *h, Operation *o) {
if (r >= 0)
r = call(h, o->secret, for_state, &error);
- check:
if (r != 0) /* failure or completed */
operation_result(o, r, &error);
else /* ongoing */
@@ -2730,6 +2850,19 @@ static int home_dispatch_acquire(Home *h, Operation *o) {
return 1;
}
+bool home_is_referenced(Home *h) {
+ assert(h);
+
+ return h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend;
+}
+
+bool home_shall_suspend(Home *h) {
+ assert(h);
+
+ /* Suspend if there's at least one client referencing this home directory that wants a suspend and none who does not. */
+ return h->ref_event_source_please_suspend && !h->ref_event_source_dont_suspend;
+}
+
static int home_dispatch_release(Home *h, Operation *o) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
int r;
@@ -2738,33 +2871,35 @@ static int home_dispatch_release(Home *h, Operation *o) {
assert(o);
assert(o->type == OPERATION_RELEASE);
- if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend)
+ if (home_is_referenced(h)) {
/* 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:
- case HOME_DIRTY:
- r = 1; /* 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:
- case HOME_LINGERING:
- 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;
- }
+ operation_result(o, r, &error);
+ return 1;
+ }
+
+ switch (home_get_state(h)) {
+
+ case HOME_UNFIXATED:
+ case HOME_ABSENT:
+ case HOME_INACTIVE:
+ case HOME_DIRTY:
+ r = 1; /* 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:
+ case HOME_LINGERING:
+ 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);
@@ -2875,7 +3010,7 @@ static int home_dispatch_pipe_eof(Home *h, Operation *o) {
assert(o);
assert(o->type == OPERATION_PIPE_EOF);
- if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend)
+ if (home_is_referenced(h))
return 1; /* Hmm, there's a reference again, let's cancel this */
switch (home_get_state(h)) {
@@ -3028,7 +3163,6 @@ int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) {
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;
@@ -3061,12 +3195,7 @@ static int home_get_image_path_seat(Home *h, char **ret) {
else if (r < 0)
return r;
- c = strdup(seat);
- if (!c)
- return -ENOMEM;
-
- *ret = TAKE_PTR(c);
- return 0;
+ return strdup_to(ret, seat);
}
int home_auto_login(Home *h, char ***ret_seats) {
diff --git a/src/home/homed-home.h b/src/home/homed-home.h
index 0f314aa..7d466cd 100644
--- a/src/home/homed-home.h
+++ b/src/home/homed-home.h
@@ -3,6 +3,7 @@
typedef struct Home Home;
+#include "hashmap.h"
#include "homed-manager.h"
#include "homed-operation.h"
#include "list.h"
@@ -185,21 +186,31 @@ 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_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error);
int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error);
int home_deactivate(Home *h, bool force, sd_bus_error *error);
-int home_create(Home *h, UserRecord *secret, sd_bus_error *error);
+int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
int home_remove(Home *h, sd_bus_error *error);
-int home_update(Home *h, UserRecord *new_record, sd_bus_error *error);
-int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, bool automatic, sd_bus_error *error);
+int home_update(Home *h, UserRecord *new_record, Hashmap *blobs, uint64_t flags, sd_bus_error *error);
+int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, 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);
+bool home_is_referenced(Home *h);
+bool home_shall_suspend(Home *h);
HomeState home_get_state(Home *h);
-int home_get_disk_status(Home *h, 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, statfs_f_type_t *ret_fstype, mode_t *ret_access_mode);
+int home_get_disk_status(
+ Home *h,
+ 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,
+ statfs_f_type_t *ret_fstype,
+ mode_t *ret_access_mode);
void home_process_notify(Home *h, char **l, int fd);
diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c
index 7cf5439..58cd037 100644
--- a/src/home/homed-manager-bus.c
+++ b/src/home/homed-manager-bus.c
@@ -6,6 +6,7 @@
#include "bus-common-errors.h"
#include "bus-polkit.h"
#include "format-util.h"
+#include "home-util.h"
#include "homed-bus.h"
#include "homed-home-bus.h"
#include "homed-manager-bus.h"
@@ -61,6 +62,53 @@ static int property_get_auto_login(
return sd_bus_message_close_container(reply);
}
+static int lookup_user_name(
+ Manager *m,
+ sd_bus_message *message,
+ const char *user_name,
+ sd_bus_error *error,
+ Home **ret) {
+
+ Home *h;
+ int r;
+
+ assert(m);
+ assert(message);
+ assert(user_name);
+ assert(ret);
+
+ if (isempty(user_name)) {
+ _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
+ uid_t uid;
+
+ /* If an empty user name is specified, then identify caller's EUID and find home by that. */
+
+ r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_creds_get_euid(creds, &uid);
+ if (r < 0)
+ return r;
+
+ h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid));
+ if (!h)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "Client's UID " UID_FMT " not managed.", uid);
+
+ } else {
+
+ if (!valid_user_group_name(user_name, 0))
+ 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);
+ }
+
+ *ret = h;
+ return 0;
+}
+
static int method_get_home_by_name(
sd_bus_message *message,
void *userdata,
@@ -77,12 +125,10 @@ static int method_get_home_by_name(
r = sd_bus_message_read(message, "s", &user_name);
if (r < 0)
return r;
- if (!valid_user_group_name(user_name, 0))
- 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 = lookup_user_name(m, message, user_name, error, &h);
+ if (r < 0)
+ return r;
r = bus_home_path(h, &path);
if (r < 0)
@@ -204,12 +250,10 @@ static int method_get_user_record_by_name(
r = sd_bus_message_read(message, "s", &user_name);
if (r < 0)
return r;
- if (!valid_user_group_name(user_name, 0))
- 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 = lookup_user_name(m, message, user_name, error, &h);
+ if (r < 0)
+ return r;
r = bus_home_get_record_json(h, message, &json, &incomplete);
if (r < 0)
@@ -274,16 +318,17 @@ static int generic_home_method(
Home *h;
int r;
+ assert(m);
+ assert(message);
+ assert(handler);
+
r = sd_bus_message_read(message, "s", &user_name);
if (r < 0)
return r;
- if (!valid_user_group_name(user_name, 0))
- 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 = lookup_user_name(m, message, user_name, error, &h);
+ if (r < 0)
+ return r;
return handler(message, h, error);
}
@@ -296,10 +341,8 @@ static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bu
return generic_home_method(userdata, message, bus_home_method_deactivate, error);
}
-static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) {
+static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs, Home **ret, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL;
- struct passwd *pw;
- struct group *gr;
bool signed_locally;
Home *other;
int r;
@@ -316,13 +359,26 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd
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)
+ r = getpwnam_malloc(hr->user_name, /* ret= */ NULL);
+ if (r >= 0)
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);
+ if (r != -ESRCH)
+ return r;
- gr = getgrnam(hr->user_name);
- if (gr)
+ r = getgrnam_malloc(hr->user_name, /* ret= */ NULL);
+ if (r >= 0)
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);
+ if (r != -ESRCH)
+ return r;
+
+ if (blobs) {
+ const char *failed = NULL;
+ r = user_record_ensure_blob_manifest(hr, blobs, &failed);
+ if (r == -EINVAL)
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest.");
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed));
+ }
r = manager_verify_user_record(m, hr);
switch (r) {
@@ -353,17 +409,24 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd
}
if (uid_is_valid(hr->uid)) {
+ _cleanup_free_ struct passwd *pw = NULL;
+ _cleanup_free_ struct group *gr = NULL;
+
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)
+ r = getpwuid_malloc(hr->uid, &pw);
+ if (r >= 0)
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);
+ if (r != -ESRCH)
+ return r;
- gr = getgrgid(hr->uid);
- if (gr)
+ r = getgrgid_malloc(hr->uid, &gr);
+ if (r >= 0)
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);
+ if (r != -ESRCH)
+ return r;
} else {
r = manager_augment_record_with_uid(m, hr);
if (r < 0)
@@ -396,11 +459,8 @@ static int method_register_home(
r = bus_verify_polkit_async(
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.create-home",
- NULL,
- true,
- UID_INVALID,
+ /* details= */ NULL,
&m->polkit_registry,
error);
if (r < 0)
@@ -408,7 +468,7 @@ static int method_register_home(
if (r == 0)
return 1; /* Will call us back */
- r = validate_and_allocate_home(m, hr, &h, error);
+ r = validate_and_allocate_home(m, hr, NULL, &h, error);
if (r < 0)
return r;
@@ -425,12 +485,11 @@ static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bu
return generic_home_method(userdata, message, bus_home_method_unregister, error);
}
-static int method_create_home(
- sd_bus_message *message,
- void *userdata,
- sd_bus_error *error) {
+static int method_create_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ uint64_t flags = 0;
Manager *m = ASSERT_PTR(userdata);
Home *h;
int r;
@@ -441,13 +500,22 @@ static int method_create_home(
if (r < 0)
return r;
+ if (endswith(sd_bus_message_get_member(message), "Ex")) {
+ r = bus_message_read_blobs(message, &blobs, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "t", &flags);
+ if (r < 0)
+ return r;
+ if ((flags & ~SD_HOMED_CREATE_FLAGS_ALL) != 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags provided.");
+ }
+
r = bus_verify_polkit_async(
message,
- CAP_SYS_ADMIN,
"org.freedesktop.home1.create-home",
- NULL,
- true,
- UID_INVALID,
+ /* details= */ NULL,
&m->polkit_registry,
error);
if (r < 0)
@@ -455,11 +523,11 @@ static int method_create_home(
if (r == 0)
return 1; /* Will call us back */
- r = validate_and_allocate_home(m, hr, &h, error);
+ r = validate_and_allocate_home(m, hr, blobs, &h, error);
if (r < 0)
return r;
- r = home_create(h, hr, error);
+ r = home_create(h, hr, blobs, flags, error);
if (r < 0)
goto fail;
@@ -471,6 +539,8 @@ static int method_create_home(
if (r < 0)
return r;
+ h->current_operation->call_flags = flags;
+
return 1;
fail:
@@ -497,6 +567,8 @@ static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_
static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
+ uint64_t flags = 0;
Manager *m = ASSERT_PTR(userdata);
Home *h;
int r;
@@ -507,13 +579,23 @@ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_er
if (r < 0)
return r;
+ if (endswith(sd_bus_message_get_member(message), "Ex")) {
+ r = bus_message_read_blobs(message, &blobs, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "t", &flags);
+ if (r < 0)
+ return r;
+ }
+
assert(hr->user_name);
h = hashmap_get(m->homes_by_name, hr->user_name);
if (!h)
return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name);
- return bus_home_method_update_record(h, message, hr, error);
+ return bus_home_update_record(h, message, hr, blobs, flags, error);
}
static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) {
@@ -557,10 +639,7 @@ static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus
HASHMAP_FOREACH(h, m->homes_by_name) {
- /* 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)
+ if (!home_shall_suspend(h))
continue;
if (!o) {
@@ -689,7 +768,12 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_ARGS("s", user_name, "s", secret),
SD_BUS_NO_RESULT,
method_activate_home,
- SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("ActivateHomeIfReferenced",
+ SD_BUS_ARGS("s", user_name, "s", secret),
+ SD_BUS_NO_RESULT,
+ method_activate_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("DeactivateHome",
SD_BUS_ARGS("s", user_name),
SD_BUS_NO_RESULT,
@@ -716,6 +800,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_NO_RESULT,
method_create_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("CreateHomeEx",
+ SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
+ SD_BUS_NO_RESULT,
+ method_create_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
/* Create $HOME for already registered JSON entry */
SD_BUS_METHOD_WITH_ARGS("RealizeHome",
@@ -751,6 +840,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_NO_RESULT,
method_update_home,
SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
+ SD_BUS_METHOD_WITH_ARGS("UpdateHomeEx",
+ SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags),
+ SD_BUS_NO_RESULT,
+ method_update_home,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE),
SD_BUS_METHOD_WITH_ARGS("ResizeHome",
SD_BUS_ARGS("s", user_name, "t", size, "s", secret),
@@ -795,6 +889,11 @@ static const sd_bus_vtable manager_vtable[] = {
SD_BUS_RESULT("h", send_fd),
method_ref_home,
0),
+ SD_BUS_METHOD_WITH_ARGS("RefHomeUnrestricted",
+ SD_BUS_ARGS("s", user_name, "b", please_suspend),
+ SD_BUS_RESULT("h", send_fd),
+ method_ref_home,
+ 0),
SD_BUS_METHOD_WITH_ARGS("ReleaseHome",
SD_BUS_ARGS("s", user_name),
SD_BUS_NO_RESULT,
diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c
index b8bef53..7669cbb 100644
--- a/src/home/homed-manager.c
+++ b/src/home/homed-manager.c
@@ -42,6 +42,7 @@
#include "quota-util.h"
#include "random-util.h"
#include "resize-fs.h"
+#include "rm-rf.h"
#include "socket-util.h"
#include "sort-util.h"
#include "stat-util.h"
@@ -79,6 +80,7 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, pat
static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata);
static int manager_gc_images(Manager *m);
+static int manager_gc_blob(Manager *m);
static int manager_enumerate_images(Manager *m);
static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name);
static void manager_revalidate_image(Manager *m, Home *h);
@@ -268,7 +270,7 @@ Manager* manager_free(Manager *m) {
(void) home_wait_for_worker(h);
m->bus = sd_bus_flush_close_unref(m->bus);
- m->polkit_registry = bus_verify_polkit_async_registry_free(m->polkit_registry);
+ m->polkit_registry = hashmap_free(m->polkit_registry);
m->device_monitor = sd_device_monitor_unref(m->device_monitor);
@@ -588,8 +590,8 @@ static int manager_acquire_uid(
assert(ret);
for (;;) {
- struct passwd *pw;
- struct group *gr;
+ _cleanup_free_ struct passwd *pw = NULL;
+ _cleanup_free_ struct group *gr = NULL;
uid_t candidate;
Home *other;
@@ -632,19 +634,27 @@ static int manager_acquire_uid(
continue;
}
- pw = getpwuid(candidate);
- if (pw) {
+ r = getpwuid_malloc(candidate, &pw);
+ if (r >= 0) {
log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.",
candidate, pw->pw_name);
continue;
}
+ if (r != -ESRCH) {
+ log_debug_errno(r, "Failed to check if an NSS user is already registered for candidate UID " UID_FMT ", assuming there might be: %m", candidate);
+ continue;
+ }
- gr = getgrgid((gid_t) candidate);
- if (gr) {
+ r = getgrgid_malloc((gid_t) candidate, &gr);
+ if (r >= 0) {
log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.",
candidate, gr->gr_name);
continue;
}
+ if (r != -ESRCH) {
+ log_debug_errno(r, "Failed to check if an NSS group is already registered for candidate UID " UID_FMT ", assuming there might be: %m", candidate);
+ continue;
+ }
r = search_ipc(candidate, (gid_t) candidate);
if (r < 0)
@@ -715,7 +725,7 @@ static int manager_add_home_by_image(
}
} else {
/* Check NSS, in case there's another user or group by this name */
- if (getpwnam(user_name) || getgrnam(user_name)) {
+ if (getpwnam_malloc(user_name, /* ret= */ NULL) >= 0 || getgrnam_malloc(user_name, /* ret= */ NULL) >= 0) {
log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path);
return 0;
}
@@ -903,7 +913,7 @@ static int manager_assess_image(
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);
+ return log_warning_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path);
if (r > 0)
storage = USER_SUBVOLUME;
else {
@@ -981,7 +991,7 @@ static int manager_connect_bus(Manager *m) {
if (r < 0)
return log_error_errno(r, "Failed to request name: %m");
- r = sd_bus_attach_event(m->bus, m->event, 0);
+ r = sd_bus_attach_event(m->bus, m->event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach bus to event loop: %m");
@@ -998,7 +1008,7 @@ static int manager_bind_varlink(Manager *m) {
assert(m);
assert(!m->varlink_server);
- r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA);
+ r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID|VARLINK_SERVER_INHERIT_USERDATA|VARLINK_SERVER_INPUT_SENSITIVE);
if (r < 0)
return log_error_errno(r, "Failed to allocate varlink server object: %m");
@@ -1044,7 +1054,7 @@ static int manager_bind_varlink(Manager *m) {
/* Avoid recursion */
if (setenv("SYSTEMD_BYPASS_USERDB", m->userdb_service, 1) < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m");
+ return log_error_errno(errno, "Failed to set $SYSTEMD_BYPASS_USERDB: %m");
return 0;
}
@@ -1355,8 +1365,11 @@ static int manager_enumerate_devices(Manager *m) {
if (r < 0)
return r;
- FOREACH_DEVICE(e, d)
+ FOREACH_DEVICE(e, d) {
+ if (device_is_processed(d) <= 0)
+ continue;
(void) manager_add_device(m, d);
+ }
return 0;
}
@@ -1428,7 +1441,7 @@ static int manager_generate_key_pair(Manager *m) {
/* 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 to open key file for writing: %m");
+ return log_error_errno(r, "Failed to 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.");
@@ -1442,7 +1455,7 @@ static int manager_generate_key_pair(Manager *m) {
/* 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 to open key file for writing: %m");
+ return log_error_errno(r, "Failed to 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.");
@@ -1619,6 +1632,9 @@ int manager_startup(Manager *m) {
/* Let's clean up home directories whose devices got removed while we were not running */
(void) manager_enqueue_gc(m, NULL);
+ /* Let's clean up blob directories for home dirs that no longer exist */
+ (void) manager_gc_blob(m);
+
return 0;
}
@@ -1707,6 +1723,29 @@ int manager_gc_images(Manager *m) {
return 0;
}
+static int manager_gc_blob(Manager *m) {
+ _cleanup_closedir_ DIR *d = NULL;
+ int r;
+
+ assert(m);
+
+ d = opendir(home_system_blob_dir());
+ if (!d) {
+ if (errno == ENOENT)
+ return 0;
+ return log_error_errno(errno, "Failed to open %s: %m", home_system_blob_dir());
+ }
+
+ FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read system blob directory: %m"))
+ if (!hashmap_contains(m->homes_by_name, de->d_name)) {
+ r = rm_rf_at(dirfd(d), de->d_name, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME);
+ if (r < 0)
+ log_warning_errno(r, "Failed to delete blob dir for missing user '%s', ignoring: %m", de->d_name);
+ }
+
+ return 0;
+}
+
static int on_deferred_rescan(sd_event_source *s, void *userdata) {
Manager *m = ASSERT_PTR(userdata);
@@ -1989,7 +2028,7 @@ static int manager_rebalance_apply(Manager *m) {
h->rebalance_pending = false;
- r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, /* automatic= */ true, &error);
+ r = home_resize(h, h->rebalance_goal, /* secret= */ NULL, &error);
if (r < 0)
log_warning_errno(r, "Failed to resize home '%s' for rebalancing, ignoring: %s",
h->user_name, bus_error_message(&error, r));
diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h
index 004246a..af165bb 100644
--- a/src/home/homed-operation.h
+++ b/src/home/homed-operation.h
@@ -39,6 +39,7 @@ typedef struct Operation {
sd_bus_message *message;
UserRecord *secret;
+ uint64_t call_flags; /* flags passed into UpdateEx() or CreateHomeEx() */
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 */
diff --git a/src/home/homed.c b/src/home/homed.c
index 04d9b56..cfb498e 100644
--- a/src/home/homed.c
+++ b/src/home/homed.c
@@ -29,7 +29,7 @@ static int run(int argc, char *argv[]) {
umask(0022);
- assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGRTMIN+18, -1) >= 0);
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGRTMIN+18) >= 0);
r = manager_new(&m);
if (r < 0)
diff --git a/src/home/homework-blob.c b/src/home/homework-blob.c
new file mode 100644
index 0000000..6b22ab6
--- /dev/null
+++ b/src/home/homework-blob.c
@@ -0,0 +1,301 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "copy.h"
+#include "fileio.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "home-util.h"
+#include "homework-blob.h"
+#include "homework.h"
+#include "install-file.h"
+#include "macro.h"
+#include "path-util.h"
+#include "recurse-dir.h"
+#include "rm-rf.h"
+#include "sha256.h"
+#include "string-util.h"
+#include "tmpfile-util.h"
+#include "umask-util.h"
+#include "utf8.h"
+
+static int copy_one_blob(
+ int src_fd,
+ int dest_dfd,
+ const char *name,
+ uint64_t *total_size,
+ uid_t uid,
+ Hashmap *manifest) {
+ _cleanup_(unlink_and_freep) char *dest_tmpname = NULL;
+ _cleanup_close_ int dest = -EBADF;
+ uint8_t hash[SHA256_DIGEST_SIZE], *known_hash;
+ off_t initial, size;
+ int r;
+
+ assert(src_fd >= 0);
+ assert(dest_dfd >= 0);
+ assert(name);
+ assert(total_size);
+ assert(uid_is_valid(uid));
+ assert(manifest);
+
+ if (!suitable_blob_filename(name)) {
+ log_warning("Blob %s has invalid filename. Skipping.", name);
+ return 0;
+ }
+
+ known_hash = hashmap_get(manifest, name);
+ if (!known_hash) {
+ log_warning("Blob %s is missing from manifest. Skipping.", name);
+ return 0;
+ }
+
+ r = fd_verify_regular(src_fd);
+ if (r < 0) {
+ log_warning_errno(r, "Blob %s is not a regular file. Skipping.", name);
+ return 0;
+ }
+
+ initial = lseek(src_fd, 0, SEEK_CUR);
+ if (initial < 0)
+ return log_debug_errno(errno, "Failed to get initial pos on fd for blob %s: %m", name);
+ if (initial > 0)
+ log_debug("Blob %s started offset %s into file", name, FORMAT_BYTES(initial));
+
+ /* Hashing is relatively cheaper compared to copying, especially since we're possibly copying across
+ * filesystems or even devices here. So first we check the hash and bail early if the file's contents
+ * don't match what's in the manifest. */
+
+ r = sha256_fd(src_fd, BLOB_DIR_MAX_SIZE, hash);
+ if (r == -EFBIG)
+ return log_warning_errno(r, "Blob %s is larger than blob directory size limit. Not copying any further.", name);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to compute sha256 for blob %s: %m", name);
+ if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) {
+ log_warning("Blob %s has incorrect hash. Skipping.", name);
+ return 0;
+ }
+
+ size = lseek(src_fd, 0, SEEK_CUR);
+ if (size < 0)
+ return log_debug_errno(errno, "Failed to get final pos on fd for blob %s: %m", name);
+ if (!DEC_SAFE(&size, initial))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid seek position on fd for %s. Couldn't get size.", name);
+
+ if (!INC_SAFE(total_size, size))
+ *total_size = UINT64_MAX;
+ log_debug("Blob %s size is %s, making the new total dir size %s", name, FORMAT_BYTES(size),
+ *total_size != UINT64_MAX ? FORMAT_BYTES(*total_size) : "overflow!");
+ if (*total_size > BLOB_DIR_MAX_SIZE)
+ return log_warning_errno(SYNTHETIC_ERRNO(EFBIG),
+ "Blob %s will cause blob directory to exceed its size limit. Not copying any further.", name);
+
+ /* Next we copy but don't yet link the file into the blob directory */
+
+ if (lseek(src_fd, initial, SEEK_SET) < 0)
+ return log_debug_errno(errno, "Failed to rewind fd for blob %s: %m", name);
+
+ dest = open_tmpfile_linkable_at(dest_dfd, name, O_RDWR|O_CLOEXEC, &dest_tmpname);
+ if (dest < 0)
+ return log_debug_errno(dest, "Failed to create dest tmpfile for blob %s: %m", name);
+
+ if (fchmod(dest, 0644) < 0)
+ return log_debug_errno(errno, "Failed to chmod blob %s: %m", name);
+ if (fchown(dest, uid, uid) < 0)
+ return log_debug_errno(errno, "Failed to chown blob %s: %m", name);
+
+ r = copy_bytes(src_fd, dest, BLOB_DIR_MAX_SIZE, 0);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to copy blob %s: %m", name);
+
+ /* The source FD might have changed while we were busy copying, thus invalidating the hash.
+ * So, we re-hash the data we just copied to make sure that this didn't happen. */
+
+ if (lseek(dest, 0, SEEK_SET) < 0)
+ return log_debug_errno(errno, "Failed to rewind blob %s for rehash: %m", name);
+
+ r = sha256_fd(dest, BLOB_DIR_MAX_SIZE, hash);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to rehash blob %s: %m", name);
+ if (memcmp(hash, known_hash, SHA256_DIGEST_SIZE) != 0) {
+ log_warning("Blob %s has changed while we were copying it. Skipping.", name);
+ return 0;
+ }
+
+ /* The file's contents still match the blob manifest, so it's safe to expose it in the directory */
+
+ r = link_tmpfile_at(dest, dest_dfd, dest_tmpname, name, 0);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to link blob %s: %m", name);
+ dest_tmpname = mfree(dest_tmpname);
+
+ return 0;
+}
+
+static int replace_blob_at(
+ int src_base_dfd,
+ const char *src_name,
+ int dest_base_dfd,
+ const char *dest_name,
+ Hashmap *manifest,
+ mode_t mode,
+ uid_t uid) {
+ _cleanup_free_ char *fn = NULL;
+ _cleanup_close_ int src_dfd = -EBADF, dest_dfd = -EBADF;
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ uint64_t total_size = 0;
+ int r;
+
+ assert(src_base_dfd >= 0);
+ assert(src_name);
+ assert(dest_base_dfd >= 0);
+ assert(dest_name);
+ assert(uid_is_valid(uid));
+
+ src_dfd = openat(src_base_dfd, src_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (src_dfd < 0) {
+ if (errno == ENOENT)
+ return 0;
+ return log_debug_errno(errno, "Failed to open src blob dir: %m");
+ }
+
+ r = tempfn_random(dest_name, NULL, &fn);
+ if (r < 0)
+ return r;
+
+ dest_dfd = open_mkdir_at(dest_base_dfd, fn, O_EXCL|O_CLOEXEC, mode);
+ if (dest_dfd < 0)
+ return log_debug_errno(dest_dfd, "Failed to create/open dest blob dir: %m");
+
+ r = readdir_all(src_dfd, RECURSE_DIR_SORT, &de);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to read src blob dir: %m");
+ goto fail;
+ }
+ for (size_t i = 0; i < de->n_entries; i++) {
+ const char *name = de->entries[i]->d_name;
+ _cleanup_close_ int src_fd = -EBADF;
+
+ src_fd = openat(src_dfd, name, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW);
+ if (src_fd < 0) {
+ r = log_debug_errno(errno, "Failed to open %s in src blob dir: %m", name);
+ goto fail;
+ }
+
+ r = copy_one_blob(src_fd, dest_dfd, name, &total_size, uid, manifest);
+ if (r == -EFBIG)
+ break;
+ if (r < 0)
+ goto fail;
+ }
+
+ if (fchown(dest_dfd, uid, uid) < 0) {
+ r = log_debug_errno(errno, "Failed to chown dest blob dir: %m");
+ goto fail;
+ }
+
+ r = install_file(dest_base_dfd, fn, dest_base_dfd, dest_name, INSTALL_REPLACE);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to move dest blob dir into place: %m");
+ goto fail;
+ }
+
+ return 0;
+
+fail:
+ (void) rm_rf_at(dest_base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
+ return r;
+}
+
+int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled) {
+ _cleanup_close_ int sys_base_dfd = -EBADF;
+ int r;
+
+ assert(h);
+ assert(root_fd >= 0);
+ assert(reconciled >= 0);
+
+ if (reconciled == USER_RECONCILE_IDENTICAL)
+ return 0;
+
+ sys_base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (sys_base_dfd < 0)
+ return log_error_errno(errno, "Failed to open system blob dir: %m");
+
+ if (reconciled == USER_RECONCILE_HOST_WON) {
+ r = replace_blob_at(sys_base_dfd, h->user_name, root_fd, ".identity-blob",
+ h->blob_manifest, 0700, h->uid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to replace embedded blobs with system blobs: %m");
+
+ log_info("Replaced embedded blob dir with contents of system blob dir.");
+ } else {
+ assert(reconciled == USER_RECONCILE_EMBEDDED_WON);
+
+ r = replace_blob_at(root_fd, ".identity-blob", sys_base_dfd, h->user_name,
+ h->blob_manifest, 0755, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to replace system blobs with embedded blobs: %m");
+
+ log_info("Replaced system blob dir with contents of embedded blob dir.");
+ }
+ return 0;
+}
+
+int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs) {
+ _cleanup_free_ char *fn = NULL;
+ _cleanup_close_ int base_dfd = -EBADF, dfd = -EBADF;
+ uint64_t total_size = 0;
+ const char *filename;
+ const void *v;
+ int r;
+
+ assert(h);
+
+ if (!blobs) /* Shortcut: If no blobs are passed from dbus, we have nothing to do. */
+ return 0;
+
+ base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (base_dfd < 0)
+ return log_error_errno(errno, "Failed to open system blob base dir: %m");
+
+ if (hashmap_isempty(blobs)) {
+ /* Shortcut: If blobs was passed but empty, we can simply delete the contents
+ * of the directory. */
+ r = rm_rf_at(base_dfd, h->user_name, REMOVE_PHYSICAL|REMOVE_MISSING_OK);
+ if (r < 0)
+ return log_error_errno(r, "Failed to empty out system blob dir: %m");
+ return 0;
+ }
+
+ r = tempfn_random(h->user_name, NULL, &fn);
+ if (r < 0)
+ return r;
+
+ dfd = open_mkdir_at(base_dfd, fn, O_EXCL|O_CLOEXEC, 0755);
+ if (dfd < 0)
+ return log_error_errno(errno, "Failed to create system blob dir: %m");
+
+ HASHMAP_FOREACH_KEY(v, filename, blobs) {
+ r = copy_one_blob(PTR_TO_FD(v), dfd, filename, &total_size, 0, h->blob_manifest);
+ if (r == -EFBIG)
+ break;
+ if (r < 0) {
+ log_error_errno(r, "Failed to copy %s into system blob dir: %m", filename);
+ goto fail;
+ }
+ }
+
+ r = install_file(base_dfd, fn, base_dfd, h->user_name, INSTALL_REPLACE);
+ if (r < 0) {
+ log_error_errno(r, "Failed to move system blob dir into place: %m");
+ goto fail;
+ }
+
+ log_info("Replaced system blob directory.");
+ return 0;
+
+fail:
+ (void) rm_rf_at(base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK);
+ return r;
+}
diff --git a/src/home/homework-blob.h b/src/home/homework-blob.h
new file mode 100644
index 0000000..fbe6c82
--- /dev/null
+++ b/src/home/homework-blob.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+#include "user-record.h"
+
+int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled);
+
+int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs);
diff --git a/src/home/homework-cifs.c b/src/home/homework-cifs.c
index 5d87131..eb87b37 100644
--- a/src/home/homework-cifs.c
+++ b/src/home/homework-cifs.c
@@ -76,7 +76,7 @@ int home_setup_cifs(
pid_t mount_pid;
int exit_status;
- passwd_fd = acquire_data_fd(*pw, strlen(*pw), /* flags= */ 0);
+ passwd_fd = acquire_data_fd(*pw);
if (passwd_fd < 0)
return log_error_errno(passwd_fd, "Failed to create data FD for password: %m");
@@ -94,7 +94,7 @@ int home_setup_cifs(
r = setenvf("PASSWD_FD", /* overwrite= */ true, "%d", passwd_fd);
if (r < 0) {
- log_error_errno(errno, "Failed to set $PASSWD_FD: %m");
+ log_error_errno(r, "Failed to set $PASSWD_FD: %m");
_exit(EXIT_FAILURE);
}
diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c
index 6870ae9..ff88367 100644
--- a/src/home/homework-directory.c
+++ b/src/home/homework-directory.c
@@ -4,6 +4,7 @@
#include "btrfs-util.h"
#include "fd-util.h"
+#include "homework-blob.h"
#include "homework-directory.h"
#include "homework-mount.h"
#include "homework-quota.h"
@@ -265,7 +266,7 @@ int home_resize_directory(
UserRecord **ret_home) {
_cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
- int r;
+ int r, reconciled;
assert(h);
assert(setup);
@@ -276,9 +277,9 @@ int home_resize_directory(
if (r < 0)
return r;
- r = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
- if (r < 0)
- return r;
+ reconciled = home_load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
+ if (reconciled < 0)
+ return reconciled;
r = home_maybe_shift_uid(h, flags, setup);
if (r < 0)
@@ -290,7 +291,11 @@ int home_resize_directory(
if (r < 0)
return r;
- r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
diff --git a/src/home/homework-fscrypt.c b/src/home/homework-fscrypt.c
index 6aae1d2..92ce5c3 100644
--- a/src/home/homework-fscrypt.c
+++ b/src/home/homework-fscrypt.c
@@ -12,6 +12,7 @@
#include "homework-fscrypt.h"
#include "homework-mount.h"
#include "homework-quota.h"
+#include "keyring-util.h"
#include "memory-util.h"
#include "missing_keyctl.h"
#include "missing_syscall.h"
@@ -29,6 +30,98 @@
#include "user-util.h"
#include "xattr-util.h"
+static int fscrypt_unlink_key(UserRecord *h) {
+ _cleanup_free_ void *keyring = NULL;
+ size_t keyring_size = 0, n_keys = 0;
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_FSCRYPT);
+
+ r = fully_set_uid_gid(
+ h->uid,
+ user_record_gid(h),
+ /* supplementary_gids= */ NULL,
+ /* n_supplementary_gids= */ 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m",
+ h->uid, user_record_gid(h));
+
+ r = keyring_read(KEY_SPEC_USER_KEYRING, &keyring, &keyring_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read the keyring of user " UID_FMT ": %m", h->uid);
+
+ n_keys = keyring_size / sizeof(key_serial_t);
+ assert(keyring_size % sizeof(key_serial_t) == 0);
+
+ /* Find any key with a description starting with 'fscrypt:' and unlink it. We need to iterate as we
+ * store the key with a description that uses the hash of the secret key, that we do not have when
+ * we are deactivating. */
+ FOREACH_ARRAY(key, ((key_serial_t *) keyring), n_keys) {
+ _cleanup_free_ char *description = NULL;
+ char *d;
+
+ r = keyring_describe(*key, &description);
+ if (r < 0) {
+ if (r == -ENOKEY) /* Something else deleted it already, that's ok. */
+ continue;
+
+ return log_error_errno(r, "Failed to describe key id %d: %m", *key);
+ }
+
+ /* The description is the final element as per manpage. */
+ d = strrchr(description, ';');
+ if (!d)
+ return log_error_errno(
+ SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse description of key id %d: %s",
+ *key,
+ description);
+
+ if (!startswith(d + 1, "fscrypt:"))
+ continue;
+
+ r = keyctl(KEYCTL_UNLINK, *key, KEY_SPEC_USER_KEYRING, 0, 0);
+ if (r < 0) {
+ if (errno == ENOKEY) /* Something else deleted it already, that's ok. */
+ continue;
+
+ return log_error_errno(
+ errno,
+ "Failed to delete encryption key with id '%d' from the keyring of user " UID_FMT ": %m",
+ *key,
+ h->uid);
+ }
+
+ log_debug("Deleted encryption key with id '%d' from the keyring of user " UID_FMT ".", *key, h->uid);
+ }
+
+ return 0;
+}
+
+int home_flush_keyring_fscrypt(UserRecord *h) {
+ int r;
+
+ assert(h);
+ assert(user_record_storage(h) == USER_FSCRYPT);
+
+ if (!uid_is_valid(h->uid))
+ return 0;
+
+ r = safe_fork("(sd-delkey)",
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_REOPEN_LOG,
+ NULL);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ if (fscrypt_unlink_key(h) < 0)
+ _exit(EXIT_FAILURE);
+ _exit(EXIT_SUCCESS);
+ }
+
+ return 0;
+}
+
static int fscrypt_upload_volume_key(
const uint8_t key_descriptor[static FS_KEY_DESCRIPTOR_SIZE],
const void *volume_key,
@@ -131,7 +224,7 @@ static int fscrypt_slot_try_one(
salt, salt_size,
0xFFFF, EVP_sha512(),
sizeof(derived), derived) != 1)
- return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed");
+ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed.");
context = EVP_CIPHER_CTX_new();
if (!context)
@@ -212,14 +305,13 @@ static int fscrypt_setup(
r = flistxattr_malloc(setup->root_fd, &xattr_buf);
if (r < 0)
- return log_error_errno(errno, "Failed to retrieve xattr list: %m");
+ return log_error_errno(r, "Failed to retrieve xattr list: %m");
NULSTR_FOREACH(xa, xattr_buf) {
_cleanup_free_ void *salt = NULL, *encrypted = NULL;
_cleanup_free_ char *value = NULL;
size_t salt_size, encrypted_size;
const char *nr, *e;
- char **list;
int n;
/* Check if this xattr has the format 'trusted.fscrypt_slot<nr>' where '<nr>' is a 32-bit unsigned integer */
@@ -237,31 +329,30 @@ static int fscrypt_setup(
e = memchr(value, ':', n);
if (!e)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator: %m", xa);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "xattr %s lacks ':' separator.", xa);
- r = unbase64mem(value, e - value, &salt, &salt_size);
+ r = unbase64mem_full(value, e - value, /* secure = */ false, &salt, &salt_size);
if (r < 0)
return log_error_errno(r, "Failed to decode salt of %s: %m", xa);
- r = unbase64mem(e+1, n - (e - value) - 1, &encrypted, &encrypted_size);
+
+ r = unbase64mem_full(e + 1, n - (e - value) - 1, /* secure = */ false, &encrypted, &encrypted_size);
if (r < 0)
return log_error_errno(r, "Failed to decode encrypted key of %s: %m", xa);
r = -ENOANO;
- FOREACH_POINTER(list, cache->pkcs11_passwords, cache->fido2_passwords, password) {
+ char **list;
+ FOREACH_ARGUMENT(list, cache->pkcs11_passwords, cache->fido2_passwords, password) {
r = fscrypt_slot_try_many(
list,
salt, salt_size,
encrypted, encrypted_size,
setup->fscrypt_key_descriptor,
ret_volume_key, ret_volume_key_size);
- if (r != -ENOANO)
- break;
- }
- if (r < 0) {
+ if (r >= 0)
+ return 0;
if (r != -ENOANO)
return r;
- } else
- return 0;
+ }
}
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords.");
@@ -319,23 +410,11 @@ int home_setup_fscrypt(
if (r < 0)
return log_error_errno(r, "Failed install encryption key in user's keyring: %m");
if (r == 0) {
- gid_t gid;
-
/* Child */
- gid = user_record_gid(h);
- if (setresgid(gid, gid, gid) < 0) {
- log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", gid);
- _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);
+ r = fully_set_uid_gid(h->uid, user_record_gid(h), /* supplementary_gids= */ NULL, /* n_supplementary_gids= */ 0);
+ if (r < 0) {
+ log_error_errno(r, "Failed to change UID/GID to " UID_FMT "/" GID_FMT ": %m", h->uid, user_record_gid(h));
_exit(EXIT_FAILURE);
}
@@ -649,7 +728,7 @@ int home_passwd_fscrypt(
r = flistxattr_malloc(setup->root_fd, &xattr_buf);
if (r < 0)
- return log_error_errno(errno, "Failed to retrieve xattr list: %m");
+ return log_error_errno(r, "Failed to retrieve xattr list: %m");
NULSTR_FOREACH(xa, xattr_buf) {
const char *nr;
diff --git a/src/home/homework-fscrypt.h b/src/home/homework-fscrypt.h
index 7c2d7aa..289e9d8 100644
--- a/src/home/homework-fscrypt.h
+++ b/src/home/homework-fscrypt.h
@@ -9,3 +9,5 @@ int home_setup_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cac
int home_create_fscrypt(UserRecord *h, HomeSetup *setup, char **effective_passwords, UserRecord **ret_home);
int home_passwd_fscrypt(UserRecord *h, HomeSetup *setup, const PasswordCache *cache, char **effective_passwords);
+
+int home_flush_keyring_fscrypt(UserRecord *h);
diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c
index 5bd78a0..20ff4c3 100644
--- a/src/home/homework-luks.c
+++ b/src/home/homework-luks.c
@@ -33,6 +33,7 @@
#include "glyph-util.h"
#include "gpt.h"
#include "home-util.h"
+#include "homework-blob.h"
#include "homework-luks.h"
#include "homework-mount.h"
#include "io-util.h"
@@ -125,7 +126,6 @@ static int probe_file_system_by_fd(
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;
@@ -167,13 +167,10 @@ static int probe_file_system_by_fd(
if (r < 0)
return r;
- s = strdup(fstype);
- if (!s)
- return -ENOMEM;
-
- *ret_fstype = TAKE_PTR(s);
+ r = strdup_to(ret_fstype, fstype);
+ if (r < 0)
+ return r;
*ret_uuid = id;
-
return 0;
}
@@ -199,7 +196,7 @@ static int block_get_size_by_fd(int fd, uint64_t *ret) {
if (!S_ISBLK(st.st_mode))
return -ENOTBLK;
- return RET_NERRNO(ioctl(fd, BLKGETSIZE64, ret));
+ return blockdev_get_device_size(fd, ret);
}
static int block_get_size_by_path(const char *path, uint64_t *ret) {
@@ -259,43 +256,30 @@ static int run_fsck(const char *node, const char *fstype) {
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(key_serial_t, keyring_unlink, -1);
-static int upload_to_keyring(
- UserRecord *h,
- const char *password,
- key_serial_t *ret_key_serial) {
+static int upload_to_keyring(UserRecord *h, const void *vk, size_t vks, key_serial_t *ret) {
_cleanup_free_ char *name = NULL;
key_serial_t serial;
assert(h);
- assert(password);
-
- /* If auto-shrink-on-logout is turned on, we need to keep the key we used to unlock the LUKS volume
- * around, since we'll need it when automatically resizing (since we can't ask the user there
- * again). We do this by uploading it into the kernel keyring, specifically the "session" one. This
- * is done under the assumption systemd-homed gets its private per-session keyring (i.e. default
- * service behaviour, given that KeyringMode=private is the default). It will survive between our
- * systemd-homework invocations that way.
- *
- * If auto-shrink-on-logout is disabled we'll skip this step, to be frugal with sensitive data. */
-
- if (user_record_auto_resize_mode(h) != AUTO_RESIZE_SHRINK_AND_GROW) { /* Won't need it */
- if (ret_key_serial)
- *ret_key_serial = -1;
- return 0;
- }
+ assert(vk);
+ assert(vks > 0);
+
+ /* We upload the LUKS volume key into the kernel session keyring, under the assumption that
+ * systemd-homed gets its own private session keyring (i.e. the default service behavior, given
+ * that KeyringMode=private is the default). That way, the key will survive between invocations
+ * of systemd-homework. */
name = strjoin("homework-user-", h->user_name);
if (!name)
return -ENOMEM;
- serial = add_key("user", name, password, strlen(password), KEY_SPEC_SESSION_KEYRING);
+ serial = add_key("user", name, vk, vks, KEY_SPEC_SESSION_KEYRING);
if (serial == -1)
return -errno;
- if (ret_key_serial)
- *ret_key_serial = serial;
-
+ if (ret)
+ *ret = serial;
return 1;
}
@@ -304,13 +288,14 @@ static int luks_try_passwords(
struct crypt_device *cd,
char **passwords,
void *volume_key,
- size_t *volume_key_size,
- key_serial_t *ret_key_serial) {
+ size_t *volume_key_size) {
int r;
assert(h);
assert(cd);
+ assert(volume_key);
+ assert(volume_key_size);
STRV_FOREACH(pp, passwords) {
size_t vks = *volume_key_size;
@@ -323,16 +308,6 @@ static int luks_try_passwords(
*pp,
strlen(*pp));
if (r >= 0) {
- if (ret_key_serial) {
- /* If ret_key_serial is non-NULL, let's try to upload the password that
- * worked, and return its serial. */
- r = upload_to_keyring(h, *pp, ret_key_serial);
- if (r < 0) {
- log_debug_errno(r, "Failed to upload LUKS password to kernel keyring, ignoring: %m");
- *ret_key_serial = -1;
- }
- }
-
*volume_key_size = vks;
return 0;
}
@@ -343,6 +318,66 @@ static int luks_try_passwords(
return -ENOKEY;
}
+static int luks_get_volume_key(
+ UserRecord *h,
+ struct crypt_device *cd,
+ const PasswordCache *cache,
+ void *volume_key,
+ size_t *volume_key_size,
+ key_serial_t *ret_key_serial) {
+
+ char **list;
+ size_t vks;
+ int r;
+
+ assert(h);
+ assert(cd);
+ assert(volume_key);
+ assert(volume_key_size);
+
+ if (cache && cache->volume_key) {
+ /* Shortcut: If volume key was loaded from the keyring then just use it */
+ if (cache->volume_key_size > *volume_key_size)
+ return log_error_errno(SYNTHETIC_ERRNO(ENOBUFS),
+ "LUKS volume key from kernel keyring too big for buffer (need %zu bytes, have %zu).",
+ cache->volume_key_size, *volume_key_size);
+ memcpy(volume_key, cache->volume_key, cache->volume_key_size);
+ *volume_key_size = cache->volume_key_size;
+ if (ret_key_serial)
+ *ret_key_serial = -1; /* Key came from keyring. No need to re-upload it */
+ return 0;
+ }
+
+ vks = *volume_key_size;
+
+ FOREACH_ARGUMENT(list,
+ cache ? cache->pkcs11_passwords : NULL,
+ cache ? cache->fido2_passwords : NULL,
+ h->password) {
+
+ r = luks_try_passwords(h, cd, list, volume_key, &vks);
+ if (r == -ENOKEY)
+ continue;
+ if (r < 0)
+ return r;
+
+ /* We got a volume key! */
+
+ if (ret_key_serial) {
+ r = upload_to_keyring(h, volume_key, vks, ret_key_serial);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to upload LUKS volume key to kernel keyring, ignoring: %m");
+ *ret_key_serial = -1;
+ }
+ }
+
+ *volume_key_size = vks;
+ return 0;
+ }
+
+ return -ENOKEY;
+}
+
static int luks_setup(
UserRecord *h,
const char *node,
@@ -351,7 +386,6 @@ static int luks_setup(
const char *cipher,
const char *cipher_mode,
uint64_t volume_key_size,
- char **passwords,
const PasswordCache *cache,
bool discard,
struct crypt_device **ret,
@@ -365,7 +399,6 @@ static int luks_setup(
_cleanup_(erase_and_freep) void *vk = NULL;
sd_id128_t p;
size_t vks;
- char **list;
int r;
assert(h);
@@ -385,7 +418,7 @@ static int luks_setup(
r = sym_crypt_get_volume_key_size(cd);
if (r <= 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size.");
vks = (size_t) r;
if (!sd_id128_is_null(uuid) || ret_found_uuid) {
@@ -418,16 +451,7 @@ static int luks_setup(
if (!vk)
return log_oom();
- r = -ENOKEY;
- FOREACH_POINTER(list,
- cache ? cache->keyring_passswords : NULL,
- cache ? cache->pkcs11_passwords : NULL,
- cache ? cache->fido2_passwords : NULL,
- passwords) {
- r = luks_try_passwords(h, cd, list, vk, &vks, ret_key_serial ? &key_serial : NULL);
- if (r != -ENOKEY)
- break;
- }
+ r = luks_get_volume_key(h, cd, cache, vk, &vks, ret_key_serial ? &key_serial : NULL);
if (r == -ENOKEY)
return log_error_errno(r, "No valid password for LUKS superblock.");
if (r < 0)
@@ -519,7 +543,6 @@ static int luks_open(
_cleanup_(erase_and_freep) void *vk = NULL;
sd_id128_t p;
- char **list;
size_t vks;
int r;
@@ -540,7 +563,7 @@ static int luks_open(
r = sym_crypt_get_volume_key_size(setup->crypt_device);
if (r <= 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size");
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size.");
vks = (size_t) r;
if (ret_found_uuid) {
@@ -559,16 +582,7 @@ static int luks_open(
if (!vk)
return log_oom();
- r = -ENOKEY;
- FOREACH_POINTER(list,
- cache ? cache->keyring_passswords : NULL,
- cache ? cache->pkcs11_passwords : NULL,
- cache ? cache->fido2_passwords : NULL,
- h->password) {
- r = luks_try_passwords(h, setup->crypt_device, list, vk, &vks, NULL);
- if (r != -ENOKEY)
- break;
- }
+ r = luks_get_volume_key(h, setup->crypt_device, cache, vk, &vks, NULL);
if (r == -ENOKEY)
return log_error_errno(r, "No valid password for LUKS superblock.");
if (r < 0)
@@ -979,7 +993,7 @@ static int format_luks_token_text(
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. ");
+ 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);
@@ -1295,9 +1309,6 @@ int home_setup_luks(
if (!IN_SET(errno, ENOTTY, EINVAL))
return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n);
- if (ioctl(setup->loop->fd, BLKGETSIZE64, &size) < 0)
- return log_error_errno(r, "Failed to read block device size of %s: %m", n);
-
if (fstat(setup->loop->fd, &st) < 0)
return log_error_errno(r, "Failed to stat block device %s: %m", n);
assert(S_ISBLK(st.st_mode));
@@ -1329,6 +1340,8 @@ int home_setup_luks(
offset *= 512U;
}
+
+ size = setup->loop->device_size;
} else {
#if HAVE_VALGRIND_MEMCHECK_H
VALGRIND_MAKE_MEM_DEFINED(&info, sizeof(info));
@@ -1403,7 +1416,6 @@ int home_setup_luks(
h->luks_cipher,
h->luks_cipher_mode,
h->luks_volume_key_size,
- h->password,
cache,
user_record_luks_discard(h) || user_record_luks_offline_discard(h),
&setup->crypt_device,
@@ -1698,12 +1710,13 @@ static struct crypt_pbkdf_type* build_minimal_pbkdf(struct crypt_pbkdf_type *buf
assert(hr);
/* For PKCS#11 derived keys (which are generated randomly and are of high quality already) we use a
- * minimal PBKDF */
+ * minimal PBKDF and CRYPT_PBKDF_NO_BENCHMARK flag to skip benchmark. */
*buffer = (struct crypt_pbkdf_type) {
.hash = user_record_luks_pbkdf_hash_algorithm(hr),
.type = CRYPT_KDF_PBKDF2,
- .iterations = 1,
- .time_ms = 1,
+ .iterations = 1000, /* recommended minimum count for pbkdf2
+ * according to NIST SP 800-132, ch. 5.2 */
+ .flags = CRYPT_PBKDF_NO_BENCHMARK
};
return buffer;
@@ -2227,8 +2240,9 @@ int home_create_luks(
if (flock(setup->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(setup->image_fd, BLKGETSIZE64, &block_device_size) < 0)
- return log_error_errno(errno, "Failed to read block device size: %m");
+ r = blockdev_get_device_size(setup->image_fd, &block_device_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read block device size: %m");
if (h->disk_size == UINT64_MAX) {
@@ -2539,7 +2553,7 @@ static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) {
/* btrfs can grow and shrink online */
- } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) {
+ } else if (is_fs_type(&sfs, XFS_SUPER_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).");
@@ -2602,7 +2616,7 @@ static int ext4_offline_resize_fs(
return r;
if (r == 0) {
/* Child */
- execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL);
+ execlp("e2fsck", "e2fsck", "-fp", setup->dm_node, NULL);
log_open();
log_error_errno(errno, "Failed to execute e2fsck: %m");
_exit(EXIT_FAILURE);
@@ -2634,7 +2648,7 @@ static int ext4_offline_resize_fs(
return r;
if (r == 0) {
/* Child */
- execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL);
+ execlp("resize2fs", "resize2fs", setup->dm_node, size_str, NULL);
log_open();
log_error_errno(errno, "Failed to execute resize2fs: %m");
_exit(EXIT_FAILURE);
@@ -2720,7 +2734,7 @@ static int prepare_resize_partition(
p = fdisk_table_get_partition(t, i);
if (!p)
- return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata: %m");
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read partition metadata.");
if (fdisk_partition_is_used(p) <= 0)
continue;
@@ -3126,7 +3140,7 @@ int home_resize_luks(
struct fdisk_partition *partition = NULL;
_cleanup_close_ int opened_image_fd = -EBADF;
_cleanup_free_ char *whole_disk = NULL;
- int r, resize_type, image_fd = -EBADF;
+ int r, resize_type, image_fd = -EBADF, reconciled = USER_RECONCILE_IDENTICAL;
sd_id128_t disk_uuid;
const char *ip, *ipo;
struct statfs sfs;
@@ -3183,8 +3197,9 @@ int home_resize_luks(
} 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");
+ r = blockdev_get_device_size(image_fd, &old_image_size);
+ if (r < 0)
+ return log_error_errno(r, "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);
@@ -3231,9 +3246,9 @@ int home_resize_luks(
return r;
if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
- r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
- if (r < 0)
- return r;
+ reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, cache, &embedded_home, &new_home);
+ if (reconciled < 0)
+ return reconciled;
}
r = home_maybe_shift_uid(h, flags, setup);
@@ -3444,7 +3459,11 @@ int home_resize_luks(
/* → Shrink */
if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
- r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
}
@@ -3532,7 +3551,11 @@ int home_resize_luks(
} else { /* → Grow */
if (!FLAGS_SET(flags, HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES)) {
- r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
}
@@ -3582,7 +3605,6 @@ int home_passwd_luks(
_cleanup_(erase_and_freep) void *volume_key = NULL;
struct crypt_pbkdf_type good_pbkdf, minimal_pbkdf;
const char *type;
- char **list;
int r;
assert(h);
@@ -3611,17 +3633,7 @@ int home_passwd_luks(
if (!volume_key)
return log_oom();
- r = -ENOKEY;
- FOREACH_POINTER(list,
- cache ? cache->keyring_passswords : NULL,
- cache ? cache->pkcs11_passwords : NULL,
- cache ? cache->fido2_passwords : NULL,
- h->password) {
-
- r = luks_try_passwords(h, setup->crypt_device, list, volume_key, &volume_key_size, NULL);
- if (r != -ENOKEY)
- break;
- }
+ r = luks_get_volume_key(h, setup->crypt_device, cache, volume_key, &volume_key_size, NULL);
if (r == -ENOKEY)
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with supplied passwords.");
if (r < 0)
@@ -3664,11 +3676,6 @@ int home_passwd_luks(
return log_error_errno(r, "Failed to set up LUKS password: %m");
log_info("Updated LUKS key slot %zu.", i);
-
- /* If we changed the password, then make sure to update the copy in the keyring, so that
- * auto-rebalance continues to work. We only do this if we operate on an active home dir. */
- if (i == 0 && FLAGS_SET(flags, HOME_SETUP_ALREADY_ACTIVATED))
- upload_to_keyring(h, effective_passwords[i], NULL);
}
return 1;
@@ -3706,36 +3713,10 @@ int home_lock_luks(UserRecord *h, HomeSetup *setup) {
return 0;
}
-static int luks_try_resume(
- struct crypt_device *cd,
- const char *dm_name,
- char **password) {
-
- int r;
-
- assert(cd);
- assert(dm_name);
-
- STRV_FOREACH(pp, password) {
- r = sym_crypt_resume_by_passphrase(
- cd,
- dm_name,
- CRYPT_ANY_SLOT,
- *pp,
- strlen(*pp));
- if (r >= 0) {
- log_info("Resumed LUKS device %s.", dm_name);
- return 0;
- }
-
- log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - password));
- }
-
- return -ENOKEY;
-}
-
int home_unlock_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache) {
- char **list;
+ _cleanup_(keyring_unlinkp) key_serial_t key_serial = -1;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ size_t vks;
int r;
assert(h);
@@ -3748,20 +3729,27 @@ int home_unlock_luks(UserRecord *h, HomeSetup *setup, const PasswordCache *cache
log_info("Discovered used LUKS device %s.", setup->dm_node);
- r = -ENOKEY;
- FOREACH_POINTER(list,
- cache ? cache->pkcs11_passwords : NULL,
- cache ? cache->fido2_passwords : NULL,
- h->password) {
- r = luks_try_resume(setup->crypt_device, setup->dm_name, list);
- if (r != -ENOKEY)
- break;
- }
+ r = sym_crypt_get_volume_key_size(setup->crypt_device);
+ if (r <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size.");
+ vks = (size_t) r;
+
+ vk = malloc(vks);
+ if (!vk)
+ return log_oom();
+
+ r = luks_get_volume_key(h, setup->crypt_device, cache, vk, &vks, &key_serial);
if (r == -ENOKEY)
return log_error_errno(r, "No valid password for LUKS superblock.");
if (r < 0)
+ return log_error_errno(r, "Failed to unlock LUKS superblock: %m");
+
+ r = sym_crypt_resume_by_volume_key(setup->crypt_device, setup->dm_name, vk, vks);
+ if (r < 0)
return log_error_errno(r, "Failed to resume LUKS superblock: %m");
+ TAKE_KEY_SERIAL(key_serial); /* Leave key in kernel keyring */
+
log_info("LUKS device resumed.");
return 0;
}
diff --git a/src/home/homework-password-cache.c b/src/home/homework-password-cache.c
index 00a0f69..b8202ef 100644
--- a/src/home/homework-password-cache.c
+++ b/src/home/homework-password-cache.c
@@ -9,49 +9,41 @@ void password_cache_free(PasswordCache *cache) {
if (!cache)
return;
+ cache->volume_key = erase_and_free(cache->volume_key);
cache->pkcs11_passwords = strv_free_erase(cache->pkcs11_passwords);
cache->fido2_passwords = strv_free_erase(cache->fido2_passwords);
- cache->keyring_passswords = strv_free_erase(cache->keyring_passswords);
}
void password_cache_load_keyring(UserRecord *h, PasswordCache *cache) {
- _cleanup_(erase_and_freep) void *p = NULL;
_cleanup_free_ char *name = NULL;
- char **strv;
+ _cleanup_(erase_and_freep) void *vk = NULL;
+ size_t vks;
key_serial_t serial;
- size_t sz;
int r;
assert(h);
assert(cache);
- /* Loads the password we need to for automatic resizing from the kernel keyring */
-
name = strjoin("homework-user-", h->user_name);
if (!name)
return (void) log_oom();
serial = request_key("user", name, NULL, 0);
- if (serial == -1)
- return (void) log_debug_errno(errno, "Failed to request key '%s', ignoring: %m", name);
-
- r = keyring_read(serial, &p, &sz);
+ if (serial == -1) {
+ if (errno == ENOKEY) {
+ log_info("Home volume key is not available in kernel keyring.");
+ return;
+ }
+ return (void) log_warning_errno(errno, "Failed to request key '%s', ignoring: %m", name);
+ }
+
+ r = keyring_read(serial, &vk, &vks);
if (r < 0)
- return (void) log_debug_errno(r, "Failed to read keyring key '%s', ignoring: %m", name);
-
- if (memchr(p, 0, sz))
- return (void) log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Cached password contains embedded NUL byte, ignoring.");
-
- strv = new(char*, 2);
- if (!strv)
- return (void) log_oom();
-
- strv[0] = TAKE_PTR(p); /* Note that keyring_read() will NUL terminate implicitly, hence we don't have
- * to NUL terminate manually here: it's a valid string. */
- strv[1] = NULL;
+ return (void) log_warning_errno(r, "Failed to read keyring key '%s', ignoring: %m", name);
- strv_free_erase(cache->keyring_passswords);
- cache->keyring_passswords = strv;
+ log_info("Successfully acquired home volume key from kernel keyring.");
- log_debug("Successfully acquired home key from kernel keyring.");
+ erase_and_free(cache->volume_key);
+ cache->volume_key = TAKE_PTR(vk);
+ cache->volume_key_size = vks;
}
diff --git a/src/home/homework-password-cache.h b/src/home/homework-password-cache.h
index fdfbcfe..e2d86eb 100644
--- a/src/home/homework-password-cache.h
+++ b/src/home/homework-password-cache.h
@@ -5,8 +5,9 @@
#include "user-record.h"
typedef struct PasswordCache {
- /* Passwords acquired from the kernel keyring */
- char **keyring_passswords;
+ /* The volume key from the kernel keyring */
+ void *volume_key;
+ size_t volume_key_size;
/* Decoding passwords from security tokens is expensive and typically requires user interaction,
* hence cache any we already figured out. */
@@ -20,9 +21,12 @@ static inline bool password_cache_contains(const PasswordCache *cache, const cha
if (!cache)
return false;
+ /* Used to decide whether or not to set a minimal PBKDF, under the assumption that if
+ * the cache contains a password then the password came from a hardware token of some kind
+ * and is thus naturally high-entropy. */
+
return strv_contains(cache->pkcs11_passwords, p) ||
- strv_contains(cache->fido2_passwords, p) ||
- strv_contains(cache->keyring_passswords, p);
+ strv_contains(cache->fido2_passwords, p);
}
void password_cache_load_keyring(UserRecord *h, PasswordCache *cache);
diff --git a/src/home/homework-quota.c b/src/home/homework-quota.c
index 508c0c0..c951682 100644
--- a/src/home/homework-quota.c
+++ b/src/home/homework-quota.c
@@ -99,7 +99,7 @@ int home_update_quota_auto(UserRecord *h, const char *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) ||
+ if (is_fs_type(&sfs, XFS_SUPER_MAGIC) ||
is_fs_type(&sfs, EXT4_SUPER_MAGIC))
return home_update_quota_classic(h, path);
@@ -107,7 +107,7 @@ int home_update_quota_auto(UserRecord *h, const char *path) {
r = btrfs_is_subvol(path);
if (r < 0)
- return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path);
+ return log_error_errno(r, "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);
diff --git a/src/home/homework.c b/src/home/homework.c
index 066483e..482db23 100644
--- a/src/home/homework.c
+++ b/src/home/homework.c
@@ -4,13 +4,17 @@
#include <sys/mount.h>
#include "blockdev-util.h"
+#include "bus-unit-util.h"
#include "chown-recursive.h"
#include "copy.h"
+#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "filesystems.h"
+#include "format-util.h"
#include "fs-util.h"
#include "home-util.h"
+#include "homework-blob.h"
#include "homework-cifs.h"
#include "homework-directory.h"
#include "homework-fido2.h"
@@ -24,6 +28,7 @@
#include "memory-util.h"
#include "missing_magic.h"
#include "mount-util.h"
+#include "parse-util.h"
#include "path-util.h"
#include "recovery-key.h"
#include "rm-rf.h"
@@ -51,6 +56,7 @@ int user_record_authenticate(
assert(h);
assert(secret);
+ assert(cache);
/* Tries to authenticate a user record with the supplied secrets. i.e. checks whether at least one
* supplied plaintext passwords matches a hashed password field of the user record. Or if a
@@ -61,9 +67,25 @@ int user_record_authenticate(
* times over the course of an operation (think: on login we authenticate the host user record, the
* record embedded in the LUKS record and the one embedded in $HOME). Hence we keep a list of
* passwords we already decrypted, so that we don't have to do the (slow and potentially interactive)
- * PKCS#11/FIDO2 dance for the relevant token again and again. */
+ * PKCS#11/FIDO2 dance for the relevant token again and again.
+ *
+ * The 'cache' parameter might also contain the LUKS volume key, loaded from the kernel keyring.
+ * In this case, authentication becomes optional - if a secret section is provided it will be
+ * verified, but if missing then authentication is skipped entirely. Thus, callers should
+ * consider carefully whether it is safe to load the volume key into 'cache' before doing so.
+ * Note that most of the time this is safe, because the home area must be active for the key
+ * to exist in the keyring, and the user would have had to authenticate when activating their
+ * home area; however, for some methods (i.e. ChangePassword, Authenticate) it makes more sense
+ * to force re-authentication. */
+
+ /* First, let's see if we already have a volume key from the keyring */
+ if (cache->volume_key &&
+ json_variant_is_blank_object(json_variant_by_key(secret->json, "secret"))) {
+ log_info("LUKS volume key from keyring unlocks user record.");
+ return 1;
+ }
- /* First, let's see if the supplied plain-text passwords work? */
+ /* Next, let's see if the supplied plain-text passwords work? */
r = user_record_test_password(h, secret);
if (r == -ENOKEY)
need_password = true;
@@ -96,10 +118,10 @@ int user_record_authenticate(
else
log_info("None of the supplied plaintext passwords unlock the user record's hashed recovery keys.");
- /* Second, test cached PKCS#11 passwords */
- for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++)
+ /* Next, test cached PKCS#11 passwords */
+ FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key)
STRV_FOREACH(pp, cache->pkcs11_passwords) {
- r = test_password_one(h->pkcs11_encrypted_key[n].hashed_password, *pp);
+ r = test_password_one(i->hashed_password, *pp);
if (r < 0)
return log_error_errno(r, "Failed to check supplied PKCS#11 password: %m");
if (r > 0) {
@@ -108,11 +130,11 @@ int user_record_authenticate(
}
}
- /* Third, test cached FIDO2 passwords */
- for (size_t n = 0; n < h->n_fido2_hmac_salt; n++)
+ /* Next, test cached FIDO2 passwords */
+ FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt)
/* See if any of the previously calculated passwords work */
STRV_FOREACH(pp, cache->fido2_passwords) {
- r = test_password_one(h->fido2_hmac_salt[n].hashed_password, *pp);
+ r = test_password_one(i->hashed_password, *pp);
if (r < 0)
return log_error_errno(r, "Failed to check supplied FIDO2 password: %m");
if (r > 0) {
@@ -121,13 +143,13 @@ int user_record_authenticate(
}
}
- /* Fourth, let's see if any of the PKCS#11 security tokens are plugged in and help us */
- for (size_t n = 0; n < h->n_pkcs11_encrypted_key; n++) {
+ /* Next, let's see if any of the PKCS#11 security tokens are plugged in and help us */
+ FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key) {
#if HAVE_P11KIT
_cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
.user_record = h,
.secret = secret,
- .encrypted_key = h->pkcs11_encrypted_key + n,
+ .encrypted_key = i,
};
r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data);
@@ -161,7 +183,9 @@ int user_record_authenticate(
if (r < 0)
return log_error_errno(r, "Failed to test PKCS#11 password: %m");
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured PKCS#11 security token %s does not decrypt encrypted key correctly.", data.encrypted_key->uri);
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Configured PKCS#11 security token %s does not decrypt encrypted key correctly.",
+ data.encrypted_key->uri);
log_info("Decrypted password from PKCS#11 security token %s unlocks user record.", data.encrypted_key->uri);
@@ -177,12 +201,12 @@ int user_record_authenticate(
#endif
}
- /* Fifth, let's see if any of the FIDO2 security tokens are plugged in and help us */
- for (size_t n = 0; n < h->n_fido2_hmac_salt; n++) {
+ /* Next, let's see if any of the FIDO2 security tokens are plugged in and help us */
+ FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt) {
#if HAVE_LIBFIDO2
_cleanup_(erase_and_freep) char *decrypted_password = NULL;
- r = fido2_use_token(h, secret, h->fido2_hmac_salt + n, &decrypted_password);
+ r = fido2_use_token(h, secret, i, &decrypted_password);
switch (r) {
case -EAGAIN:
need_token = true;
@@ -209,11 +233,12 @@ int user_record_authenticate(
if (r < 0)
return r;
- r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password);
+ r = test_password_one(i->hashed_password, decrypted_password);
if (r < 0)
return log_error_errno(r, "Failed to test FIDO2 password: %m");
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Configured FIDO2 security token does not decrypt encrypted key correctly.");
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Configured FIDO2 security token does not decrypt encrypted key correctly.");
log_info("Decrypted password from FIDO2 security token unlocks user record.");
@@ -275,10 +300,10 @@ static void drop_caches_now(void) {
int r;
/* Drop file system caches now. See https://docs.kernel.org/admin-guide/sysctl/vm.html
- * for details. We write "2" into /proc/sys/vm/drop_caches to ensure dentries/inodes are flushed, but
+ * for details. We write "3" into /proc/sys/vm/drop_caches to ensure dentries/inodes are flushed, but
* not more. */
- r = write_string_file("/proc/sys/vm/drop_caches", "2\n", WRITE_STRING_FILE_DISABLE_BUFFER);
+ r = write_string_file("/proc/sys/vm/drop_caches", "3\n", WRITE_STRING_FILE_DISABLE_BUFFER);
if (r < 0)
log_warning_errno(r, "Failed to drop caches, ignoring: %m");
else
@@ -354,6 +379,9 @@ static int keyring_flush(UserRecord *h) {
assert(h);
+ if (user_record_storage(h) == USER_FSCRYPT)
+ (void) home_flush_keyring_fscrypt(h);
+
name = strjoin("homework-user-", h->user_name);
if (!name)
return log_oom();
@@ -638,7 +666,7 @@ int home_load_embedded_identity(
*
* · 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)
+ * · The record in the home directory itself (~/.identity)
*
* Now we have to reconcile all three, and let the newest one win. */
@@ -695,16 +723,15 @@ int home_load_embedded_identity(
if (ret_new_home)
*ret_new_home = TAKE_PTR(new_home);
- return 0;
+ return r; /* We pass along who won the reconciliation */
}
-int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) {
+int home_store_embedded_identity(UserRecord *h, int root_fd, 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|USER_RECORD_PERMISSIVE, &embedded);
if (r < 0)
@@ -827,7 +854,7 @@ int home_refresh(
UserRecord **ret_new_home) {
_cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL;
- int r;
+ int r, reconciled;
assert(h);
assert(setup);
@@ -836,9 +863,9 @@ int home_refresh(
/* When activating a home directory, does the identity work: loads the identity from the $HOME
* directory, reconciles it with our idea, chown()s everything. */
- r = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home);
- if (r < 0)
- return r;
+ reconciled = home_load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, cache, &embedded_home, &new_home);
+ if (reconciled < 0)
+ return reconciled;
r = home_maybe_shift_uid(h, flags, setup);
if (r < 0)
@@ -848,7 +875,11 @@ int home_refresh(
if (r < 0)
return r;
- r = home_store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup->root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup->root_fd, reconciled);
if (r < 0)
return r;
@@ -1031,12 +1062,13 @@ static int home_deactivate(UserRecord *h, bool force) {
return 0;
}
-static int copy_skel(int root_fd, const char *skel) {
+static int copy_skel(UserRecord *h, int root_fd, const char *skel) {
int r;
+ assert(h);
assert(root_fd >= 0);
- r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE, NULL, NULL);
+ r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", h->uid, h->gid, COPY_MERGE|COPY_REPLACE, NULL, NULL);
if (r == -ENOENT) {
log_info("Skeleton directory %s missing, ignoring.", skel);
return 0;
@@ -1064,11 +1096,15 @@ int home_populate(UserRecord *h, int dir_fd) {
assert(h);
assert(dir_fd >= 0);
- r = copy_skel(dir_fd, user_record_skeleton_directory(h));
+ r = copy_skel(h, dir_fd, user_record_skeleton_directory(h));
+ if (r < 0)
+ return r;
+
+ r = home_store_embedded_identity(h, dir_fd, NULL);
if (r < 0)
return r;
- r = home_store_embedded_identity(h, dir_fd, h->uid, NULL);
+ r = home_reconcile_blob_dirs(h, dir_fd, USER_RECONCILE_HOST_WON);
if (r < 0)
return r;
@@ -1089,7 +1125,6 @@ static int user_record_compile_effective_passwords(
char ***ret_effective_passwords) {
_cleanup_strv_free_erase_ char **effective = NULL;
- size_t n;
int r;
assert(h);
@@ -1134,17 +1169,16 @@ static int user_record_compile_effective_passwords(
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Missing plaintext password for defined hashed password");
}
- for (n = 0; n < h->n_recovery_key; n++) {
+ FOREACH_ARRAY(i, h->recovery_key, h->n_recovery_key) {
bool found = false;
- log_debug("Looking for plaintext recovery key for: %s", h->recovery_key[n].hashed_password);
+ log_debug("Looking for plaintext recovery key for: %s", i->hashed_password);
STRV_FOREACH(j, h->password) {
_cleanup_(erase_and_freep) char *mangled = NULL;
const char *p;
- if (streq(h->recovery_key[n].type, "modhex64")) {
-
+ if (streq(i->type, "modhex64")) {
r = normalize_recovery_key(*j, &mangled);
if (r == -EINVAL) /* Not properly formatted, probably a regular password. */
continue;
@@ -1155,7 +1189,7 @@ static int user_record_compile_effective_passwords(
} else
p = *j;
- r = test_password_one(h->recovery_key[n].hashed_password, p);
+ r = test_password_one(i->hashed_password, p);
if (r < 0)
return log_error_errno(r, "Failed to test plaintext recovery key: %m");
if (r > 0) {
@@ -1172,15 +1206,16 @@ static int user_record_compile_effective_passwords(
}
if (!found)
- return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO), "Missing plaintext recovery key for defined recovery key");
+ return log_error_errno(SYNTHETIC_ERRNO(EREMOTEIO),
+ "Missing plaintext recovery key for defined recovery key.");
}
- for (n = 0; n < h->n_pkcs11_encrypted_key; n++) {
+ FOREACH_ARRAY(i, h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key) {
#if HAVE_P11KIT
_cleanup_(pkcs11_callback_data_release) struct pkcs11_callback_data data = {
.user_record = h,
.secret = h,
- .encrypted_key = h->pkcs11_encrypted_key + n,
+ .encrypted_key = i,
};
r = pkcs11_find_token(data.encrypted_key->uri, pkcs11_callback, &data);
@@ -1209,19 +1244,20 @@ static int user_record_compile_effective_passwords(
#endif
}
- for (n = 0; n < h->n_fido2_hmac_salt; n++) {
+ FOREACH_ARRAY(i, h->fido2_hmac_salt, h->n_fido2_hmac_salt) {
#if HAVE_LIBFIDO2
_cleanup_(erase_and_freep) char *decrypted_password = NULL;
- r = fido2_use_token(h, h, h->fido2_hmac_salt + n, &decrypted_password);
+ r = fido2_use_token(h, h, i, &decrypted_password);
if (r < 0)
return r;
- r = test_password_one(h->fido2_hmac_salt[n].hashed_password, decrypted_password);
+ r = test_password_one(i->hashed_password, decrypted_password);
if (r < 0)
return log_error_errno(r, "Failed to test FIDO2 password: %m");
if (r == 0)
- return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Decrypted password from token is not correct, refusing.");
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM),
+ "Decrypted password from token is not correct, refusing.");
if (ret_effective_passwords) {
r = strv_extend(&effective, decrypted_password);
@@ -1306,7 +1342,7 @@ static int determine_default_storage(UserStorage *ret) {
return 0;
}
-static int home_create(UserRecord *h, UserRecord **ret_home) {
+static int home_create(UserRecord *h, Hashmap *blobs, UserRecord **ret_home) {
_cleanup_strv_free_erase_ char **effective_passwords = NULL;
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(user_record_unrefp) UserRecord *new_home = NULL;
@@ -1368,6 +1404,10 @@ static int home_create(UserRecord *h, UserRecord **ret_home) {
if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE))
return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h));
+ r = home_apply_new_blob_dir(h, blobs);
+ if (r < 0)
+ return r;
+
switch (user_record_storage(h)) {
case USER_LUKS:
@@ -1519,20 +1559,32 @@ static int home_remove(UserRecord *h) {
return 0;
}
-static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags *flags) {
- bool has_mount = false;
- int r;
-
+static int home_basic_validate_update(UserRecord *h) {
assert(h);
- assert(setup);
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)));
+ return 0;
+}
+
+static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags *flags) {
+ bool has_mount = false;
+ int r;
+
+ assert(h);
+ assert(setup);
+
+ r = home_basic_validate_update(h);
+ if (r < 0)
+ return r;
+
r = user_record_test_home_directory_and_warn(h);
if (r < 0)
return r;
@@ -1573,25 +1625,48 @@ static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags
return has_mount; /* return true if the home record is already active */
}
-static int home_update(UserRecord *h, UserRecord **ret) {
+static int home_update(UserRecord *h, Hashmap *blobs, UserRecord **ret) {
_cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL;
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(password_cache_free) PasswordCache cache = {};
HomeSetupFlags flags = 0;
+ bool offline;
int r;
assert(h);
assert(ret);
- r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
+ offline = getenv_bool("SYSTEMD_HOMEWORK_UPDATE_OFFLINE") > 0;
+
+ if (!offline) {
+ password_cache_load_keyring(h, &cache);
+
+ r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
+ if (r < 0)
+ return r;
+ assert(r > 0); /* Insist that a password was verified */
+
+ r = home_validate_update(h, &setup, &flags);
+ } else {
+ /* In offline mode we skip all authentication, since we're
+ * not propagating anything into the home area. The new home
+ * records's authentication will still be checked when the user
+ * next logs in, so this is fine */
+
+ r = home_basic_validate_update(h);
+ }
if (r < 0)
return r;
- assert(r > 0); /* Insist that a password was verified */
- r = home_validate_update(h, &setup, &flags);
+ r = home_apply_new_blob_dir(h, blobs);
if (r < 0)
return r;
+ if (offline) {
+ log_info("Offline update requested. Not touching embedded records.");
+ return user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_PERMISSIVE, ret);
+ }
+
r = home_setup(h, flags, &setup, &cache, &header_home);
if (r < 0)
return r;
@@ -1608,7 +1683,11 @@ static int home_update(UserRecord *h, UserRecord **ret) {
if (r < 0)
return r;
- r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup.root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup.root_fd, USER_RECONCILE_HOST_WON);
if (r < 0)
return r;
@@ -1630,7 +1709,7 @@ static int home_update(UserRecord *h, UserRecord **ret) {
return 0;
}
-static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) {
+static int home_resize(UserRecord *h, UserRecord **ret) {
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(password_cache_free) PasswordCache cache = {};
HomeSetupFlags flags = 0;
@@ -1642,25 +1721,16 @@ static int home_resize(UserRecord *h, bool automatic, UserRecord **ret) {
if (h->disk_size == UINT64_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing.");
- if (automatic)
- /* In automatic mode don't want to ask the user for the password, hence load it from the kernel keyring */
- password_cache_load_keyring(h, &cache);
- else {
- /* In manual mode let's ensure the user is fully authenticated */
- r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
- if (r < 0)
- return r;
- assert(r > 0); /* Insist that a password was verified */
- }
+ password_cache_load_keyring(h, &cache);
- r = home_validate_update(h, &setup, &flags);
+ r = user_record_authenticate(h, h, &cache, /* strict_verify= */ true);
if (r < 0)
return r;
+ assert(r > 0); /* Insist that a password was verified */
- /* In automatic mode let's skip syncing identities, because we can't validate them, since we can't
- * ask the user for reauthentication */
- if (automatic)
- flags |= HOME_SETUP_RESIZE_DONT_SYNC_IDENTITIES;
+ r = home_validate_update(h, &setup, &flags);
+ if (r < 0)
+ return r;
switch (user_record_storage(h)) {
@@ -1683,7 +1753,7 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
_cleanup_(password_cache_free) PasswordCache cache = {};
HomeSetupFlags flags = 0;
- int r;
+ int r, reconciled;
assert(h);
assert(ret_home);
@@ -1703,9 +1773,9 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
if (r < 0)
return r;
- r = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home);
- if (r < 0)
- return r;
+ reconciled = home_load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &cache, &embedded_home, &new_home);
+ if (reconciled < 0)
+ return reconciled;
r = home_maybe_shift_uid(h, flags, &setup);
if (r < 0)
@@ -1733,7 +1803,11 @@ static int home_passwd(UserRecord *h, UserRecord **ret_home) {
if (r < 0)
return r;
- r = home_store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home);
+ r = home_store_embedded_identity(new_home, setup.root_fd, embedded_home);
+ if (r < 0)
+ return r;
+
+ r = home_reconcile_blob_dirs(new_home, setup.root_fd, reconciled);
if (r < 0)
return r;
@@ -1795,6 +1869,38 @@ static int home_inspect(UserRecord *h, UserRecord **ret_home) {
return 1;
}
+static int user_session_freezer(uid_t uid, bool freeze_now, UnitFreezer **ret) {
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ assert(uid_is_valid(uid));
+ assert(ret);
+
+ r = getenv_bool("SYSTEMD_HOME_LOCK_FREEZE_SESSION");
+ if (r < 0 && r != -ENXIO)
+ log_warning_errno(r, "Cannot parse value of $SYSTEMD_HOME_LOCK_FREEZE_SESSION, ignoring: %m");
+ else if (r == 0) {
+ if (freeze_now)
+ log_notice("Session remains unfrozen on explicit request ($SYSTEMD_HOME_LOCK_FREEZE_SESSION=0).\n"
+ "This is not recommended, and might result in unexpected behavior including data loss!");
+
+ *ret = NULL;
+ return 0;
+ }
+
+ if (asprintf(&unit, "user-" UID_FMT ".slice", uid) < 0)
+ return log_oom();
+
+ if (freeze_now)
+ r = unit_freezer_new_freeze(unit, ret);
+ else
+ r = unit_freezer_new(unit, ret);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
static int home_lock(UserRecord *h) {
_cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT;
int r;
@@ -1812,10 +1918,23 @@ static int home_lock(UserRecord *h) {
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);
- r = home_lock_luks(h, &setup);
+ _cleanup_(unit_freezer_freep) UnitFreezer *f = NULL;
+
+ r = user_session_freezer(h->uid, /* freeze_now= */ true, &f);
if (r < 0)
return r;
+ r = home_lock_luks(h, &setup);
+ if (r < 0) {
+ if (f)
+ (void) unit_freezer_thaw(f);
+
+ return r;
+ }
+
+ /* Explicitly flush any per-user key from the keyring */
+ (void) keyring_flush(h);
+
log_info("Everything completed.");
return 1;
}
@@ -1843,6 +1962,15 @@ static int home_unlock(UserRecord *h) {
if (r < 0)
return r;
+ _cleanup_(unit_freezer_freep) UnitFreezer *f = NULL;
+
+ /* We want to thaw the session only after it's safe to access $HOME */
+ r = user_session_freezer(h->uid, /* freeze_now= */ false, &f);
+ if (r > 0)
+ r = unit_freezer_thaw(f);
+ if (r < 0)
+ return r;
+
log_info("Everything completed.");
return 1;
}
@@ -1851,10 +1979,12 @@ static int run(int argc, char *argv[]) {
_cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_fclose_ FILE *opened_file = NULL;
+ _cleanup_hashmap_free_ Hashmap *blobs = NULL;
unsigned line = 0, column = 0;
- const char *json_path = NULL;
+ const char *json_path = NULL, *blob_filename;
FILE *json_file;
usec_t start;
+ JsonVariant *fdmap, *blob_fd_variant;
int r;
start = now(CLOCK_MONOTONIC);
@@ -1885,6 +2015,48 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column);
+ fdmap = json_variant_by_key(v, HOMEWORK_BLOB_FDMAP_FIELD);
+ if (fdmap) {
+ r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops);
+ if (r < 0)
+ return log_oom();
+
+ JSON_VARIANT_OBJECT_FOREACH(blob_filename, blob_fd_variant, fdmap) {
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ assert(json_variant_is_integer(blob_fd_variant));
+ assert(json_variant_integer(blob_fd_variant) >= 0);
+ assert(json_variant_integer(blob_fd_variant) <= INT_MAX - SD_LISTEN_FDS_START);
+ fd = SD_LISTEN_FDS_START + (int) json_variant_integer(blob_fd_variant);
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *resolved = NULL;
+ r = fd_get_path(fd, &resolved);
+ log_debug("Got blob from daemon: %s (%d) → %s",
+ blob_filename, fd, resolved ?: STRERROR(r));
+ }
+
+ filename = strdup(blob_filename);
+ if (!filename)
+ return log_oom();
+
+ r = fd_cloexec(fd, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable O_CLOEXEC on blob %s: %m", filename);
+
+ r = hashmap_put(blobs, filename, FD_TO_PTR(fd));
+ if (r < 0)
+ return log_error_errno(r, "Failed to insert blob %s into map: %m", filename);
+ TAKE_PTR(filename); /* Ownership transfers to hashmap */
+ TAKE_FD(fd);
+ }
+
+ r = json_variant_filter(&v, STRV_MAKE(HOMEWORK_BLOB_FDMAP_FIELD));
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip internal fdmap from JSON: %m");
+ }
+
home = user_record_new();
if (!home)
return log_oom();
@@ -1928,15 +2100,13 @@ static int run(int argc, char *argv[]) {
else if (streq(argv[1], "deactivate-force"))
r = home_deactivate(home, true);
else if (streq(argv[1], "create"))
- r = home_create(home, &new_home);
+ r = home_create(home, blobs, &new_home);
else if (streq(argv[1], "remove"))
r = home_remove(home);
else if (streq(argv[1], "update"))
- r = home_update(home, &new_home);
- else if (streq(argv[1], "resize")) /* Resize on user request */
- r = home_resize(home, false, &new_home);
- else if (streq(argv[1], "resize-auto")) /* Automatic resize */
- r = home_resize(home, true, &new_home);
+ r = home_update(home, blobs, &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"))
diff --git a/src/home/homework.h b/src/home/homework.h
index cef3f4e..fb2b43e 100644
--- a/src/home/homework.h
+++ b/src/home/homework.h
@@ -87,7 +87,7 @@ int home_maybe_shift_uid(UserRecord *h, HomeSetupFlags flags, HomeSetup *setup);
int home_populate(UserRecord *h, int dir_fd);
int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, PasswordCache *cache, UserRecord **ret_embedded_home, UserRecord **ret_new_home);
-int home_store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home);
+int home_store_embedded_identity(UserRecord *h, int root_fd, UserRecord *old_home);
int home_extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup);
int user_record_authenticate(UserRecord *h, UserRecord *secret, PasswordCache *cache, bool strict_verify);
diff --git a/src/home/meson.build b/src/home/meson.build
index 09831de..f573c5f 100644
--- a/src/home/meson.build
+++ b/src/home/meson.build
@@ -2,6 +2,7 @@
systemd_homework_sources = files(
'home-util.c',
+ 'homework-blob.c',
'homework-cifs.c',
'homework-directory.c',
'homework-fscrypt.c',
@@ -105,6 +106,11 @@ executables += [
threads,
],
},
+ test_template + {
+ 'sources' : files('test-homed-regression-31896.c'),
+ 'conditions' : ['ENABLE_HOMED'],
+ 'type' : 'manual',
+ },
]
modules += [
@@ -137,4 +143,8 @@ if conf.get('ENABLE_HOMED') == 1
install_data('homed.conf',
install_dir : pkgconfigfiledir)
endif
+
+ meson.add_install_script(sh, '-c',
+ ln_s.format(bindir / 'homectl',
+ bindir / 'systemd-home-fallback-shell'))
endif
diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf
index 5af1a68..b808592 100644
--- a/src/home/org.freedesktop.home1.conf
+++ b/src/home/org.freedesktop.home1.conf
@@ -59,6 +59,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
+ send_member="ActivateHomeIfReferenced"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
send_member="DeactivateHome"/>
<allow send_destination="org.freedesktop.home1"
@@ -75,6 +79,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
+ send_member="CreateHomeEx"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
send_member="RealizeHome"/>
<allow send_destination="org.freedesktop.home1"
@@ -95,6 +103,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
+ send_member="UpdateHomeEx"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
send_member="ResizeHome"/>
<allow send_destination="org.freedesktop.home1"
@@ -119,6 +131,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
+ send_member="RefHomeUnrestricted"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Manager"
send_member="ReleaseHome"/>
<allow send_destination="org.freedesktop.home1"
@@ -141,6 +157,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
+ send_member="ActivateIfReferenced"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Home"
send_member="Deactivate"/>
<allow send_destination="org.freedesktop.home1"
@@ -169,6 +189,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
+ send_member="UpdateEx"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Home"
send_member="Resize"/>
<allow send_destination="org.freedesktop.home1"
@@ -193,6 +217,10 @@
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
+ send_member="RefUnrestricted"/>
+
+ <allow send_destination="org.freedesktop.home1"
+ send_interface="org.freedesktop.home1.Home"
send_member="Release"/>
<allow receive_sender="org.freedesktop.home1"/>
diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy
index a337b32..3b19ed3 100644
--- a/src/home/org.freedesktop.home1.policy
+++ b/src/home/org.freedesktop.home1.policy
@@ -69,4 +69,13 @@
</defaults>
</action>
+ <action id="org.freedesktop.home1.activate-home">
+ <description gettext-domain="systemd">Activate a home area</description>
+ <message gettext-domain="systemd">Authentication is required to activate a user's home area.</message>
+ <defaults>
+ <allow_any>auth_admin_keep</allow_any>
+ <allow_inactive>auth_admin_keep</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ </action>
</policyconfig>
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
index ba8d8f6..4616f08 100644
--- a/src/home/pam_systemd_home.c
+++ b/src/home/pam_systemd_home.c
@@ -20,10 +20,16 @@
#include "user-record.h"
#include "user-util.h"
+typedef enum AcquireHomeFlags {
+ ACQUIRE_MUST_AUTHENTICATE = 1 << 0,
+ ACQUIRE_PLEASE_SUSPEND = 1 << 1,
+ ACQUIRE_REF_ANYWAY = 1 << 2,
+} AcquireHomeFlags;
+
static int parse_argv(
pam_handle_t *handle,
int argc, const char **argv,
- bool *please_suspend,
+ AcquireHomeFlags *flags,
bool *debug) {
assert(argc >= 0);
@@ -38,8 +44,8 @@ static int parse_argv(
k = parse_boolean(v);
if (k < 0)
pam_syslog(handle, LOG_WARNING, "Failed to parse suspend= argument, ignoring: %s", v);
- else if (please_suspend)
- *please_suspend = k;
+ else if (flags)
+ SET_FLAG(*flags, ACQUIRE_PLEASE_SUSPEND, k);
} else if (streq(argv[i], "debug")) {
if (debug)
@@ -62,7 +68,7 @@ static int parse_argv(
static int parse_env(
pam_handle_t *handle,
- bool *please_suspend) {
+ AcquireHomeFlags *flags) {
const char *v;
int r;
@@ -83,8 +89,8 @@ static int parse_env(
r = parse_boolean(v);
if (r < 0)
pam_syslog(handle, LOG_WARNING, "Failed to parse $SYSTEMD_HOME_SUSPEND argument, ignoring: %s", v);
- else if (please_suspend)
- *please_suspend = r;
+ else if (flags)
+ SET_FLAG(*flags, ACQUIRE_PLEASE_SUSPEND, r);
return 0;
}
@@ -99,7 +105,6 @@ static int acquire_user_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;
_cleanup_free_ char *homed_field = NULL;
const char *json = NULL;
int r;
@@ -142,6 +147,7 @@ static int acquire_user_record(
} else {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_free_ char *generic_field = NULL, *json_copy = NULL;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data);
if (r != PAM_SUCCESS)
@@ -275,21 +281,21 @@ static int handle_generic_user_record_error(
const sd_bus_error *error,
bool debug) {
+ int r;
+
assert(user_name);
assert(error);
- int r;
-
/* Logs about all errors, except for PAM_CONV_ERR, i.e. when requesting more info failed. */
if (sd_bus_error_has_name(error, BUS_ERROR_HOME_ABSENT)) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL,
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL,
_("Home of user %s is currently absent, please plug in the necessary storage device or backing file system."), user_name);
return pam_syslog_pam_error(handle, LOG_ERR, PAM_PERM_DENIED,
"Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
} else if (sd_bus_error_has_name(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too frequent login attempts for user %s, try again later."), user_name);
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Too frequent login attempts for user %s, try again later."), user_name);
return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES,
"Failed to acquire home for user %s: %s", user_name, bus_error_message(error, ret));
@@ -301,10 +307,10 @@ static int handle_generic_user_record_error(
/* This didn't work? Ask for an (additional?) password */
if (strv_isempty(secret->password))
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Password: "));
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Password: "));
else {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient for authentication of user %s."), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, try again: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient for authentication of user %s."), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, try again: "));
}
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -326,10 +332,10 @@ static int handle_generic_user_record_error(
/* Hmm, homed asks for recovery key (because no regular password is defined maybe)? Provide it. */
if (strv_isempty(secret->password))
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Recovery key: "));
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Recovery key: "));
else {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password/recovery key incorrect or not sufficient for authentication of user %s."), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, reenter recovery key: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password/recovery key incorrect or not sufficient for authentication of user %s."), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, reenter recovery key: "));
}
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -349,11 +355,11 @@ static int handle_generic_user_record_error(
assert(secret);
if (strv_isempty(secret->password)) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token of user %s not inserted."), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token of user %s not inserted."), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: "));
} else {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient, and configured security token of user %s not inserted."), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password incorrect or not sufficient, and configured security token of user %s not inserted."), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Try again with password: "));
}
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -363,7 +369,6 @@ static int handle_generic_user_record_error(
return PAM_AUTHTOK_ERR;
}
-
r = user_record_set_password(secret, STRV_MAKE(newp), true);
if (r < 0)
return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store password: %m");
@@ -373,7 +378,7 @@ static int handle_generic_user_record_error(
assert(secret);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Security token PIN: "));
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Security token PIN: "));
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -390,7 +395,7 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please authenticate physically on security token of user %s."), user_name);
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Please authenticate physically on security token of user %s."), user_name);
r = user_record_set_pkcs11_protected_authentication_path_permitted(secret, true);
if (r < 0)
@@ -401,7 +406,7 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please confirm presence on security token of user %s."), user_name);
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Please confirm presence on security token of user %s."), user_name);
r = user_record_set_fido2_user_presence_permitted(secret, true);
if (r < 0)
@@ -412,7 +417,7 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Please verify user on security token of user %s."), user_name);
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Please verify user on security token of user %s."), user_name);
r = user_record_set_fido2_user_verification_permitted(secret, true);
if (r < 0)
@@ -421,7 +426,7 @@ static int handle_generic_user_record_error(
} else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_PIN_LOCKED)) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)"));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN is locked, please unlock it first. (Hint: Removal and re-insertion might suffice.)"));
return PAM_SERVICE_ERR;
} else if (sd_bus_error_has_name(error, BUS_ERROR_TOKEN_BAD_PIN)) {
@@ -429,8 +434,8 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN incorrect for user %s."), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN incorrect for user %s."), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -448,8 +453,8 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only a few tries left!)"), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only a few tries left!)"), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -467,8 +472,8 @@ static int handle_generic_user_record_error(
assert(secret);
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only one try left!)"), user_name);
- r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Security token PIN of user %s incorrect (only one try left!)"), user_name);
+ r = pam_prompt_graceful(handle, PAM_PROMPT_ECHO_OFF, &newp, _("Sorry, retry security token PIN: "));
if (r != PAM_SUCCESS)
return PAM_CONV_ERR; /* no logging here */
@@ -490,14 +495,12 @@ static int handle_generic_user_record_error(
static int acquire_home(
pam_handle_t *handle,
- bool please_authenticate,
- bool please_suspend,
+ AcquireHomeFlags flags,
bool debug,
PamBusData **bus_data) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
- bool do_auth = please_authenticate, home_not_active = false, home_locked = false;
- _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false, unrestricted = false;
_cleanup_close_ int acquired_fd = -EBADF;
_cleanup_free_ char *fd_field = NULL;
const void *home_fd_ptr = NULL;
@@ -507,13 +510,27 @@ static int acquire_home(
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().
+ /* This acquires a reference to a home directory in the following ways:
+ *
+ * 1. If please_authenticate is false, it tries to call RefHome() first — which
+ * will get us a reference to the home without authentication (which will work for homes that are
+ * not encrypted, or that already are activated). If this works, we are done. Yay!
*
- * 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. */
+ * 2. Otherwise, we'll call AcquireHome() — which will try to activate the home getting us a
+ * reference. If this works, we are done. Yay!
+ *
+ * 3. if ref_anyway, we'll call RefHomeUnrestricted() — which will give us a reference in any case
+ * (even if the activation failed!).
+ *
+ * The idea is that please_authenticate is set to false for the PAM session hooks (since for those
+ * authentication doesn't matter), and true for the PAM authentication hooks (since for those
+ * authentication is essential). And ref_anyway should be set if we are pretty sure that we can later
+ * activate the home directory via our fallback shell logic, and hence are OK if we can't activate
+ * things here. Usecase for that are SSH logins where SSH does the authentication and thus only the
+ * session hooks are called. But from the session hooks SSH doesn't allow asking questions, hence we
+ * simply allow the login attempt to continue but then invoke our fallback shell that will prompt the
+ * user for the missing unlock credentials, and then chainload the real shell.
+ */
r = pam_get_user(handle, &username, NULL);
if (r != PAM_SUCCESS)
@@ -534,25 +551,26 @@ static int acquire_home(
if (r == PAM_SUCCESS && PTR_TO_FD(home_fd_ptr) >= 0)
return PAM_SUCCESS;
- r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data);
- if (r != PAM_SUCCESS)
- return r;
-
r = acquire_user_record(handle, username, debug, &ur, bus_data);
if (r != PAM_SUCCESS)
return r;
/* Implement our own retry loop here instead of relying on the PAM client's one. That's because it
- * might happen that 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. */
+ * might happen that 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 and all previously used passwords again. */
+
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, bus_data);
+ if (r != PAM_SUCCESS)
+ return r;
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;
+ const char *method = NULL;
if (do_auth && !secret) {
const char *cached_password = NULL;
@@ -576,7 +594,14 @@ static int acquire_home(
}
}
- r = bus_message_new_method_call(bus, &m, bus_home_mgr, do_auth ? "AcquireHome" : "RefHome");
+ if (do_auth)
+ method = "AcquireHome"; /* If we shall authenticate no matter what */
+ else if (unrestricted)
+ method = "RefHomeUnrestricted"; /* If we shall get a ref no matter what */
+ else
+ method = "RefHome"; /* If we shall get a ref (if possible) */
+
+ r = bus_message_new_method_call(bus, &m, bus_home_mgr, method);
if (r < 0)
return pam_bus_log_create_error(handle, r);
@@ -590,21 +615,22 @@ static int acquire_home(
return pam_bus_log_create_error(handle, r);
}
- r = sd_bus_message_append(m, "b", please_suspend);
+ r = sd_bus_message_append(m, "b", FLAGS_SET(flags, ACQUIRE_PLEASE_SUSPEND));
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_NOT_ACTIVE))
+ if (sd_bus_error_has_names(&error, BUS_ERROR_HOME_NOT_ACTIVE, BUS_ERROR_HOME_BUSY)) {
/* Only on RefHome(): We can't access the home directory currently, unless
* it's unlocked with a password. Hence, let's try this again, this time with
* authentication. */
home_not_active = true;
- else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED))
+ do_auth = true;
+ } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) {
home_locked = true; /* Similar */
- else {
+ do_auth = true;
+ } else {
r = handle_generic_user_record_error(handle, ur->user_name, secret, r, &error, debug);
if (r == PAM_CONV_ERR) {
/* Password/PIN prompts will fail in certain environments, for example when
@@ -612,20 +638,26 @@ static int acquire_home(
* per-service PAM logic. In that case, print a friendly message and accept
* failure. */
- if (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);
- if (home_locked)
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name);
+ if (!FLAGS_SET(flags, ACQUIRE_REF_ANYWAY)) {
+ if (home_not_active)
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently not active, please log in locally first."), ur->user_name);
+ if (home_locked)
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Home of user %s is currently locked, please unlock locally first."), ur->user_name);
+
+ if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) || debug)
+ pam_syslog(handle, FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
- if (please_authenticate || debug)
- pam_syslog(handle, please_authenticate ? LOG_ERR : LOG_DEBUG, "Failed to prompt for password/prompt.");
+ return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
+ }
- return home_not_active || home_locked ? PAM_PERM_DENIED : PAM_CONV_ERR;
- }
- if (r != PAM_SUCCESS)
+ /* ref_anyway is true, hence let's now get a ref no matter what. */
+ unrestricted = true;
+ do_auth = false;
+ } else if (r != PAM_SUCCESS)
return r;
+ else
+ do_auth = true; /* The issue was dealt with, some more information was collected. Let's try to authenticate, again. */
}
-
} else {
int fd;
@@ -641,18 +673,15 @@ static int acquire_home(
}
if (++n_attempts >= 5) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL,
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL,
_("Too many unsuccessful login attempts for user %s, refusing."), ur->user_name);
return pam_syslog_pam_error(handle, LOG_ERR, PAM_MAXTRIES,
"Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r));
}
-
- /* Try again, this time with authentication if we didn't do that before. */
- do_auth = true;
}
/* Later PAM modules may need the auth token, but only during pam_authenticate. */
- if (please_authenticate && !strv_isempty(secret->password)) {
+ if (FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE) && !strv_isempty(secret->password)) {
r = pam_set_item(handle, PAM_AUTHTOK, *secret->password);
if (r != PAM_SUCCESS)
return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to set PAM auth token: @PAMERR@");
@@ -672,7 +701,19 @@ static int acquire_home(
return r;
}
- pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
+ /* If we didn't actually manage to unlock the home directory, then we rely on the fallback-shell to
+ * unlock it for us. But until that happens we don't want that logind spawns the per-user service
+ * manager for us (since it would see an inaccessible home directory). Hence set an environment
+ * variable that pam_systemd looks for). */
+ if (unrestricted) {
+ r = pam_putenv(handle, "XDG_SESSION_INCOMPLETE=1");
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_WARNING, r, "Failed to set XDG_SESSION_INCOMPLETE= environment variable: @PAMERR@");
+
+ pam_syslog(handle, LOG_NOTICE, "Home for user %s acquired in incomplete mode, requires later activation.", ur->user_name);
+ } else
+ pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name);
+
return PAM_SUCCESS;
}
@@ -703,53 +744,99 @@ static int release_home_fd(pam_handle_t *handle, const char *username) {
_public_ PAM_EXTERN int pam_sm_authenticate(
pam_handle_t *handle,
- int flags,
+ int sm_flags,
int argc, const char **argv) {
- bool debug = false, suspend_please = false;
+ AcquireHomeFlags flags = 0;
+ bool debug = false;
- if (parse_env(handle, &suspend_please) < 0)
+ pam_log_setup();
+
+ if (parse_env(handle, &flags) < 0)
return PAM_AUTH_ERR;
if (parse_argv(handle,
argc, argv,
- &suspend_please,
+ &flags,
&debug) < 0)
return PAM_AUTH_ERR;
pam_debug_syslog(handle, debug, "pam-systemd-homed authenticating");
- return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug, NULL);
+ return acquire_home(handle, ACQUIRE_MUST_AUTHENTICATE|flags, debug, /* bus_data= */ NULL);
}
-_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
+_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int sm_flags, int argc, const char **argv) {
+ return PAM_SUCCESS;
+}
+
+static int fallback_shell_can_work(
+ pam_handle_t *handle,
+ AcquireHomeFlags *flags) {
+
+ const char *tty = NULL, *display = NULL;
+ int r;
+
+ assert(handle);
+ assert(flags);
+
+ r = pam_get_item_many(
+ handle,
+ PAM_TTY, &tty,
+ PAM_XDISPLAY, &display);
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM items: @PAMERR@");
+
+ /* The fallback shell logic only works on TTY logins, hence only allow it if there's no X11 display
+ * set, and a TTY field is set that is neither "cron" (which is what crond sets, god knows why) not
+ * contains a colon (which is what various graphical X11 logins do). Note that ssh sets the tty to
+ * "ssh" here, which we allow (I mean, ssh is after all the primary reason we do all this). */
+ if (isempty(display) &&
+ tty &&
+ !strchr(tty, ':') &&
+ !streq(tty, "cron"))
+ *flags |= ACQUIRE_REF_ANYWAY; /* Allow login even if we can only ref, not activate */
+
return PAM_SUCCESS;
}
_public_ PAM_EXTERN int pam_sm_open_session(
pam_handle_t *handle,
- int flags,
+ int sm_flags,
int argc, const char **argv) {
/* Let's release the D-Bus connection once this function exits, 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. */
_cleanup_(pam_bus_data_disconnectp) PamBusData *d = NULL;
- bool debug = false, suspend_please = false;
+ AcquireHomeFlags flags = 0;
+ bool debug = false;
int r;
- if (parse_env(handle, &suspend_please) < 0)
+ pam_log_setup();
+
+ if (parse_env(handle, &flags) < 0)
return PAM_SESSION_ERR;
if (parse_argv(handle,
argc, argv,
- &suspend_please,
+ &flags,
&debug) < 0)
return PAM_SESSION_ERR;
pam_debug_syslog(handle, debug, "pam-systemd-homed session start");
- r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug, &d);
+ r = fallback_shell_can_work(handle, &flags);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ /* Explicitly get saved PamBusData here. Otherwise, this function may succeed without setting 'd'
+ * even if there is an opened sd-bus connection, and it will be leaked. See issue #31375. */
+ r = pam_get_bus_data(handle, "pam-systemd-home", &d);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_home(handle, flags, debug, &d);
if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */
return PAM_SUCCESS;
if (r != PAM_SUCCESS)
@@ -760,7 +847,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
return pam_syslog_pam_error(handle, LOG_ERR, r,
"Failed to set PAM environment variable $SYSTEMD_HOME: @PAMERR@");
- r = pam_putenv(handle, suspend_please ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0");
+ r = pam_putenv(handle, FLAGS_SET(flags, ACQUIRE_PLEASE_SUSPEND) ? "SYSTEMD_HOME_SUSPEND=1" : "SYSTEMD_HOME_SUSPEND=0");
if (r != PAM_SUCCESS)
return pam_syslog_pam_error(handle, LOG_ERR, r,
"Failed to set PAM environment variable $SYSTEMD_HOME_SUSPEND: @PAMERR@");
@@ -770,16 +857,17 @@ _public_ PAM_EXTERN int pam_sm_open_session(
_public_ PAM_EXTERN int pam_sm_close_session(
pam_handle_t *handle,
- int flags,
+ int sm_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 = false;
int r;
+ pam_log_setup();
+
if (parse_argv(handle,
argc, argv,
NULL,
@@ -803,6 +891,7 @@ _public_ PAM_EXTERN int pam_sm_close_session(
if (r != PAM_SUCCESS)
return r;
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL);
if (r != PAM_SUCCESS)
return r;
@@ -829,27 +918,34 @@ _public_ PAM_EXTERN int pam_sm_close_session(
_public_ PAM_EXTERN int pam_sm_acct_mgmt(
pam_handle_t *handle,
- int flags,
+ int sm_flags,
int argc,
const char **argv) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
- bool debug = false, please_suspend = false;
+ AcquireHomeFlags flags = 0;
+ bool debug = false;
usec_t t;
int r;
- if (parse_env(handle, &please_suspend) < 0)
+ pam_log_setup();
+
+ if (parse_env(handle, &flags) < 0)
return PAM_AUTH_ERR;
if (parse_argv(handle,
argc, argv,
- &please_suspend,
+ &flags,
&debug) < 0)
return PAM_AUTH_ERR;
pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
- r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug, NULL);
+ r = fallback_shell_can_work(handle, &flags);
+ if (r != PAM_SUCCESS)
+ return r;
+
+ r = acquire_home(handle, flags, debug, /* bus_data= */ NULL);
if (r != PAM_SUCCESS)
return r;
@@ -865,20 +961,20 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
break;
case -ENOLCK:
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("User record is blocked, prohibiting access."));
+ (void) pam_prompt_graceful(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."));
+ (void) pam_prompt_graceful(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."));
+ (void) pam_prompt_graceful(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."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access."));
return PAM_ACCT_EXPIRED;
}
@@ -890,7 +986,7 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
usec_t n = now(CLOCK_REALTIME);
if (t > n) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Too many logins, try again in %s."),
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Too many logins, try again in %s."),
FORMAT_TIMESPAN(t - n, USEC_PER_SEC));
return PAM_MAXTRIES;
@@ -901,21 +997,21 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
switch (r) {
case -EKEYREVOKED:
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password change required."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password change required."));
return PAM_NEW_AUTHTOK_REQD;
case -EOWNERDEAD:
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password expired, change required."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password expired, change required."));
return PAM_NEW_AUTHTOK_REQD;
/* Strictly speaking this is only about password expiration, and we might want to allow
* authentication via PKCS#11 or so, but let's ignore this fine distinction for now. */
case -EKEYREJECTED:
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password is expired, but can't change, refusing login."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password is expired, but can't change, refusing login."));
return PAM_AUTHTOK_EXPIRED;
case -EKEYEXPIRED:
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("Password will expire soon, please change."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("Password will expire soon, please change."));
break;
case -ESTALE:
@@ -929,7 +1025,7 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
default:
if (r < 0) {
- (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access."));
+ (void) pam_prompt_graceful(handle, PAM_ERROR_MSG, NULL, _("User record not valid, prohibiting access."));
return PAM_AUTHTOK_EXPIRED;
}
@@ -941,17 +1037,18 @@ _public_ PAM_EXTERN int pam_sm_acct_mgmt(
_public_ PAM_EXTERN int pam_sm_chauthtok(
pam_handle_t *handle,
- int flags,
+ int sm_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 = false;
int r;
+ pam_log_setup();
+
if (parse_argv(handle,
argc, argv,
NULL,
@@ -960,22 +1057,17 @@ _public_ PAM_EXTERN int pam_sm_chauthtok(
pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
- r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL);
- if (r != PAM_SUCCESS)
- return r;
-
r = acquire_user_record(handle, NULL, debug, &ur, NULL);
if (r != PAM_SUCCESS)
return r;
/* Start with cached credentials */
- r = pam_get_item(handle, PAM_OLDAUTHTOK, (const void**) &old_password);
- if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS))
- return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get old password: @PAMERR@");
-
- r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password);
- if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS))
- return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get cached password: @PAMERR@");
+ r = pam_get_item_many(
+ handle,
+ PAM_OLDAUTHTOK, &old_password,
+ PAM_AUTHTOK, &new_password);
+ if (r != PAM_SUCCESS)
+ return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get cached passwords: @PAMERR@");
if (isempty(new_password)) {
/* No, it's not cached, then let's ask for the password and its verification, and cache
@@ -1000,7 +1092,7 @@ _public_ PAM_EXTERN int pam_sm_chauthtok(
}
/* Now everything is cached and checked, let's exit from the preliminary check */
- if (FLAGS_SET(flags, PAM_PRELIM_CHECK))
+ if (FLAGS_SET(sm_flags, PAM_PRELIM_CHECK))
return PAM_SUCCESS;
old_secret = user_record_new();
@@ -1021,6 +1113,11 @@ _public_ PAM_EXTERN int pam_sm_chauthtok(
if (r < 0)
return pam_syslog_errno(handle, LOG_ERR, r, "Failed to store new password: %m");
+ _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ r = pam_acquire_bus_connection(handle, "pam-systemd-home", &bus, NULL);
+ if (r != PAM_SUCCESS)
+ 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;
diff --git a/src/home/test-homed-regression-31896.c b/src/home/test-homed-regression-31896.c
new file mode 100644
index 0000000..1530a2f
--- /dev/null
+++ b/src/home/test-homed-regression-31896.c
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bus-locator.h"
+#include "main-func.h"
+#include "tests.h"
+
+static int run(int argc, char **argv) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *ref = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ const char *username = NULL;
+
+ /* This is a regression test for the following bug:
+ * https://github.com/systemd/systemd/pull/31896
+ * It is run as part of TEST-46-HOMED
+ */
+
+ test_setup_logging(LOG_DEBUG);
+ assert_se(sd_bus_open_system(&bus) >= 0);
+
+ assert_se(argc == 2);
+ username = argv[1];
+
+ assert_se(bus_call_method(bus, bus_home_mgr, "RefHomeUnrestricted", NULL, &ref, "sb", username, true) >= 0);
+
+ assert_se(bus_call_method_async(bus, NULL, bus_home_mgr, "AuthenticateHome", NULL, NULL, "ss", username, "{}") >= 0);
+ assert_se(sd_bus_flush(bus) >= 0);
+
+ (void) bus_call_method(bus, bus_home_mgr, "ReleaseHome", &error, NULL, "s", username);
+ assert_se(!sd_bus_error_has_name(&error, SD_BUS_ERROR_NO_REPLY)); /* Make sure we didn't crash */
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c
index dd099a0..25618d0 100644
--- a/src/home/user-record-sign.c
+++ b/src/home/user-record-sign.c
@@ -136,11 +136,11 @@ int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) {
return -EIO;
if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) {
- n_bad ++;
+ n_bad++;
continue;
}
- n_good ++;
+ n_good++;
}
return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) :
diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c
index 089cbb1..3ae0883 100644
--- a/src/home/user-record-util.c
+++ b/src/home/user-record-util.c
@@ -3,6 +3,7 @@
#include <sys/xattr.h>
#include "errno-util.h"
+#include "fd-util.h"
#include "home-util.h"
#include "id128-util.h"
#include "libcrypt-util.h"
@@ -10,6 +11,7 @@
#include "recovery-key.h"
#include "mountpoint-util.h"
#include "path-util.h"
+#include "sha256.h"
#include "stat-util.h"
#include "user-record-util.h"
#include "user-util.h"
@@ -282,7 +284,7 @@ int user_record_add_binding(
gid_t gid) {
_cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL;
- _cleanup_free_ char *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL;
+ _cleanup_free_ char *blob = NULL, *ip = NULL, *hd = NULL, *ip_auto = NULL, *lc = NULL, *lcm = NULL, *fst = NULL;
sd_id128_t mid;
int r;
@@ -291,6 +293,10 @@ int user_record_add_binding(
if (!h->json)
return -EUNATCH;
+ blob = path_join(home_system_blob_dir(), h->user_name);
+ if (!blob)
+ return -ENOMEM;
+
r = sd_id128_get_machine(&mid);
if (r < 0)
return r;
@@ -331,6 +337,7 @@ int user_record_add_binding(
r = json_build(&new_binding_entry,
JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("blobDirectory", JSON_BUILD_STRING(blob)),
JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)),
JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(partition_uuid))),
JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUuid", JSON_BUILD_STRING(SD_ID128_TO_UUID_STRING(luks_uuid))),
@@ -370,6 +377,8 @@ int user_record_add_binding(
if (r < 0)
return r;
+ free_and_replace(h->blob_directory, blob);
+
if (storage >= 0)
h->storage = storage;
@@ -428,7 +437,7 @@ int user_record_test_home_directory(UserRecord *h) {
if (r == 0)
return -ENOTDIR;
- r = path_is_mount_point(hd, NULL, 0);
+ r = path_is_mount_point(hd);
if (r < 0)
return r;
if (r > 0)
@@ -1155,6 +1164,7 @@ int user_record_merge_secret(UserRecord *h, UserRecord *secret) {
int r;
assert(h);
+ assert(secret);
/* Merges the secrets from 'secret' into 'h'. */
@@ -1382,6 +1392,15 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error) {
if (hr->service && !streq(hr->service, "io.systemd.Home"))
return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home.");
+ if (hr->blob_directory) {
+ /* This function is always called w/o binding section, so if hr->blob_dir is set then the caller set it themselves */
+ assert((hr->mask & USER_RECORD_BINDING) == 0);
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage custom blob directories.");
+ }
+
+ if (json_variant_by_key(hr->json, HOMEWORK_BLOB_FDMAP_FIELD))
+ return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record contains unsafe internal fields.");
+
return 0;
}
@@ -1510,3 +1529,103 @@ int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) {
h->mask |= USER_RECORD_PER_MACHINE;
return 0;
}
+
+int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_hashmap_free_ Hashmap *manifest = NULL;
+ const char *filename;
+ void *key, *value;
+ uint64_t total_size = 0;
+ int r;
+
+ assert(h);
+ assert(h->json);
+ assert(blobs);
+ assert(ret_failed);
+
+ /* Ensures that blobManifest exists (possibly creating it using the
+ * contents of blobs), and that the set of keys in both hashmaps are
+ * exactly the same. If it fails to handle one blob file, the filename
+ * is put it ret_failed for nicer error reporting. ret_failed is a pointer
+ * to the same memory blobs uses to store its keys, so it is valid for
+ * as long as blobs is valid and the corresponding key isn't removed! */
+
+ if (h->blob_manifest) {
+ /* blobManifest already exists. In this case we verify
+ * that the sets of keys are equal and that's it */
+
+ HASHMAP_FOREACH_KEY(value, key, h->blob_manifest)
+ if (!hashmap_contains(blobs, key))
+ return -EINVAL;
+ HASHMAP_FOREACH_KEY(value, key, blobs)
+ if (!hashmap_contains(h->blob_manifest, key))
+ return -EINVAL;
+
+ return 0;
+ }
+
+ /* blobManifest doesn't exist, so we need to create it */
+
+ HASHMAP_FOREACH_KEY(value, filename, blobs) {
+ _cleanup_free_ char *filename_dup = NULL;
+ _cleanup_free_ uint8_t *hash = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *hash_json = NULL;
+ int fd = PTR_TO_FD(value);
+ off_t initial, size;
+
+ *ret_failed = filename;
+
+ filename_dup = strdup(filename);
+ if (!filename_dup)
+ return -ENOMEM;
+
+ hash = malloc(SHA256_DIGEST_SIZE);
+ if (!hash)
+ return -ENOMEM;
+
+ initial = lseek(fd, 0, SEEK_CUR);
+ if (initial < 0)
+ return -errno;
+
+ r = sha256_fd(fd, BLOB_DIR_MAX_SIZE, hash);
+ if (r < 0)
+ return r;
+
+ size = lseek(fd, 0, SEEK_CUR);
+ if (size < 0)
+ return -errno;
+ if (!DEC_SAFE(&size, initial))
+ return -EOVERFLOW;
+
+ if (!INC_SAFE(&total_size, size))
+ total_size = UINT64_MAX;
+ if (total_size > BLOB_DIR_MAX_SIZE)
+ return -EFBIG;
+
+ if (lseek(fd, initial, SEEK_SET) < 0)
+ return -errno;
+
+ r = json_variant_new_hex(&hash_json, hash, SHA256_DIGEST_SIZE);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename_dup, hash);
+ if (r < 0)
+ return r;
+ TAKE_PTR(filename_dup); /* Ownership transfers to hashmap */
+ TAKE_PTR(hash);
+
+ r = json_variant_set_field(&v, filename, hash_json);
+ if (r < 0)
+ return r;
+
+ *ret_failed = NULL;
+ }
+
+ r = json_variant_set_field_non_null(&h->json, "blobManifest", v);
+ if (r < 0)
+ return r;
+
+ h->blob_manifest = TAKE_PTR(manifest);
+ return 0;
+}
diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h
index 508e2bd..1295a8e 100644
--- a/src/home/user-record-util.h
+++ b/src/home/user-record-util.h
@@ -6,6 +6,11 @@
#include "user-record.h"
#include "group-record.h"
+/* We intentionally use snake_case instead of the usual camelCase here to further
+ * reduce the chance of collision with a field any legitimate user record may ever
+ * want to set. */
+#define HOMEWORK_BLOB_FDMAP_FIELD "__systemd_homework_internal_blob_fdmap"
+
int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid);
int group_record_synthesize(GroupRecord *g, UserRecord *u);
@@ -63,3 +68,5 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error);
bool user_record_shall_rebalance(UserRecord *h);
int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight);
+
+int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed);