From 6f6ca5a224b745258456e6a869ba0504c6613db7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:48:29 +0200 Subject: [PATCH] logind: automatically pull in systemd-pcrlogin@.service on first login --- src/login/logind-dbus.c | 2 +- src/login/logind-user.c | 62 +++++++++++++++++++++++++++++- src/login/logind-user.h | 7 ++++ units/user-runtime-dir@.service.in | 5 ++- units/user@.service.in | 4 +- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index 5ff5a1f656..b773dbfa42 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -4321,7 +4321,7 @@ int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *err /* If the user is stopping, we're tracking stop jobs here. So don't send reply. */ if (!user->stopping) { char **user_job; - FOREACH_ARGUMENT(user_job, &user->runtime_dir_job, &user->service_manager_job) + FOREACH_ARGUMENT(user_job, &user->runtime_dir_job, &user->service_manager_job, &user->measure_job) if (streq_ptr(path, *user_job)) { *user_job = mfree(*user_job); diff --git a/src/login/logind-user.c b/src/login/logind-user.c index 11ca04dc8d..c0342dfe16 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -10,6 +10,7 @@ #include "bus-locator.h" #include "bus-util.h" #include "clean-ipc.h" +#include "efi-loader.h" #include "env-file.h" #include "errno-util.h" #include "escape.h" @@ -84,6 +85,10 @@ int user_new(Manager *m, UserRecord *ur, User **ret) { if (r < 0) return r; + r = unit_name_build("systemd-pcrlogin", lu, ".service", &u->measure_unit); + if (r < 0) + return r; + r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u); if (r < 0) return r; @@ -100,6 +105,10 @@ int user_new(Manager *m, UserRecord *ur, User **ret) { if (r < 0) return r; + r = hashmap_put(m->user_units, u->measure_unit, u); + if (r < 0) + return r; + *ret = TAKE_PTR(u); return 0; } @@ -118,15 +127,21 @@ User *user_free(User *u) { if (u->service_manager_unit) { (void) hashmap_remove_value(u->manager->user_units, u->service_manager_unit, u); - free(u->service_manager_job); free(u->service_manager_unit); } + free(u->service_manager_job); if (u->runtime_dir_unit) { (void) hashmap_remove_value(u->manager->user_units, u->runtime_dir_unit, u); - free(u->runtime_dir_job); free(u->runtime_dir_unit); } + free(u->runtime_dir_job); + + if (u->measure_unit) { + (void) hashmap_remove_value(u->manager->user_units, u->measure_unit, u); + free(u->measure_unit); + } + free(u->measure_job); if (u->slice) { (void) hashmap_remove_value(u->manager->user_units, u->slice, u); @@ -177,6 +192,7 @@ static int user_save_internal(User *u) { env_file_fputs_assignment(f, "RUNTIME=", u->runtime_path); env_file_fputs_assignment(f, "RUNTIME_DIR_JOB=", u->runtime_dir_job); env_file_fputs_assignment(f, "SERVICE_JOB=", u->service_manager_job); + env_file_fputs_assignment(f, "MEASURE_JOB=", u->measure_job); if (u->display) env_file_fputs_assignment(f, "DISPLAY=", u->display->id); @@ -303,6 +319,7 @@ int user_load(User *u) { r = parse_env_file(NULL, u->state_file, "RUNTIME_DIR_JOB", &u->runtime_dir_job, "SERVICE_JOB", &u->service_manager_job, + "MEASURE_JOB", &u->measure_job, "STOPPING", &stopping, "REALTIME", &realtime, "MONOTONIC", &monotonic, @@ -358,6 +375,39 @@ static int user_start_runtime_dir(User *u) { return 0; } +static int user_start_measure(User *u) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(u); + assert(!u->stopping); + assert(u->manager); + assert(u->measure_unit); + + /* Measures the user's record into the 'login' TPM2 NvPCR on the first login of this boot. This is + * strictly best-effort: a measurement failure must never block the login (simply because NvPCRs might + * not be available). Unlike the runtime-dir and service-manager units, this one is started but never + * stopped (see user_stop_service()): it is a Type=oneshot/RemainAfterExit unit living in + * system.slice that stays active until reboot, so a later re-login's StartUnit is a no-op and every + * user is measured exactly once per boot. + * + * Only bother if we're actually running in measured-OS mode. The unit itself also carries + * ConditionSecurity=measured-os, but checking here avoids queuing a no-op job on every first login + * on systems that don't measure the OS */ + if (efi_measured_os(LOG_DEBUG) <= 0) + return 0; + + u->measure_job = mfree(u->measure_job); + + r = manager_start_unit(u->manager, u->measure_unit, &error, &u->measure_job); + if (r < 0) + return log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_UNIT_MASKED) ? LOG_DEBUG : LOG_WARNING, + r, "Failed to start user measurement service '%s', ignoring: %s", + u->measure_unit, bus_error_message(&error, r)); + + return 0; +} + static bool user_wants_service_manager(const User *u) { assert(u); @@ -512,6 +562,14 @@ int user_start(User *u) { /* Set slice parameters */ (void) user_update_slice(u); + /* Measure the user record into the 'login' NvPCR. We do this *before* starting the runtime dir + * (and, further below, the service manager): manager_start_unit() is synchronous, so by the + * time those units are enqueued the measurement's start job already exists, and their + * After=systemd-pcrlogin@%i.service ordering then guarantees the measurement completes before + * the user's session becomes available. Best-effort and never stopped again, so each user is + * measured exactly once per boot. */ + (void) user_start_measure(u); + (void) user_start_runtime_dir(u); } diff --git a/src/login/logind-user.h b/src/login/logind-user.h index b38af08744..2ae8faf375 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -43,6 +43,13 @@ typedef struct User { char *service_manager_unit; char *service_manager_job; + /* systemd-pcrlogin@UID.service — measures the user record into the 'login' NvPCR on first login. + * Unlike the units above, logind starts this but never stops it: it must run exactly once per boot, + * so it lives in system.slice, stays active until reboot, and is intentionally absent from + * user_stop_service(). */ + char *measure_unit; + char *measure_job; + Session *display; dual_timestamp timestamp; /* When this User object was 'started' the first time */ diff --git a/units/user-runtime-dir@.service.in b/units/user-runtime-dir@.service.in index 241e9267bb..d83b32570d 100644 --- a/units/user-runtime-dir@.service.in +++ b/units/user-runtime-dir@.service.in @@ -10,7 +10,10 @@ [Unit] Description=User Runtime Directory /run/user/%i Documentation=man:user@.service(5) -After=systemd-logind.service dbus.service +# Order after the user record measurement (started by logind before this unit), so that the user's record is +# measured into the 'login' TPM2 NvPCR before the runtime dir — and thus the user's session — becomes +# available. This is ordering only (no Wants=/Requires=): a measurement failure must never block login. +After=systemd-logind.service dbus.service systemd-pcrlogin@%i.service IgnoreOnIsolate=yes [Service] diff --git a/units/user@.service.in b/units/user@.service.in index 381ab2a0db..a58af4a7d0 100644 --- a/units/user@.service.in +++ b/units/user@.service.in @@ -11,7 +11,9 @@ Description=User Manager for UID %i Documentation=man:user@.service(5) BindsTo=user-runtime-dir@%i.service -After=systemd-logind.service user-runtime-dir@%i.service dbus.service systemd-oomd.service +# Order after the user record measurement (see user-runtime-dir@.service), so the user's record is measured +# into the 'login' TPM2 NvPCR before the user manager starts. Ordering only, so login is never blocked on it. +After=systemd-logind.service user-runtime-dir@%i.service dbus.service systemd-oomd.service systemd-pcrlogin@%i.service IgnoreOnIsolate=yes [Service]