From 23a526546db971a1dcd73fa29bbe2499aa6bb2f4 Mon Sep 17 00:00:00 2001 From: Simon McVittie <smcv@collabora.com> Date: Tue, 12 Nov 2024 21:30:20 +0000 Subject: [PATCH] pressure-vessel: Generate /etc/passwd, /etc/group for the container Sometimes our current uid will not be present in /etc/passwd. For example, when using systemd-homed the equivalent of a passwd(5) record is synthesized by libnss_systemd. A similar thing could happen when using a remote user directory like LDAP, or for the group database. Inside the container, we have the basic nsswitch plugins from glibc (notably libnss_files, which reads /etc/passwd and /etc/group), but we do not have access to non-standard nsswitch plugins like libnss_systemd or LDAP. This can cause crashes or bugs in games that assume that `getpwuid(getuid())` will always succeed (like Factorio), especially if they do not respect `$HOME` (like Factorio). Instead of bind-mounting the host system's /etc/passwd and /etc/group as-is, we can synthesize a /etc/passwd and /etc/group that are guaranteed to contain at least our uid and primary gid, matching what Flatpak does. Flatpak generates very minimal files that list our uid and gid, plus the default overflow uid and gid 65534 (labelled as `nfsnobody` for historical reasons). However, pressure-vessel has historically provided more complete files, so it's conceivable that a game might be relying on being able to resolve (for example) root as uid 0; so we copy everything from the host /etc/passwd and /etc/group, excluding only the line(s) that would duplicate the one we synthesize. Resolves: https://github.com/ValveSoftware/steam-runtime/issues/705 Signed-off-by: Simon McVittie <smcv@collabora.com> --- pressure-vessel/meson.build | 2 + pressure-vessel/passwd.c | 263 ++++++++++++++++++++++++++++++++++++ pressure-vessel/passwd.h | 28 ++++ pressure-vessel/runtime.c | 31 ++++- 4 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 pressure-vessel/passwd.c create mode 100644 pressure-vessel/passwd.h diff --git a/pressure-vessel/meson.build b/pressure-vessel/meson.build index 245e313c7..5bb38e9cf 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 000000000..f17e48e80 --- /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 000000000..d54b66e41 --- /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 ac9df5997..ef0f58819 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++) -- GitLab