diff --git a/pressure-vessel/meson.build b/pressure-vessel/meson.build index 245e313c7941edfda395a04a39c0e40fdb6dc808..5bb38e9cfb0a91f4070727211b76daa907dc846c 100644 --- a/pressure-vessel/meson.build +++ b/pressure-vessel/meson.build @@ -171,6 +171,8 @@ pressure_vessel_wrap_lib = static_library( 'flatpak-run-x11.c', 'graphics-provider.c', 'graphics-provider.h', + 'passwd.c', + 'passwd.h', 'runtime.c', 'runtime.h', 'wrap-context.c', diff --git a/pressure-vessel/passwd.c b/pressure-vessel/passwd.c new file mode 100644 index 0000000000000000000000000000000000000000..f17e48e803b3847f3a00efc7bcc2d507cf69f454 --- /dev/null +++ b/pressure-vessel/passwd.c @@ -0,0 +1,263 @@ +/* + * Copyright © 2020-2024 Collabora Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "passwd.h" + +#include "steam-runtime-tools/log-internal.h" + +/* Append @field to @buffer, replacing colons or newlines with '_' + * to avoid possibly corrupting the passwd(5)/group(5) syntax. */ +static void +append_field (GString *buffer, + const char *field) +{ + gsize len = strlen (field); + gsize old_len = buffer->len; + gboolean warned = FALSE; + gsize i; + + g_string_append (buffer, field); + + for (i = 0; i < len; i++) + { + switch (buffer->str[old_len + i]) + { + case ':': + case '\n': + if (!warned) + { + _srt_log_warning ("Field \"%s\" cannot be represented in passwd(5)/group(5)", + field); + warned = TRUE; + } + + buffer->str[old_len + i] = '_'; + break; + + default: + break; + } + } + + g_assert (buffer->str[old_len + len] == '\0'); +} + +/* + * Assume that the first line in @buffer is a single user in passwd(5) + * syntax or a single group in group(5) syntax. + * + * Append all lines from @source/@path to @buffer, unless they would + * duplicate the user/group that is already there. + */ +static gboolean +append_remaining_lines (GString *buffer, + SrtSysroot *source, + const char *path, + GError **error) +{ + g_autofree gchar *exclude_same_name = NULL; + g_autofree gchar *content = NULL; + const char *line; + gsize line_num; + + if (buffer->len != 0) + { + const char *colon; + + colon = strchr (buffer->str, ':'); + g_assert (colon != NULL); + /* username + ':' as a prefix to exclude matching lines, e.g. "gfreeman:" */ + exclude_same_name = g_strndup (buffer->str, (colon - buffer->str) + 1); + } + + if (!_srt_sysroot_load (source, path, SRT_RESOLVE_FLAGS_READABLE, + NULL, &content, NULL, error)) + return FALSE; + + line = content; + line_num = 0; + + while (TRUE) + { + const char *end_of_line; + gsize len; + + end_of_line = strchr (line, '\n'); + + if (end_of_line == NULL) + len = strlen (line); + else + len = end_of_line - line; + + if (exclude_same_name != NULL && g_str_has_prefix (line, exclude_same_name)) + { + g_debug ("Skipping %s:%zu \"%s...\" because it is our user/group", + path, line_num, exclude_same_name); + } + else if (len > 0) + { + g_string_append_len (buffer, line, len); + g_string_append_c (buffer, '\n'); + } + + if (end_of_line == NULL) + break; + + line = end_of_line + 1; + line_num++; + } + + return TRUE; +} + +/* getpwuid(), but with mock output for unit-testing */ +static const struct passwd * +getpwuid_wrapper (uid_t uid, + PvMockPasswdLookup *mock) +{ + if (G_LIKELY (mock == NULL)) + return getpwuid (uid); + + g_assert_cmpint (mock->uid, ==, uid); + + if (mock->lookup_errno != 0) + errno = mock->lookup_errno; + + return mock->pwd; +} + +/* + * @source: A sysroot from which we can read /etc/passwd + * @mock: (allow-none): A mock version of the real getpwuid() result, + * used during unit-testing + * + * Return contents for a passwd(5) that has at least our own uid. + */ +gchar * +pv_generate_etc_passwd (SrtSysroot *source, + PvMockPasswdLookup *mock) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GString) buffer = NULL; + struct passwd fallback = + { + .pw_name = NULL, + .pw_passwd = (char *) "x", + .pw_uid = -1, + .pw_gid = -1, + .pw_gecos = NULL, + .pw_dir = NULL, + .pw_shell = (char *) "/bin/bash", + }; + const struct passwd *pw = NULL; + + fallback.pw_name = (char *) g_get_user_name (); + + if (fallback.pw_name == NULL) + fallback.pw_name = (char *) "user"; + + fallback.pw_gecos = (char *) g_get_real_name (); + + if (fallback.pw_gecos == NULL) + fallback.pw_gecos = fallback.pw_name; + + fallback.pw_uid = getuid (); + fallback.pw_gid = getgid (); + fallback.pw_dir = (char *) g_get_home_dir (); + + errno = 0; + pw = getpwuid_wrapper (fallback.pw_uid, mock); + + if (pw == NULL) + { + int saved_errno = errno; + + _srt_log_warning ("Unable to resolve uid %d: %s", + fallback.pw_uid, + saved_errno == 0 ? "user not found" : g_strerror (errno)); + pw = &fallback; + } + + g_assert (pw != NULL); + buffer = g_string_new (""); + append_field (buffer, pw->pw_name); + g_string_append_printf (buffer, ":x:%d:%d:", pw->pw_uid, pw->pw_gid); + append_field (buffer, pw->pw_gecos); + g_string_append_c (buffer, ':'); + append_field (buffer, pw->pw_dir); + /* We always behave as if the user's shell is bash, because we can rely + * on that existing in the container, whereas an alternative shell like + * /bin/zsh might not. */ + g_string_append (buffer, ":/bin/bash\n"); + + if (!append_remaining_lines (buffer, source, "/etc/passwd", &local_error)) + { + _srt_log_warning ("%s", local_error->message); + g_clear_error (&local_error); + } + + return g_string_free (g_steal_pointer (&buffer), FALSE); +} + +/* getgrgid(), but with mock output for unit-testing */ +static const struct group * +getgrgid_wrapper (gid_t gid, + PvMockPasswdLookup *mock) +{ + if (G_LIKELY (mock == NULL)) + return getgrgid (gid); + + g_assert_cmpint (mock->gid, ==, gid); + + if (mock->lookup_errno != 0) + errno = mock->lookup_errno; + + return mock->grp; +} + +/* + * @source: A sysroot from which we can read /etc/passwd + * @mock: (allow-none): A mock version of the real getgrgid() result, + * used during unit-testing + * + * Return contents for a group(5) that has at least our own primary gid. + */ +gchar * +pv_generate_etc_group (SrtSysroot *source, + PvMockPasswdLookup *mock) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GString) buffer = NULL; + gid_t primary_gid = getgid (); + const struct group *gr = NULL; + + errno = 0; + gr = getgrgid_wrapper (primary_gid, mock); + + if (gr == NULL) + { + int saved_errno = errno; + + _srt_log_warning ("Unable to resolve gid %d: %s", + primary_gid, + saved_errno == 0 ? "group not found" : g_strerror (errno)); + } + + buffer = g_string_new (""); + + if (gr != NULL) + { + append_field (buffer, gr->gr_name); + g_string_append_printf (buffer, ":x:%d:\n", gr->gr_gid); + } + + if (!append_remaining_lines (buffer, source, "/etc/group", &local_error)) + { + _srt_log_warning ("%s", local_error->message); + g_clear_error (&local_error); + } + + return g_string_free (g_steal_pointer (&buffer), FALSE); +} diff --git a/pressure-vessel/passwd.h b/pressure-vessel/passwd.h new file mode 100644 index 0000000000000000000000000000000000000000..d54b66e41f964e8f3bd9935f2806609b30fe5064 --- /dev/null +++ b/pressure-vessel/passwd.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2020-2024 Collabora Ltd. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <sys/types.h> +#include <grp.h> +#include <pwd.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/resolve-in-sysroot-internal.h" + +/* Mock data to be used during unit-testing */ +typedef struct +{ + uid_t uid; + gid_t gid; + const struct passwd *pwd; + const struct group *grp; + int lookup_errno; +} PvMockPasswdLookup; + +gchar *pv_generate_etc_passwd (SrtSysroot *source, + PvMockPasswdLookup *mock); +gchar *pv_generate_etc_group (SrtSysroot *source, + PvMockPasswdLookup *mock); diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c index ac9df5997f8719010d7451ee60f6ad59ebc73938..ef0f5881975b4fda71808d54a0c1d519d341fd58 100644 --- a/pressure-vessel/runtime.c +++ b/pressure-vessel/runtime.c @@ -46,6 +46,7 @@ #include "exports.h" #include "flatpak-run-private.h" #include "mtree.h" +#include "passwd.h" #include "supported-architectures.h" #include "tree-copy.h" #include "utils.h" @@ -3209,10 +3210,6 @@ bind_runtime_base (PvRuntime *self, }; static const char * const from_host[] = { - /* TODO: Synthesize a passwd with only the user and nobody, - * like Flatpak does? */ - "/etc/group", - "/etc/passwd", "/etc/host.conf", "/etc/hosts", "/etc/resolv.conf", @@ -3496,6 +3493,32 @@ bind_runtime_base (PvRuntime *self, g_return_val_if_reached (FALSE); } + { + g_autofree gchar *content = pv_generate_etc_passwd (self->real_root, NULL); + + g_assert (content != NULL); + + if (!pv_runtime_bind_into_container (self, bwrap, + "etc-passwd", content, -1, + "/etc/passwd", + PV_RUNTIME_EMULATION_ROOTS_BOTH, + error)) + return FALSE; + } + + { + g_autofree gchar *content = pv_generate_etc_group (self->real_root, NULL); + + g_assert (content != NULL); + + if (!pv_runtime_bind_into_container (self, bwrap, + "etc-group", content, -1, + "/etc/group", + PV_RUNTIME_EMULATION_ROOTS_BOTH, + error)) + return FALSE; + } + if (self->provider != NULL) { for (i = 0; from_provider[i] != NULL; i++)