passwords, not just the first. i.e. if there are multiple defined, prefer
unlocked over locked and prefer non-empty over empty.
-* homed: while a home dir is not activated generate slightly different NSS
- records for it, that reports the home dir as "/" and the shell as some binary
- provided by us. Then, when an SSH login happens and SSH permits it our binary
- is invoked. This binary can then talk to homed and activate the homedir if
- it's not around yet, prompting the user for a password. Once that succeeded
- we'll switch to the real user record, i.e. home dir and shell, and our tool
- exec()s the latter. Net effect: ssh'ing into a homed account will just work:
- we'll neatly prompt for the homedir's password if its needed. –– Building on
- this we could take this even further: since this tool will potentially have
- access to the client's ssh-agent (if ssh-agent forwarding is enabled) we
+* homed: if the homed shell fallback thing has access to an SSH agent, try to
+ use it to unlock home dir (if ssh-agent forwarding is enabled). We
could implement SSH unlocking of a homedir with that: when enrolling a new
ssh pubkey in a user record we'd ask the ssh-agent to sign some random value
with the privkey, then use that as luks key to unlock the home dir. Will not
`fileSystemType` → The file system type backing the home directory: a short
string, such as "btrfs", "ext4", "xfs".
+`fallbackShell`, `fallbackHomeDirectory` → These fields have the same contents
+and format as the `shell` and `homeDirectory` fields (see above). When the
+`useFallback` field (see below) is set to true, the data from these fields
+should override the fields of the same name without the `fallback` prefix.
+
+`useFallback` → A boolean that allows choosing between the regular `shell` and
+`homeDirectory` fields or the fallback fields of the same name (see above). If
+`true` the fallback fields should be used in place of the regular fields, if
+`false` or unset the regular fields should be used. This mechanism is used for
+enable subsystems such as SSH to allow logins into user accounts, whose homed
+directories need further unlocking (because the SSH native authentication
+cannot release a suitabable disk encryption key), which the fallback shell
+provides.
+
## Fields in the `signature` section
As mentioned, the `signature` section of the user record may contain one or
out b incomplete,
out o bus_path);
ListHomes(out a(susussso) home_areas);
- @org.freedesktop.systemd1.Privileged("true")
ActivateHome(in s user_name,
in s secret);
+ ActivateHomeIfReferenced(in s user_name,
+ in s secret);
@org.freedesktop.systemd1.Privileged("true")
DeactivateHome(in s user_name);
RegisterHome(in s user_record);
in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
+ RefHomeUnrestricted(in s user_name,
+ in b please_suspend,
+ out h send_fd);
+ @org.freedesktop.systemd1.Privileged("true")
ReleaseHome(in s user_name);
InhibitSuspendHome(in s user_name,
out h send_fd);
<variablelist class="dbus-method" generated="True" extra-ref="ActivateHome()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="ActivateHomeIfReferenced()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="DeactivateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RegisterHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RefHome()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="RefHomeUnrestricted()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="ReleaseHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="InhibitSuspendHome()"/>
<classname>org.freedesktop.home1.Home</classname> interface documented below, but may be called on the
manager object and takes a user name as additional argument, instead.</para>
+ <para><function>ActivateHomeIfReferenced()</function> is identical to
+ <function>ActivateHome()</function>. However, the call only succeeds if the home directory is currently
+ referenced. Useful in conjunction with <function>RefHomeUnrestricted()</function>, which allows
+ creating a reference to a home directory even if the home directory is not active.</para>
+
<para><function>DeactivateHome()</function> deactivates (i.e. unmounts) the home directory of the
specified user. It is equivalent to the <function>Deactivate()</function> method on the
<classname>org.freedesktop.home1.Home</classname> interface documented below.</para>
<function>Ref()</function> on the <classname>org.freedesktop.home1.Home</classname>
interface.</para>
+ <para><function>RefHomeUnrestricted()</function> is identical to <function>RefHome()</function> but
+ succeeds even if the home area is not active currently. This is useful on conjunction with
+ <function>ActivateHomeIfReferenced()</function>.</para>
+
<para><function>ReleaseHome()</function> releases a home directory again, if all file descriptors
referencing it are already closed, that where acquired through <function>AcquireHome()</function> or
<function>RefHome()</function>. Note that this call does not actually cause the deactivation of the
node /org/freedesktop/home1/home {
interface org.freedesktop.home1.Home {
methods:
- @org.freedesktop.systemd1.Privileged("true")
Activate(in s secret);
+ ActivateIfReferenced(in s secret);
@org.freedesktop.systemd1.Privileged("true")
Deactivate();
Unregister();
Ref(in b please_suspend,
out h send_fd);
@org.freedesktop.systemd1.Privileged("true")
+ RefUnrestricted(in b please_suspend,
+ out h send_fd);
+ @org.freedesktop.systemd1.Privileged("true")
Release();
InhibitSuspend(out h send_fd);
properties:
<variablelist class="dbus-method" generated="True" extra-ref="Activate()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="ActivateIfReferenced()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="Deactivate()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Unregister()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Ref()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="RefUnrestricted()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="Release()"/>
<variablelist class="dbus-method" generated="True" extra-ref="InhibitSuspend()"/>
<refsect2>
<title>Methods</title>
- <para><function>Activate()</function>, <function>Deactivate()</function>,
- <function>Unregister()</function>, <function>Realize()</function>, <function>Remove()</function>,
- <function>Fixate()</function>, <function>Authenticate()</function>, <function>Update()</function>,
- <function>Resize()</function>, <function>ChangePassword()</function>, <function>Lock()</function>,
- <function>Unlock()</function>, <function>Acquire()</function>, <function>Ref()</function>,
- <function>Release()</function>, <function>InhibitSuspend()</function> operate like their matching counterparts on the
+ <para><function>Activate()</function>, <function>ActivateIfReferenced()</function>,
+ <function>Deactivate()</function>, <function>Unregister()</function>, <function>Realize()</function>,
+ <function>Remove()</function>, <function>Fixate()</function>, <function>Authenticate()</function>,
+ <function>Update()</function>, <function>Resize()</function>, <function>ChangePassword()</function>,
+ <function>Lock()</function>, <function>Unlock()</function>, <function>Acquire()</function>,
+ <function>Ref()</function>, <function>RefUnrestricted()</function>, <function>Release()</function>,
+ <function>InhibitSuspend()</function> operate like their matching counterparts on the
<classname>org.freedesktop.home1.Manager</classname> interface (see above). The main difference is that
they are methods of the home directory objects, and hence carry no additional user name
parameter. Which of the two flavors of methods to call depends on the handles to the user known on the
<title>History</title>
<refsect2>
<title>The Manager Object</title>
- <para><function>InhibitSuspendHome()</function> was added in version 256.</para>
+ <para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function> wer added in version 256.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>
- <para><function>InhibitSuspend()</function> was added in version 256.</para>
+ <para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function> and <function>RefUnrestricted()</function> were added in version 256.</para>
</refsect2>
</refsect1>
TakeControl(in b force);
ReleaseControl();
SetType(in s type);
+ SetClass(in s class);
SetDisplay(in s display);
SetTTY(in h tty_fd);
TakeDevice(in u major,
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u Audit = ...;
readonly s Type = '...';
- @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s Class = '...';
readonly b Active = ...;
readonly s State = '...';
<variablelist class="dbus-method" generated="True" extra-ref="SetType()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="SetClass()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="SetDisplay()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetTTY()"/>
connection. This should help prevent a session from entering an inconsistent state, for example if the
controller crashes. The only argument <varname>type</varname> is the new session type.</para>
+ <para><function>SetClass()</function> allows the caller to change the class of the session dynamically.
+ It may only be called by session's owening user. Currently, this call may be exclusively used to change
+ the class from <literal>user-incomplete</literal> to <literal>user</literal>. The call is synchronous,
+ and will return only once the user's service manager has successfully been started, if necessary. The
+ only argument <varname>type</varname> is the new session type.</para>
+
<para><function>SetDisplay()</function> allows the display name of the graphical session to be changed. This is
useful if the display server is started as part of the session. It can only be called by session's current
controller. If <function>TakeControl()</function> has not been called, this method will fail. The only argument
<title>Session Objects</title>
<para><function>SetDisplay()</function> was added in version 252.</para>
<para><function>SetTTY()</function> was added in version 254.</para>
+ <para><function>SetClass()</function> was added in version 256.</para>
</refsect2>
</refsect1>
</refentry>
<entry><constant>user-early</constant></entry>
<entry>Similar to <literal>user</literal> but sessions of this class are not ordered after <filename>systemd-user-sessions.service</filename>, i.e. may be started before regular sessions are allowed to be established. This session class is the default for sessions of the root user that would otherwise qualify for the <constant>user</constant> class, see above. (Added in v256.)</entry>
</row>
+ <row>
+ <entry><constant>user-incomplete</constant></entry>
+ <entry>Similar to <literal>user</literal> but for sessions which are not fully set up yet, i.e. have no home directory mounted ot similar. This is used by <citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry> to allow users to log in via <command>ssh</command> before their home directory is mounted, delaying the mount until the user provided the unlock password. Sessions of this class are upgraded to the regular <constant>user</constant> class once the home directory is activated.</entry>
+ </row>
<row>
<entry><constant>greeter</constant></entry>
<entry>Similar to <literal>user</literal> but for sessions that are spawned by a display manager ephemerally and which prompt the user for login credentials.</entry>
<para><function>sd_session_get_class()</function> may be used to determine the class of the session
identified by the specified session identifier. The returned string is one of <literal>user</literal>,
- <literal>user-early</literal>, <literal>greeter</literal>, <literal>lock-screen</literal>,
- <literal>background</literal>, <literal>background-light</literal>, <literal>manager</literal> or
- <literal>manager-early</literal> and needs to be freed with the libc <citerefentry
+ <literal>user-early</literal>, <literal>user-incomplete</literal>, <literal>greeter</literal>,
+ <literal>lock-screen</literal>, <literal>background</literal>, <literal>background-light</literal>,
+ <literal>manager</literal> or <literal>manager-early</literal> and needs to be freed with the libc
+ <citerefentry
project='man-pages'><refentrytitle>free</refentrytitle><manvolnum>3</manvolnum></citerefentry> call after
use.</para>
</listitem>
<listitem>
<para>when a <command>stop</command>-ped, <command>disable</command>-d, or <command>mask</command>-ed
- unit still has active triggering units.</para>
+ unit still has active triggering units,</para>
+ </listitem>
+ <listitem>
+ <para>when a unit file is changed and requires <command>daemon-reload</command>.</para>
</listitem>
</itemizedlist>
</para>
<varlistentry>
<term><varname>IgnoreSIGPIPE=</varname></term>
- <listitem><para>Takes a boolean argument. If true, causes <constant>SIGPIPE</constant> to be ignored in the
- executed process. Defaults to true because <constant>SIGPIPE</constant> generally is useful only in shell
- pipelines.</para></listitem>
+ <listitem><para>Takes a boolean argument. If true, <constant>SIGPIPE</constant> is ignored in the
+ executed process. Defaults to true since <constant>SIGPIPE</constant> is generally only useful in
+ shell pipelines.</para></listitem>
</varlistentry>
</variablelist>
#include "bootctl-reboot-to-firmware.h"
#include "efi-api.h"
+#include "errno-util.h"
#include "parse-util.h"
int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) {
puts("supported");
return 1; /* recognizable error #1 */
}
- if (r == -EOPNOTSUPP) {
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
puts("not supported");
return 2; /* recognizable error #2 */
}
return 0;
}
}
+
+int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ static const JsonDispatch dispatch_table[] = {
+ { "state", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, 0, 0 },
+ {}
+ };
+ bool b;
+ int r;
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &b);
+ if (r != 0)
+ return r;
+
+ r = efi_set_reboot_to_firmware(b);
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, NULL);
+}
+
+int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ int r;
+
+ if (json_variant_elements(parameters) > 0)
+ return varlink_error_invalid_parameter(link, parameters);
+
+ r = efi_get_reboot_to_firmware();
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
+ if (r < 0)
+ return r;
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_BOOLEAN("state", r)));
+}
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "varlink.h"
+
int verb_reboot_to_firmware(int argc, char *argv[], void *userdata);
+
+int vl_method_set_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
+int vl_method_get_reboot_to_firmware(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
dev_t esp_devid = 0, xbootldr_devid = 0;
int r, k;
- r = acquire_esp(/* unprivileged_mode= */ -1, /* graceful= */ false, NULL, NULL, NULL, &esp_uuid, &esp_devid);
+ r = acquire_esp(/* unprivileged_mode= */ -1,
+ /* graceful= */ false,
+ /* ret_part= */ NULL,
+ /* ret_pstart= */ NULL,
+ /* ret_psize= */ NULL,
+ &esp_uuid,
+ &esp_devid);
if (arg_print_esp_path) {
if (r == -EACCES) /* If we couldn't acquire the ESP path, log about access errors (which is the only
* error the find_esp_and_warn() won't log on its own) */
return 0;
}
- r = acquire_xbootldr(/* unprivileged_mode= */ -1, &xbootldr_uuid, &xbootldr_devid);
+ r = acquire_xbootldr(
+ /* unprivileged_mode= */ -1,
+ &xbootldr_uuid,
+ &xbootldr_devid);
if (arg_print_dollar_boot_path) {
if (r == -EACCES)
return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
int verb_unlink(int argc, char *argv[], void *userdata) {
return verb_list(argc, argv, userdata);
}
+
+int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+ dev_t esp_devid = 0, xbootldr_devid = 0;
+ int r;
+
+ assert(link);
+
+ if (json_variant_elements(parameters) > 0)
+ return varlink_error_invalid_parameter(link, parameters);
+
+ r = acquire_esp(/* unprivileged_mode= */ false,
+ /* graceful= */ false,
+ /* ret_part= */ NULL,
+ /* ret_pstart= */ NULL,
+ /* ret_psize= */ NULL,
+ /* ret_uuid=*/ NULL,
+ &esp_devid);
+ if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */
+ return log_error_errno(r, "Failed to determine ESP location: %m");
+ if (r < 0)
+ return r;
+
+ r = acquire_xbootldr(
+ /* unprivileged_mode= */ false,
+ /* ret_uuid= */ NULL,
+ &xbootldr_devid);
+ if (r == -EACCES)
+ return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
+ if (r < 0)
+ return r;
+
+ r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid);
+ if (r < 0)
+ return r;
+
+ _cleanup_(json_variant_unrefp) JsonVariant *previous = NULL;
+ for (size_t i = 0; i < config.n_entries; i++) {
+ if (previous) {
+ r = varlink_notifyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_VARIANT("entry", previous)));
+ if (r < 0)
+ return r;
+
+ previous = json_variant_unref(previous);
+ }
+
+ r = boot_entry_to_json(&config, i, &previous);
+ if (r < 0)
+ return r;
+ }
+
+ return varlink_replyb(link, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_CONDITION(previous, "entry", JSON_BUILD_VARIANT(previous))));
+}
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "varlink.h"
+
int verb_status(int argc, char *argv[], void *userdata);
int verb_list(int argc, char *argv[], void *userdata);
int verb_unlink(int argc, char *argv[], void *userdata);
+
+int vl_method_list_boot_entries(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata);
#include "parse-argument.h"
#include "pretty-print.h"
#include "utf8.h"
+#include "varlink.h"
+#include "varlink-io.systemd.BootControl.h"
#include "verbs.h"
#include "virt.h"
char *arg_efi_boot_option_description = NULL;
bool arg_dry_run = false;
ImagePolicy *arg_image_policy = NULL;
+bool arg_varlink = false;
STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry is only supported with --unlink or --cleanup");
+ r = varlink_invocation(VARLINK_ALLOW_ACCEPT);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
+ if (r > 0) {
+ arg_varlink = true;
+ arg_pager_flags |= PAGER_DISABLE;
+ }
+
return 1;
}
if (r <= 0)
return r;
+ if (arg_varlink) {
+ _cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL;
+
+ /* Invocation as Varlink service */
+
+ r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_BootControl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = varlink_server_bind_method_many(
+ varlink_server,
+ "io.systemd.BootControl.ListBootEntries", vl_method_list_boot_entries,
+ "io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware,
+ "io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+ r = varlink_server_loop_auto(varlink_server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return EXIT_SUCCESS;
+ }
+
if (arg_print_root_device > 0) {
_cleanup_free_ char *path = NULL;
dev_t devno;
extern char *arg_efi_boot_option_description;
extern bool arg_dry_run;
extern ImagePolicy *arg_image_policy;
+extern bool arg_varlink;
static inline const char *arg_dollar_boot_path(void) {
/* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */
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)
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 },
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;
_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,
+ /* interctive= */ false,
+ h->uid,
+ &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;
_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;
- return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name);
+ 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;
+
+ 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 ? HOME_FIFO_PLEASE_SUSPEND : HOME_FIFO_DONT_SUSPEND);
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",
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_METHOD_WITH_ARGS("InhibitSuspend",
SD_BUS_NO_ARGS,
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:
return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error);
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)),
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);
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,
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,
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
send_interface="org.freedesktop.home1.Manager"
send_member="ActivateHome"/>
+ <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"/>
send_interface="org.freedesktop.home1.Manager"
send_member="RefHome"/>
+ <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"/>
send_interface="org.freedesktop.home1.Home"
send_member="Activate"/>
+ <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"/>
send_interface="org.freedesktop.home1.Home"
send_member="Ref"/>
+ <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_active>auth_admin_keep</allow_active>
</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>
typedef enum AcquireHomeFlags {
ACQUIRE_MUST_AUTHENTICATE = 1 << 0,
ACQUIRE_PLEASE_SUSPEND = 1 << 1,
+ ACQUIRE_REF_ANYWAY = 1 << 2,
} AcquireHomeFlags;
static int parse_argv(
PamBusData **bus_data) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL;
- bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false;
+ bool do_auth = FLAGS_SET(flags, ACQUIRE_MUST_AUTHENTICATE), home_not_active = false, home_locked = false, unrestricted = false;
_cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
_cleanup_close_ int acquired_fd = -EBADF;
_cleanup_free_ char *fd_field = NULL;
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:
*
- * 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. */
+ * 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!
+ *
+ * 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)
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 all all previously used passwords again. */
for (;;) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ const char *method = NULL;
if (do_auth && !secret) {
const char *cached_password = NULL;
}
}
- 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);
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
* per-service PAM logic. In that case, print a friendly message and accept
* failure. */
- 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_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 (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.");
+ 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;
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. */
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;
}
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 sm_flags,
pam_debug_syslog(handle, debug, "pam-systemd-homed session start");
+ r = fallback_shell_can_work(handle, &flags);
+ 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;
pam_debug_syslog(handle, debug, "pam-systemd-homed account management");
- r = acquire_home(handle, flags, 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;
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_CANT_AUTHENTICATE, EKEYREVOKED),
SD_BUS_ERROR_MAP(BUS_ERROR_HOME_IN_USE, EADDRINUSE),
SD_BUS_ERROR_MAP(BUS_ERROR_REBALANCE_NOT_NEEDED, EALREADY),
+ SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_REFERENCED, EBADR),
SD_BUS_ERROR_MAP_END
};
#define BUS_ERROR_HOME_CANT_AUTHENTICATE "org.freedesktop.home1.HomeCantAuthenticate"
#define BUS_ERROR_HOME_IN_USE "org.freedesktop.home1.HomeInUse"
#define BUS_ERROR_REBALANCE_NOT_NEEDED "org.freedesktop.home1.RebalanceNotNeeded"
+#define BUS_ERROR_HOME_NOT_REFERENCED "org.freedesktop.home1.HomeNotReferenced"
BUS_ERROR_MAP_ELF_USE(bus_common_errors);
&user_object),
};
-static int session_jobs_reply(Session *s, uint32_t jid, const char *unit, const char *result) {
+static void session_jobs_reply(Session *s, uint32_t jid, const char *unit, const char *result) {
assert(s);
assert(unit);
if (!s->started)
- return 0;
+ return;
if (result && !streq(result, "done")) {
_cleanup_(sd_bus_error_free) sd_bus_error e = SD_BUS_ERROR_NULL;
sd_bus_error_setf(&e, BUS_ERROR_JOB_FAILED,
"Job %u for unit '%s' failed with '%s'", jid, unit, result);
- return session_send_create_reply(s, &e);
+
+ (void) session_send_create_reply(s, &e);
+ (void) session_send_upgrade_reply(s, &e);
+ return;
}
- return session_send_create_reply(s, NULL);
+ (void) session_send_create_reply(s, /* error= */ NULL);
+ (void) session_send_upgrade_reply(s, /* error= */ NULL);
}
int match_job_removed(sd_bus_message *message, void *userdata, sd_bus_error *error) {
if (session) {
if (streq_ptr(path, session->scope_job)) {
session->scope_job = mfree(session->scope_job);
- (void) session_jobs_reply(session, id, unit, result);
+ session_jobs_reply(session, id, unit, result);
session_save(session);
user_save(session->user);
LIST_FOREACH(sessions_by_user, s, user->sessions)
/* Don't propagate user service failures to the client */
- (void) session_jobs_reply(s, id, unit, /* error = */ NULL);
+ session_jobs_reply(s, id, unit, /* error = */ NULL /* don't propagate user service failures to the client */);
user_save(user);
}
return sd_bus_reply_method_return(message, NULL);
}
+static int method_set_class(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
+ Session *s = ASSERT_PTR(userdata);
+ SessionClass class;
+ const char *c;
+ uid_t uid;
+ int r;
+
+ assert(message);
+
+ r = sd_bus_message_read(message, "s", &c);
+ if (r < 0)
+ return r;
+
+ class = session_class_from_string(c);
+ if (class < 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
+ "Invalid session class '%s'", c);
+
+ /* For now, we'll allow only upgrades user-incomplete → user */
+ if (class != SESSION_USER)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
+ "Class may only be set to 'user', refusing.");
+ if (s->class == SESSION_USER) /* No change, shortcut */
+ return sd_bus_reply_method_return(message, NULL);
+ if (s->class != SESSION_USER_INCOMPLETE)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
+ "Only sessions with class 'user-incomplete' may change class, refusing.");
+
+ if (s->upgrade_message)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
+ "Set session class operation already in progress, refsuing.");
+
+ 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;
+
+ if (uid != 0 && uid != s->user->user_record->uid)
+ return sd_bus_error_set(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change its class");
+
+ session_set_class(s, class);
+
+ sd_bus_message_unref(s->upgrade_message);
+ s->upgrade_message = sd_bus_message_ref(message);
+
+ r = session_send_upgrade_reply(s, /* error= */ NULL);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
static int method_set_display(sd_bus_message *message, void *userdata, sd_bus_error *error) {
Session *s = ASSERT_PTR(userdata);
const char *display;
false);
}
+int session_send_upgrade_reply(Session *s, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *c = NULL;
+ assert(s);
+
+ if (!s->upgrade_message)
+ return 0;
+
+ if (!sd_bus_error_is_set(error) && !session_ready(s))
+ return 0;
+
+ c = TAKE_PTR(s->upgrade_message);
+ if (error)
+ return sd_bus_reply_method_error(c, error);
+
+ session_save(s);
+
+ return sd_bus_reply_method_return(c, NULL);
+}
+
static const sd_bus_vtable session_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_PROPERTY("Leader", "u", bus_property_get_pid, offsetof(Session, leader.pid), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Audit", "u", NULL, offsetof(Session, audit_id), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Type", "s", property_get_type, offsetof(Session, type), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
- SD_BUS_PROPERTY("Class", "s", property_get_class, offsetof(Session, class), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("Class", "s", property_get_class, offsetof(Session, class), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Active", "b", property_get_active, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("State", "s", property_get_state, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("IdleHint", "b", property_get_idle_hint, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_NO_RESULT,
method_set_type,
SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetClass",
+ SD_BUS_ARGS("s", class),
+ SD_BUS_NO_RESULT,
+ method_set_class,
+ SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetDisplay",
SD_BUS_ARGS("s", display),
SD_BUS_NO_RESULT,
int session_send_lock_all(Manager *m, bool lock);
int session_send_create_reply(Session *s, sd_bus_error *error);
+int session_send_upgrade_reply(Session *s, sd_bus_error *error);
int bus_session_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error);
int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error);
session_reset_leader(s, /* keep_fdstore = */ true);
sd_bus_message_unref(s->create_message);
+ sd_bus_message_unref(s->upgrade_message);
free(s->tty);
free(s->display);
(void) session_send_changed(s, "Type", NULL);
}
+void session_set_class(Session *s, SessionClass c) {
+ assert(s);
+
+ if (s->class == c)
+ return;
+
+ s->class = c;
+ (void) session_save(s);
+ (void) session_send_changed(s, "Class", NULL);
+
+ /* This class change might mean we need the per-user session manager now. Try to start it */
+ user_start_service_manager(s->user);
+}
+
int session_set_display(Session *s, const char *display) {
int r;
static const char* const session_class_table[_SESSION_CLASS_MAX] = {
[SESSION_USER] = "user",
[SESSION_USER_EARLY] = "user-early",
+ [SESSION_USER_INCOMPLETE] = "user-incomplete",
[SESSION_GREETER] = "greeter",
[SESSION_LOCK_SCREEN] = "lock-screen",
[SESSION_BACKGROUND] = "background",
typedef enum SessionClass {
SESSION_USER, /* A regular user session */
SESSION_USER_EARLY, /* A user session, that is not ordered after systemd-user-sessions.service (i.e. for root) */
+ SESSION_USER_INCOMPLETE, /* A user session that is only half-way set up and doesn't pull in the service manager, and can be upgraded to a full user session later */
SESSION_GREETER, /* A login greeter pseudo-session */
SESSION_LOCK_SCREEN, /* A lock screen */
SESSION_BACKGROUND, /* Things like cron jobs, which are non-interactive */
#define SESSION_CLASS_IS_EARLY(class) IN_SET((class), SESSION_USER_EARLY, SESSION_MANAGER_EARLY)
/* Which session classes want their own scope units? (all of them, except the manager, which comes in its own service unit already */
-#define SESSION_CLASS_WANTS_SCOPE(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND, SESSION_BACKGROUND_LIGHT)
+#define SESSION_CLASS_WANTS_SCOPE(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_USER_INCOMPLETE, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND, SESSION_BACKGROUND_LIGHT)
/* Which session classes want their own per-user service manager? */
#define SESSION_CLASS_WANTS_SERVICE_MANAGER(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND)
bool was_active:1;
- sd_bus_message *create_message;
+ sd_bus_message *create_message; /* The D-Bus message used to create the session, which we haven't responded to yet */
+ sd_bus_message *upgrade_message; /* The D-Bus message used to upgrade the session class user-incomplete → user, which we haven't responded to yet */
/* Set up when a client requested to release the session via the bus */
sd_event_source *timer_event_source;
int session_get_locked_hint(Session *s);
int session_set_locked_hint(Session *s, bool b);
void session_set_type(Session *s, SessionType t);
+void session_set_class(Session *s, SessionClass c);
int session_set_display(Session *s, const char *display);
int session_set_tty(Session *s, const char *tty);
int session_create_fifo(Session *s);
send_interface="org.freedesktop.login1.Session"
send_member="SetType"/>
+ <allow send_destination="org.freedesktop.login1"
+ send_interface="org.freedesktop.login1.Session"
+ send_member="SetClass"/>
+
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Session"
send_member="TakeDevice"/>
return fallback;
}
+static bool getenv_harder_bool(pam_handle_t *handle, const char *key, bool fallback) {
+ const char *v;
+ int r;
+
+ assert(handle);
+ assert(key);
+
+ v = getenv_harder(handle, key, NULL);
+ if (isempty(v))
+ return fallback;
+
+ r = parse_boolean(v);
+ if (r < 0) {
+ pam_syslog(handle, LOG_ERR, "Boolean environment variable value of '%s' is not valid: %s", key, v);
+ return fallback;
+ }
+
+ return r;
+}
+
static int update_environment(pam_handle_t *handle, const char *key, const char *value) {
int r;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int session_fd = -EBADF, existing, r;
- bool debug = false, remote;
+ bool debug = false, remote, incomplete;
uint32_t vtnr = 0;
uid_t original_uid;
type = getenv_harder(handle, "XDG_SESSION_TYPE", type_pam);
class = getenv_harder(handle, "XDG_SESSION_CLASS", class_pam);
desktop = getenv_harder(handle, "XDG_SESSION_DESKTOP", desktop_pam);
+ incomplete = getenv_harder_bool(handle, "XDG_SESSION_INCOMPLETE", false);
if (streq_ptr(service, "systemd-user")) {
/* If we detect that we are running in the "systemd-user" PAM stack, then let's patch the class to
((IN_SET(user_record_disposition(ur), USER_INTRINSIC, USER_SYSTEM, USER_DYNAMIC) &&
streq(type, "tty")) ? "user-early" : "user");
+ if (incomplete) {
+ if (streq(class, "user"))
+ class = "user-incomplete";
+ else
+ pam_syslog_pam_error(handle, LOG_WARNING, 0, "PAM session of class '%s' is incomplete, which is not supported, ignoring.", class);
+ }
+
remote = !isempty(remote_host) && !is_localhost(remote_host);
r = pam_get_data(handle, "systemd.memory_max", (const void **)&memory_max);
return -status;
}
+int boot_entry_to_json(const BootConfig *c, size_t i, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ const BootEntry *e;
+ int r;
+
+ assert(c);
+ assert(ret);
+
+ if (i >= c->n_entries) {
+ *ret = NULL;
+ return 0;
+ }
+
+ e = c->entries + i;
+
+ r = json_variant_merge_objectb(
+ &v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("type", JSON_BUILD_STRING(boot_entry_type_json_to_string(e->type))),
+ JSON_BUILD_PAIR_CONDITION(e->id, "id", JSON_BUILD_STRING(e->id)),
+ JSON_BUILD_PAIR_CONDITION(e->path, "path", JSON_BUILD_STRING(e->path)),
+ JSON_BUILD_PAIR_CONDITION(e->root, "root", JSON_BUILD_STRING(e->root)),
+ JSON_BUILD_PAIR_CONDITION(e->title, "title", JSON_BUILD_STRING(e->title)),
+ JSON_BUILD_PAIR_CONDITION(boot_entry_title(e), "showTitle", JSON_BUILD_STRING(boot_entry_title(e))),
+ JSON_BUILD_PAIR_CONDITION(e->sort_key, "sortKey", JSON_BUILD_STRING(e->sort_key)),
+ JSON_BUILD_PAIR_CONDITION(e->version, "version", JSON_BUILD_STRING(e->version)),
+ JSON_BUILD_PAIR_CONDITION(e->machine_id, "machineId", JSON_BUILD_STRING(e->machine_id)),
+ JSON_BUILD_PAIR_CONDITION(e->architecture, "architecture", JSON_BUILD_STRING(e->architecture)),
+ JSON_BUILD_PAIR_CONDITION(e->kernel, "linux", JSON_BUILD_STRING(e->kernel)),
+ JSON_BUILD_PAIR_CONDITION(e->efi, "efi", JSON_BUILD_STRING(e->efi)),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->initrd), "initrd", JSON_BUILD_STRV(e->initrd)),
+ JSON_BUILD_PAIR_CONDITION(e->device_tree, "devicetree", JSON_BUILD_STRING(e->device_tree)),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->device_tree_overlay), "devicetreeOverlay", JSON_BUILD_STRV(e->device_tree_overlay))));
+ if (r < 0)
+ return log_oom();
+
+ /* Sanitizers (only memory sanitizer?) do not like function call with too many
+ * arguments and trigger false positive warnings. Let's not add too many json objects
+ * at once. */
+ r = json_variant_merge_objectb(
+ &v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("isReported", JSON_BUILD_BOOLEAN(e->reported_by_loader)),
+ JSON_BUILD_PAIR_CONDITION(e->tries_left != UINT_MAX, "triesLeft", JSON_BUILD_UNSIGNED(e->tries_left)),
+ JSON_BUILD_PAIR_CONDITION(e->tries_done != UINT_MAX, "triesDone", JSON_BUILD_UNSIGNED(e->tries_done)),
+ JSON_BUILD_PAIR_CONDITION(c->default_entry >= 0, "isDefault", JSON_BUILD_BOOLEAN(i == (size_t) c->default_entry)),
+ JSON_BUILD_PAIR_CONDITION(c->selected_entry >= 0, "isSelected", JSON_BUILD_BOOLEAN(i == (size_t) c->selected_entry))));
+
+ if (r < 0)
+ return log_oom();
+
+ r = json_cmdline(e, &c->global_addons, &v);
+ if (r < 0)
+ return log_oom();
+
+ *ret = TAKE_PTR(v);
+ return 1;
+}
+
int show_boot_entries(const BootConfig *config, JsonFormatFlags json_format) {
int r;
_cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
for (size_t i = 0; i < config->n_entries; i++) {
- const BootEntry *e = config->entries + i;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
- r = json_variant_merge_objectb(
- &v, JSON_BUILD_OBJECT(
- JSON_BUILD_PAIR("type", JSON_BUILD_STRING(boot_entry_type_json_to_string(e->type))),
- JSON_BUILD_PAIR_CONDITION(e->id, "id", JSON_BUILD_STRING(e->id)),
- JSON_BUILD_PAIR_CONDITION(e->path, "path", JSON_BUILD_STRING(e->path)),
- JSON_BUILD_PAIR_CONDITION(e->root, "root", JSON_BUILD_STRING(e->root)),
- JSON_BUILD_PAIR_CONDITION(e->title, "title", JSON_BUILD_STRING(e->title)),
- JSON_BUILD_PAIR_CONDITION(boot_entry_title(e), "showTitle", JSON_BUILD_STRING(boot_entry_title(e))),
- JSON_BUILD_PAIR_CONDITION(e->sort_key, "sortKey", JSON_BUILD_STRING(e->sort_key)),
- JSON_BUILD_PAIR_CONDITION(e->version, "version", JSON_BUILD_STRING(e->version)),
- JSON_BUILD_PAIR_CONDITION(e->machine_id, "machineId", JSON_BUILD_STRING(e->machine_id)),
- JSON_BUILD_PAIR_CONDITION(e->architecture, "architecture", JSON_BUILD_STRING(e->architecture)),
- JSON_BUILD_PAIR_CONDITION(e->kernel, "linux", JSON_BUILD_STRING(e->kernel)),
- JSON_BUILD_PAIR_CONDITION(e->efi, "efi", JSON_BUILD_STRING(e->efi)),
- JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->initrd), "initrd", JSON_BUILD_STRV(e->initrd)),
- JSON_BUILD_PAIR_CONDITION(e->device_tree, "devicetree", JSON_BUILD_STRING(e->device_tree)),
- JSON_BUILD_PAIR_CONDITION(!strv_isempty(e->device_tree_overlay), "devicetreeOverlay", JSON_BUILD_STRV(e->device_tree_overlay))));
- if (r < 0)
- return log_oom();
-
- r = json_cmdline(e, &config->global_addons, &v);
- if (r < 0)
- return log_oom();
-
- /* Sanitizers (only memory sanitizer?) do not like function call with too many
- * arguments and trigger false positive warnings. Let's not add too many json objects
- * at once. */
- r = json_variant_merge_objectb(
- &v, JSON_BUILD_OBJECT(
- JSON_BUILD_PAIR("isReported", JSON_BUILD_BOOLEAN(e->reported_by_loader)),
- JSON_BUILD_PAIR_CONDITION(e->tries_left != UINT_MAX, "triesLeft", JSON_BUILD_UNSIGNED(e->tries_left)),
- JSON_BUILD_PAIR_CONDITION(e->tries_done != UINT_MAX, "triesDone", JSON_BUILD_UNSIGNED(e->tries_done)),
- JSON_BUILD_PAIR_CONDITION(config->default_entry >= 0, "isDefault", JSON_BUILD_BOOLEAN(i == (size_t) config->default_entry)),
- JSON_BUILD_PAIR_CONDITION(config->selected_entry >= 0, "isSelected", JSON_BUILD_BOOLEAN(i == (size_t) config->selected_entry))));
-
+ r = boot_entry_to_json(config, i, &v);
if (r < 0)
return log_oom();
return log_oom();
}
- json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL);
-
+ return json_variant_dump(array, json_format | JSON_FORMAT_EMPTY_ARRAY, NULL, NULL);
} else {
for (size_t n = 0; n < config->n_entries; n++) {
r = show_boot_entry(
JsonFormatFlags json_format);
int boot_filename_extract_tries(const char *fname, char **ret_stripped, unsigned *ret_tries_left, unsigned *ret_tries_done);
+
+int boot_entry_to_json(const BootConfig *c, size_t i, JsonVariant **ret);
if (r < 0)
return r;
- /* The variable contains a series of individually NUL terminated UTF-16 strings. */
+ /* The variable contains a series of individually NUL terminated UTF-16 strings. We gracefully
+ * consider the final NUL byte optional (i.e. the last string may or may not end in a NUL byte).*/
for (size_t i = 0, start = 0;; i++) {
_cleanup_free_ char *decoded = NULL;
if (!end && entries[i] != 0)
continue;
+ /* Empty string at the end of variable? That's the trailer, we are done (i.e. we have a final
+ * NUL terminator). */
+ if (end && start == i)
+ break;
+
/* We reached the end of a string, let's decode it into UTF-8 */
decoded = utf16_to_utf8(entries + start, (i - start) * sizeof(char16_t));
if (!decoded)
} else
log_debug("Ignoring invalid loader entry '%s'.", decoded);
- /* We reached the end of the variable */
+ /* Exit the loop if we reached the end of the variable (i.e. we do not have a final NUL
+ * terminator) */
if (end)
break;
'varlink.c',
'varlink-idl.c',
'varlink-io.systemd.c',
+ 'varlink-io.systemd.BootControl.c',
'varlink-io.systemd.Credentials.c',
'varlink-io.systemd.Hostname.c',
'varlink-io.systemd.Journal.c',
printf(" Real Name: %s\n", hr->real_name);
hd = user_record_home_directory(hr);
- if (hd)
- printf(" Directory: %s\n", hd);
+ if (hd) {
+ printf(" Directory: %s", hd);
+
+ if (hr->fallback_home_directory && hr->use_fallback)
+ printf(" %s(fallback)%s", ansi_highlight_yellow(), ansi_normal());
+
+ printf("\n");
+ }
storage = user_record_storage(hr);
if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */
printf(" Removable: %s\n", yes_no(b));
shell = user_record_shell(hr);
- if (shell)
- printf(" Shell: %s\n", shell);
+ if (shell) {
+ printf(" Shell: %s", shell);
+
+ if (hr->fallback_shell && hr->use_fallback)
+ printf(" %s(fallback)%s", ansi_highlight_yellow(), ansi_normal());
+
+ printf("\n");
+ }
if (hr->email_address)
printf(" Email: %s\n", hr->email_address);
free(h->home_directory);
free(h->home_directory_auto);
+ free(h->fallback_shell);
+ free(h->fallback_home_directory);
+
strv_free(h->member_of);
strv_free(h->capability_bounding_set);
strv_free(h->capability_ambient_set);
static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch status_dispatch_table[] = {
- { "diskUsage", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
- { "diskFree", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
- { "diskSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
- { "diskCeiling", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
- { "diskFloor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
- { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
- { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
- { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
- { "goodAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
- { "badAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
- { "lastGoodAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
- { "lastBadAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
- { "rateLimitBeginUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
- { "rateLimitCount", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
- { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 },
- { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
- { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "diskUsage", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
+ { "diskFree", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
+ { "diskSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
+ { "diskCeiling", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
+ { "diskFloor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
+ { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
+ { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
+ { "goodAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
+ { "badAuthenticationCounter", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
+ { "lastGoodAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
+ { "lastBadAuthenticationUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
+ { "rateLimitBeginUSec", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
+ { "rateLimitCount", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
+ { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 },
+ { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "fallbackShell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, fallback_shell), 0 },
+ { "fallbackHomeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, fallback_home_directory), 0 },
+ { "useFallback", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, use_fallback), 0 },
{},
};
return h->access_mode != MODE_INVALID ? h->access_mode : 0700;
}
-const char* user_record_home_directory(UserRecord *h) {
+static const char *user_record_home_directory_real(UserRecord *h) {
assert(h);
if (h->home_directory)
return "/";
}
+const char* user_record_home_directory(UserRecord *h) {
+ assert(h);
+
+ if (h->use_fallback && h->fallback_home_directory)
+ return h->fallback_home_directory;
+
+ return user_record_home_directory_real(h);
+}
+
const char *user_record_image_path(UserRecord *h) {
assert(h);
if (h->image_path_auto)
return h->image_path_auto;
- return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL;
+ /* For some storage types the image is the home directory itself. (But let's ignore the fallback logic for it) */
+ return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ?
+ user_record_home_directory_real(h) : NULL;
}
const char *user_record_cifs_user_name(UserRecord *h) {
(h->nodev ? MS_NODEV : 0);
}
-const char *user_record_shell(UserRecord *h) {
+static const char *user_record_shell_real(UserRecord *h) {
assert(h);
if (h->shell)
return NOLOGIN;
}
+const char *user_record_shell(UserRecord *h) {
+ const char *shell;
+
+ assert(h);
+
+ shell = user_record_shell_real(h);
+
+ /* Return fallback shall if we are told so — except if the primary shell is already a nologin shell,
+ * then let's not risk anything. */
+ if (h->use_fallback && h->fallback_shell)
+ return is_nologin_shell(shell) ? NOLOGIN : h->fallback_shell;
+
+ return shell;
+}
+
const char *user_record_real_name(UserRecord *h) {
assert(h);
char *home_directory;
char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */
+ /* fallback shell and home dir */
+ char *fallback_shell;
+ char *fallback_home_directory;
+
uid_t uid;
gid_t gid;
uint64_t disk_ceiling;
uint64_t disk_floor;
+ bool use_fallback; /* if true → use fallback_shell + fallback_home_directory instead of the regular ones */
+
char *state;
char *service;
int signed_locally;
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "varlink-io.systemd.BootControl.h"
+
+static VARLINK_DEFINE_ENUM_TYPE(
+ BootEntryType,
+ VARLINK_DEFINE_ENUM_VALUE(type1),
+ VARLINK_DEFINE_ENUM_VALUE(type2),
+ VARLINK_DEFINE_ENUM_VALUE(loader),
+ VARLINK_DEFINE_ENUM_VALUE(auto));
+
+static VARLINK_DEFINE_STRUCT_TYPE(
+ BootEntry,
+ VARLINK_DEFINE_FIELD_BY_TYPE(type, BootEntryType, 0),
+ VARLINK_DEFINE_FIELD(id, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(path, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(root, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(title, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(showTitle, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(sortKey, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(version, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(machineId, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(architecture, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(options, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(linux, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(efi, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(initrd, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(devicetree, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(devicetreeOverlay, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(isReported, VARLINK_BOOL, 0),
+ VARLINK_DEFINE_FIELD(triesLeft, VARLINK_INT, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(triesDone, VARLINK_INT, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(isDefault, VARLINK_BOOL, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(isSelected, VARLINK_BOOL, VARLINK_NULLABLE));
+
+static VARLINK_DEFINE_METHOD(
+ ListBootEntries,
+ VARLINK_DEFINE_OUTPUT_BY_TYPE(entry, BootEntry, VARLINK_NULLABLE));
+
+static VARLINK_DEFINE_METHOD(
+ SetRebootToFirmware,
+ VARLINK_DEFINE_INPUT(state, VARLINK_BOOL, 0));
+
+static VARLINK_DEFINE_METHOD(
+ GetRebootToFirmware,
+ VARLINK_DEFINE_OUTPUT(state, VARLINK_BOOL, 0));
+
+static VARLINK_DEFINE_ERROR(
+ RebootToFirmwareNotSupported);
+
+VARLINK_DEFINE_INTERFACE(
+ io_systemd_BootControl,
+ "io.systemd.BootControl",
+ &vl_type_BootEntryType,
+ &vl_type_BootEntry,
+ &vl_method_ListBootEntries,
+ &vl_method_SetRebootToFirmware,
+ &vl_method_GetRebootToFirmware,
+ &vl_error_RebootToFirmwareNotSupported);
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "varlink-idl.h"
+
+extern const VarlinkInterface vl_interface_io_systemd_BootControl;
void warn_unit_file_changed(const char *unit) {
assert(unit);
+ if (arg_no_warn)
+ return;
+
log_warning("Warning: The unit file, source configuration file or drop-ins of %s changed on disk. Run 'systemctl%s daemon-reload' to reload units.",
unit,
arg_runtime_scope == RUNTIME_SCOPE_SYSTEM ? "" : " --user");
#include "varlink.h"
#include "varlink-idl.h"
#include "varlink-io.systemd.h"
+#include "varlink-io.systemd.BootControl.h"
#include "varlink-io.systemd.Credentials.h"
#include "varlink-io.systemd.Journal.h"
#include "varlink-io.systemd.ManagedOOM.h"
print_separator();
test_parse_format_one(&vl_interface_io_systemd_Credentials);
print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_BootControl);
+ print_separator();
test_parse_format_one(&vl_interface_xyz_test);
}
install_btrfs
generate_module_dependencies
fi
+ inst_binary ssh
+ inst_binary sshd
+ inst_binary ssh-keygen
}
do_test "$@"
install_suse_systemd() {
local pkgs
+ dinfo "Install basic filesystem structure"
+ install_rpm filesystem
+
dinfo "Install SUSE systemd"
pkgs=(
systemd
+ systemd-boot
systemd-container
systemd-coredump
systemd-experimental
(! userdbctl "--$opt=foo" "--$opt=''" "--$opt=🐱")
done
+# FIXME: sshd seems to crash inside asan currently, skip the actual ssh test hence
+if command -v ssh &> /dev/null && command -v sshd &> /dev/null && ! [[ -v ASAN_OPTIONS ]]; then
+
+ at_exit() {
+ systemctl stop mysshserver.socket
+ rm -f /tmp/homed.id_rsa /run/systemd/system/mysshserver.socket /run/systemd/system/mysshserver@.service
+ systemctl daemon-reload
+ homectl remove homedsshtest ||:
+ mv /etc/pam.d/sshd.save46 mv /etc/pam.d/sshd
+ }
+
+ trap at_exit EXIT
+
+ # Test that SSH logins work with delayed unlocking
+ ssh-keygen -N '' -C '' -t rsa -f /tmp/homed.id_rsa
+ NEWPASSWORD=hunter4711 homectl create \
+ --disk-size=min \
+ --luks-discard=yes \
+ --luks-pbkdf-type=pbkdf2 \
+ --luks-pbkdf-time-cost=1ms \
+ --enforce-password-policy=no \
+ --ssh-authorized-keys=@/tmp/homed.id_rsa.pub \
+ --stop-delay=0 \
+ homedsshtest
+
+ mkdir -p /etc/ssh
+ test -f /etc/ssh/ssh_host_rsa_key || ssh-keygen -t rsa -C '' -N '' -f /etc/ssh/ssh_host_rsa_key
+
+ # ssh wants this dir around, but distros cannot agree on a common name for it, let's just create all that are aware of distros use
+ mkdir -p /usr/share/empty.sshd /var/empty /var/empty/sshd
+
+ mv /etc/pam.d/sshd /etc/pam.d/sshd.save46
+
+ cat > /etc/pam.d/sshd <<EOF
+auth sufficient pam_unix.so nullok
+auth sufficient pam_systemd_home.so
+auth required pam_deny.so
+account sufficient pam_systemd_home.so
+account sufficient pam_unix.so
+account required pam_permit.so
+session optional pam_systemd_home.so
+session optional pam_systemd.so
+session required pam_unix.so
+EOF
+
+ cat >> /etc/ssh/sshd_config <<EOF
+AuthorizedKeysCommand /usr/bin/userdbctl ssh-authorized-keys %u
+AuthorizedKeysCommandUser root
+UsePAM yes
+AcceptEnv PASSWORD
+LogLevel DEBUG3
+EOF
+
+ cat > /run/systemd/system/mysshserver.socket <<EOF
+[Socket]
+ListenStream=4711
+Accept=yes
+EOF
+
+ cat > /run/systemd/system/mysshserver@.service <<EOF
+[Service]
+ExecStart=-/usr/sbin/sshd -i -d -e
+StandardInput=socket
+StandardOutput=socket
+StandardError=journal
+EOF
+
+ systemctl daemon-reload
+ systemctl start mysshserver.socket
+
+ userdbctl user -j homedsshtest
+
+ ssh -t -t -4 -p 4711 -i /tmp/homed.id_rsa -o "SetEnv PASSWORD=hunter4711" -o "StrictHostKeyChecking no" homedsshtest@localhost echo zzz | tail -n 1 | tr -d '\r' > /tmp/homedsshtest.out
+ cat /tmp/homedsshtest.out
+ test "$(cat /tmp/homedsshtest.out)" = "zzz"
+ rm /tmp/homedsshtest.out
+
+ ssh -t -t -4 -p 4711 -i /tmp/homed.id_rsa -o "SetEnv PASSWORD=hunter4711" -o "StrictHostKeyChecking no" homedsshtest@localhost env
+
+ wait_for_state homedsshtest inactive
+ homectl remove homedsshtest
+fi
+
systemd-analyze log-level info
touch /testok
exit 0
fi
+if [[ ! -d /usr/lib/systemd/boot/efi ]]; then
+ echo "sd-boot is not installed, skipping."
+ exit 0
+fi
+
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
SYSTEMD_RELAX_ESP_CHECKS=yes SYSTEMD_RELAX_XBOOTLDR_CHECKS=yes basic_tests --root "${IMAGE_DIR}/root"
}
+testcase_bootctl_varlink() {
+ varlinkctl call --collect /run/systemd/io.systemd.BootControl io.systemd.BootControl.ListBootEntries '{}'
+
+ # We have no UEFI in the test environment, hence just check that this fails cleanly
+ ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.GetRebootToFirmware '{}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+ ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":true}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+ ( SYSTEMD_LOG_TARGET=console varlinkctl call --json=short /run/systemd/io.systemd.BootControl io.systemd.BootControl.SetRebootToFirmware '{"state":false}' 2>&1 || true ) | grep -q io.systemd.BootControl.RebootToFirmwareNotSupported
+}
+
run_testcases
'file' : 'systemd-boot-update.service',
'conditions' : ['ENABLE_BOOTLOADER'],
},
+ {
+ 'file' : 'systemd-bootctl@.service.in',
+ 'conditions' : ['ENABLE_BOOTLOADER'],
+ },
+ {
+ 'file' : 'systemd-bootctl.socket',
+ 'conditions' : ['ENABLE_BOOTLOADER'],
+ 'symlinks' : ['sockets.target.wants/'],
+ },
{
'file' : 'systemd-confext.service',
'conditions' : ['ENABLE_SYSEXT'],
--- /dev/null
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Boot Control (Varlink)
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+After=local-fs.target
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/io.systemd.BootControl
+FileDescriptorName=varlink
+SocketMode=0600
+Accept=yes
--- /dev/null
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Boot Control (Varlink)
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=local-fs.target
+Before=shutdown.target
+
+[Service]
+Environment=LISTEN_FDNAMES=varlink
+ExecStart={{BINDIR}}/bootctl