File NetworkManager-CVE-2025-9615.patch of Package NetworkManager
From bfcd0d10466f673c1a73c5e77b73c6070677cec4 Mon Sep 17 00:00:00 2001
From: Beniamino Galvani <bgalvani@redhat.com>
Date: Fri, 10 Oct 2025 11:44:55 +0200
Subject: [PATCH] libnm-core, core: add permission helpers
Add utility functions to get the number of users and the first user
from the connection.permissions property of a connection.
(cherry picked from commit 59543620dcf7bb3e4b1316536f0330ab4a752e3e)
(cherry picked from commit 2fc662cc712e9e1a1992cbbc187f257f57476e53)
(cherry picked from commit abdf3385d6881d98ffa4311e4d34a74c1b558020)
helpers: move helper programs to the same directory
Create a new 'nm-helpers' directory for all the helper programs, to
avoid having too many subdirs in the src directory.
(cherry picked from commit 3d76d12eee88b667d1a385b861c54fcdd4e490ed)
(cherry picked from commit afa6fc951b4a19b55d76fb365446a5cf8896a1d3)
(cherry picked from commit d1776c539465c81da6143c1470cedb72312e1561)
daemon-helper: add read-file-as-user
Add a new command to read the content of a file after switching to the
given user. This command can be used to enforce Unix filesystem
permissions when accessing a file on behalf of a user.
(cherry picked from commit 285457a5f8284f21387753d7f245e3f51ce29248)
(cherry picked from commit 022b992846712ecff6454524eb3934ff0800cf54)
(cherry picked from commit 310887be7123ace3c0517c15e5552761f0201d5b)
supplicant: remove blobs before adding new ones
When connecting, we add the blobs to the Interface object of the
supplicant. Those blobs are not removed on disconnect and so when we
try to add blobs with the same id, the supplicant returns an error.
Make sure we start from a clean slate on each connection attempt, by
deleting all existing blobs. Probably we should also delete the added
blobs on disconnect, but that's left for a future improvement.
(cherry picked from commit 0093bbd9507df3b16eaa08cd3a6b799b678c7599)
(cherry picked from commit ce3ebf6d3e5c511f872872bf51bee7f08db6f045)
(cherry picked from commit 4f3597448dd774729b74810de262f7fcffb6d2f9)
core: support returning binary output from the daemon helper
The full output of the daemon helper is added to a NMStrBuf, without
interpreting it as a string (that is, without stopping at the first
NUL character).
However, when we retrieve the content from the NMStrBuf we assume it's
a string. This is fine for certain commands that expect a string
output, but it's not for other commands as the read-file-as-user one.
Add a new argument to nm_utils_spawn_helper() to specify whether the
output is binary or not. Also have different finish functions
depending on the return type.
(cherry picked from commit 1d90d50fc6e8c167581c6831c2511bc4148f234b)
(cherry picked from commit 59df5fc93fc30c9b8c9ceca3c42b173f831f53f7)
(cherry picked from commit 7acf70dfb912ca65e1daaf669ad5f4ed1faadbcb)
supplicant: rename variables
Rename uid to to blob_id, and con_uid to con_uuid.
(cherry picked from commit 586f7700b8ad6b4b4cffdb4cdb2bed2e4726ef5c)
(cherry picked from commit a17f51fe156ed63882d5dc49e594e23913f883fd)
(cherry picked from commit b7926872e154fb508691de8aa8885de86b96456f)
core: add functions to read private files of connections
Add function nm_utils_read_private_files(). It can be used to read a
list of paths as the given user. It spawns the daemon-helper to read
each path and returns asynchronously a hash table containing the files
content.
Also add nm_utils_get_connection_private_files_paths() to return a
list of file paths referenced in a connection. The function currently
returns only 802.1x file paths for certificates and keys.
(cherry picked from commit de4eb64253d493364d676b509f63f2e8d1810061)
(cherry picked from commit 9432822f3460975520aeba4b3367108d5322b28a)
(cherry picked from commit 399d7be7712b268c630f9a62b27b1b7cf904c307)
device: read private files in stage2
During stage2 (prepare) of an activation, check if the connection is
private and if it contains any certificate/key path. If so, start
reading the files and delay stage2. Once done, store the files'
content into priv->private_files.table and continue the activation.
(cherry picked from commit 98e6dbdf21e5b165bae498ab2a29bb14f331ccd1)
(cherry picked from commit a417df34847ae7cd1eb0d77af8b70beb6619cfbe)
(cherry picked from commit b8f8731636ff1de6a2e44b17e95ae27320e9dc4e)
core: pass certificates as blobs to supplicant for private connections
In case of private connections, the device has already read the
certificates and keys content from disk, validating that the owner of
the connection has access to them. Pass those files as blobs to the
supplicant so that it doesn't have to read them again from the
filesystem, creating the opportunity for TOCTOU bugs.
(cherry picked from commit 36ea70c0993cb48d3155c2de6d6c8e48a2b08c60)
(cherry picked from commit aac5b80fcad34489e737b6eb1c5389bd32169d23)
(cherry picked from commit f08ee617b99a25c7c3d4d820fbe2539f9b78e986)
core,libnm-core: introduce property flag for certificate and keys
If we add a new property in the future and it references a certificate
or key stored on disk, we need to also implement the logic to verify
the access to the file for private connections.
Add a new property flag NM_SETTING_PARAM_CERT_KEY_FILE to existing
certificate and key properties, so that it's easier to see that they
need special treatment. Also add some assertions to verify that the
properties with the flag are handled properly.
While at it, move the enumeration of private-files to the settings.
(cherry picked from commit acbfae5e051af8647e32d14ccc6be05419dcca77)
(cherry picked from commit e3c27f2a22b75c98c300c5ba6249193b9047eaaf)
(cherry picked from commit 9bc4d626809ed534a49dd5e436c4079eccf60b0a)
---
Makefile.am | 12 +-
src/core/devices/nm-device-ethernet.c | 11 +-
src/core/devices/nm-device-macsec.c | 11 +-
src/core/devices/nm-device-private.h | 2 +
src/core/devices/nm-device-utils.c | 3 +-
src/core/devices/nm-device.c | 125 ++++
src/core/devices/wifi/nm-device-wifi.c | 4 +-
src/core/nm-core-utils.c | 217 +++++-
src/core/nm-core-utils.h | 19 +-
src/core/supplicant/nm-supplicant-config.c | 170 +++--
src/core/supplicant/nm-supplicant-config.h | 4 +-
src/core/supplicant/nm-supplicant-interface.c | 201 ++++-
.../supplicant/tests/test-supplicant-config.c | 4 +-
src/libnm-core-impl/nm-setting-8021x.c | 97 ++-
src/libnm-core-impl/nm-setting-connection.c | 41 ++
src/libnm-core-impl/nm-setting-private.h | 10 +
src/libnm-core-impl/nm-setting.c | 29 +
src/libnm-core-intern/nm-core-internal.h | 7 +
src/libnm-std-aux/nm-std-utils.c | 114 ++-
src/libnm-std-aux/nm-std-utils.h | 4 +
src/meson.build | 2 +-
src/nm-daemon-helper/meson.build | 15 -
src/nm-helpers/README.md | 43 ++
src/nm-helpers/meson.build | 56 ++
.../nm-daemon-helper.c | 33 +
src/nm-helpers/nm-priv-helper.c | 695 ++++++++++++++++++
src/nm-helpers/nm-priv-helper.conf | 13 +
.../org.freedesktop.nm_priv_helper.service.in | 5 +
28 files changed, 1817 insertions(+), 130 deletions(-)
delete mode 100644 src/nm-daemon-helper/meson.build
create mode 100644 src/nm-helpers/README.md
create mode 100644 src/nm-helpers/meson.build
rename src/{nm-daemon-helper => nm-helpers}/nm-daemon-helper.c (73%)
create mode 100644 src/nm-helpers/nm-priv-helper.c
create mode 100644 src/nm-helpers/nm-priv-helper.conf
create mode 100644 src/nm-helpers/org.freedesktop.nm_priv_helper.service.in
diff --git a/Makefile.am b/Makefile.am
index 7cc50332d5..7005d0f245 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4612,28 +4612,28 @@ EXTRA_DIST += \
$(NULL)
###############################################################################
-# src/nm-daemon-helper
+# src/nm-helpers
###############################################################################
-libexec_PROGRAMS += src/nm-daemon-helper/nm-daemon-helper
+libexec_PROGRAMS += src/nm-helpers/nm-daemon-helper
-src_nm_daemon_helper_nm_daemon_helper_CPPFLAGS = \
+src_nm_helpers_nm_daemon_helper_CPPFLAGS = \
$(dflt_cppflags) \
-I$(srcdir)/src \
-I$(builddir)/src \
$(NULL)
-src_nm_daemon_helper_nm_daemon_helper_LDFLAGS = \
+src_nm_helpers_nm_daemon_helper_LDFLAGS = \
-Wl,--version-script="$(srcdir)/linker-script-binary.ver" \
$(SANITIZER_EXEC_LDFLAGS) \
$(NULL)
-src_nm_daemon_helper_nm_daemon_helper_LDADD = \
+src_nm_helpers_nm_daemon_helper_LDADD = \
src/libnm-std-aux/libnm-std-aux.la \
$(NULL)
EXTRA_DIST += \
- src/nm-daemon-helper/meson.build \
+ src/nm-helpers/meson.build \
$(NULL)
###############################################################################
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 95336c7a52..ac537a7326 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -624,10 +624,17 @@ build_supplicant_config(NMDeviceEthernet *self, GError **error)
mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
nm_device_get_ifindex(NM_DEVICE(self)));
- config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+ config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+ nm_utils_get_connection_first_permissions_user(connection));
security = nm_connection_get_setting_802_1x(connection);
- if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) {
+ if (!nm_supplicant_config_add_setting_8021x(config,
+ security,
+ con_uuid,
+ mtu,
+ TRUE,
+ nm_device_get_private_files(NM_DEVICE(self)),
+ error)) {
g_prefix_error(error, "802-1x-setting: ");
g_clear_object(&config);
}
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 51b7225bbf..dd6635af3f 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -234,7 +234,8 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
mtu = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
nm_device_get_ifindex(NM_DEVICE(self)));
- config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+ config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+ nm_utils_get_connection_first_permissions_user(connection));
s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC);
@@ -247,7 +248,13 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) {
s_8021x = nm_connection_get_setting_802_1x(connection);
- if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) {
+ if (!nm_supplicant_config_add_setting_8021x(config,
+ s_8021x,
+ con_uuid,
+ mtu,
+ TRUE,
+ nm_device_get_private_files(NM_DEVICE(self)),
+ error)) {
g_prefix_error(error, "802-1x-setting: ");
return NULL;
}
diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
index eb37b14ffb..45be521ec7 100644
--- a/src/core/devices/nm-device-private.h
+++ b/src/core/devices/nm-device-private.h
@@ -200,4 +200,6 @@ void nm_device_auth_request(NMDevice * self,
NMManagerDeviceAuthRequestFunc callback,
gpointer user_data);
+GHashTable *nm_device_get_private_files(NMDevice *self);
+
#endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index f40ca570f6..c972b02beb 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -223,7 +223,7 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data
gs_free_error GError *error = NULL;
gs_free char * output = NULL;
- output = nm_utils_spawn_helper_finish(result, &error);
+ output = nm_utils_spawn_helper_finish_string(result, &error);
if (nm_utils_error_is_cancelled(error))
return;
@@ -240,6 +240,7 @@ resolve_addr_spawn_helper(ResolveAddrInfo *info)
nm_utils_inet_ntop(info->addr_family, &info->address, addr_str);
_LOG2D(info, "start lookup via nm-daemon-helper");
nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str),
+ FALSE,
g_task_get_cancellable(info->task),
resolve_addr_helper_cb,
info);
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 50386d0018..9d3b200807 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -229,6 +229,12 @@ typedef struct {
int addr_family;
} HostnameResolver;
+typedef enum {
+ PRIVATE_FILES_STATE_UNKNOWN = 0,
+ PRIVATE_FILES_STATE_READING,
+ PRIVATE_FILES_STATE_DONE,
+} PrivateFilesState;
+
/*****************************************************************************/
enum {
@@ -702,6 +708,13 @@ typedef struct _NMDevicePrivate {
guint64 rx_bytes;
} stats;
+ struct {
+ GHashTable *table;
+ GCancellable *cancellable;
+ char *user;
+ PrivateFilesState state;
+ } private_files;
+
bool mtu_force_set_done : 1;
NMOptionBool promisc_reset;
@@ -8487,6 +8500,49 @@ tc_commit(NMDevice *self)
return TRUE;
}
+static void
+read_private_files_cb(GObject *source_object, GAsyncResult *result, gpointer data)
+{
+ gs_unref_hashtable GHashTable *table = NULL;
+ gs_free_error GError *error = NULL;
+ NMDevice *self;
+ NMDevicePrivate *priv;
+
+ table = nm_utils_read_private_files_finish(result, &error);
+ if (nm_utils_error_is_cancelled(error))
+ return;
+
+ self = NM_DEVICE(data);
+ priv = NM_DEVICE_GET_PRIVATE(self);
+
+ if (error) {
+ NMConnection *connection = nm_device_get_applied_connection(self);
+
+ _LOGW(LOGD_DEVICE,
+ "could not read files for private connection %s owned by user '%s': %s",
+ connection ? nm_connection_get_uuid(connection) : NULL,
+ priv->private_files.user,
+ error->message);
+ nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+ return;
+ }
+
+ _LOGD(LOGD_DEVICE, "private files successfully read");
+
+ priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+ priv->private_files.table = g_steal_pointer(&table);
+ g_clear_pointer(&priv->private_files.user, g_free);
+ g_clear_object(&priv->private_files.cancellable);
+
+ nm_device_activate_schedule_stage2_device_config(self, FALSE);
+}
+
+GHashTable *
+nm_device_get_private_files(NMDevice *self)
+{
+ return NM_DEVICE_GET_PRIVATE(self)->private_files.table;
+}
+
/*
* activate_stage2_device_config
*
@@ -8499,6 +8555,7 @@ activate_stage2_device_config(NMDevice *self)
{
NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
NMDeviceClass * klass;
+ NMConnection *applied;
NMActStageReturn ret;
NMSettingWired * s_wired;
gboolean no_firmware = FALSE;
@@ -8507,6 +8564,68 @@ activate_stage2_device_config(NMDevice *self)
nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+ applied = nm_device_get_applied_connection(self);
+
+ /* If the connection is private (owned by a specific user), we need to
+ * verify that the user has permission to access any files specified in
+ * the connection, such as certificates and keys. We do that by calling
+ * nm_utils_read_private_files() and saving the file contents in a hash
+ * table that can be accessed later during the activation. It is important
+ * to never access the files again to avoid TOCTOU bugs.
+ */
+ switch (priv->private_files.state) {
+ case PRIVATE_FILES_STATE_UNKNOWN:
+ {
+ gs_free const char **paths = NULL;
+ NMSettingConnection *s_con;
+ const char *user;
+
+ s_con = nm_connection_get_setting_connection(applied);
+ nm_assert(s_con);
+ user = _nm_setting_connection_get_first_permissions_user(s_con);
+
+ priv->private_files.user = g_strdup(user);
+ if (!priv->private_files.user) {
+ priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+ break;
+ }
+
+ paths = nm_utils_get_connection_private_files_paths(applied);
+ if (!paths) {
+ priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+ break;
+ }
+
+ if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) {
+ _LOGW(LOGD_DEVICE,
+ "private connections with multiple users are not allowed to reference "
+ "certificates and keys on the filesystem. Specify only one user in the "
+ "connection.permissions property.");
+ nm_device_state_changed(self,
+ NM_DEVICE_STATE_FAILED,
+ NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+ return;
+ }
+
+ priv->private_files.state = PRIVATE_FILES_STATE_READING;
+ priv->private_files.cancellable = g_cancellable_new();
+
+ _LOGD(LOGD_DEVICE, "reading private files");
+ nm_utils_read_private_files(paths,
+ priv->private_files.user,
+ priv->private_files.cancellable,
+ read_private_files_cb,
+ self);
+ return;
+ }
+ case PRIVATE_FILES_STATE_READING:
+ /* wait */
+ return;
+ case PRIVATE_FILES_STATE_DONE:
+ /* proceed */
+ break;
+ }
+
if (!nm_device_sys_iface_state_is_external_or_assume(self))
_ethtool_state_set(self);
@@ -16032,6 +16151,12 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
if (NM_DEVICE_GET_CLASS(self)->deactivate)
NM_DEVICE_GET_CLASS(self)->deactivate(self);
+ /* Clean up private files */
+ nm_clear_g_cancellable(&priv->private_files.cancellable);
+ g_clear_pointer(&priv->private_files.table, g_hash_table_unref);
+ g_clear_pointer(&priv->private_files.user, g_free);
+ priv->private_files.state = PRIVATE_FILES_STATE_UNKNOWN;
+
ifindex = nm_device_get_ip_ifindex(self);
if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index fca2fde515..eb0a68f64a 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -2863,7 +2863,8 @@ build_supplicant_config(NMDeviceWifi *self,
s_wireless = nm_connection_get_setting_wireless(connection);
g_return_val_if_fail(s_wireless != NULL, NULL);
- config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface));
+ config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface),
+ nm_utils_get_connection_first_permissions_user(connection));
/* Warn if AP mode may not be supported */
if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP)
@@ -2934,6 +2935,7 @@ build_supplicant_config(NMDeviceWifi *self,
mtu,
pmf,
fils,
+ nm_device_get_private_files(NM_DEVICE(self)),
error)) {
g_prefix_error(error, "802-11-wireless-security: ");
goto error;
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 8fdc737960..4ceed21876 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -4772,6 +4772,7 @@ typedef struct {
int child_stdin;
int child_stdout;
+ gboolean binary_output;
GSource *input_source;
GSource *output_source;
@@ -4845,7 +4846,17 @@ helper_complete(HelperInfo *info, GError *error)
}
nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
- g_task_return_pointer(info->task, nm_str_buf_finalize(&info->in_buffer, NULL), g_free);
+
+ if (info->binary_output) {
+ g_task_return_pointer(
+ info->task,
+ g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len),
+ (GDestroyNotify) (g_bytes_unref));
+ } else {
+ g_task_return_pointer(info->task,
+ nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1),
+ g_free);
+ }
helper_info_free(info);
}
@@ -4973,6 +4984,7 @@ helper_cancelled(GObject *object, gpointer user_data)
void
nm_utils_spawn_helper(const char *const * args,
+ gboolean binary_output,
GCancellable * cancellable,
GAsyncReadyCallback callback,
gpointer cb_data)
@@ -4991,8 +5003,13 @@ nm_utils_spawn_helper(const char *const * args,
.child_stdin = -1,
.child_stdout = -1,
.pid = -1,
+ .binary_output = binary_output,
};
+ /* Store if the caller requested binary output so that we can check later
+ * that the right result function is called. */
+ g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL);
+
if (!g_spawn_async_with_pipes("/",
(char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
(char **) NM_MAKE_STRV(),
@@ -5072,11 +5089,207 @@ nm_utils_spawn_helper(const char *const * args,
}
char *
-nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error)
+nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error)
+{
+ GTask *task = G_TASK(result);
+
+ nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+ /* Check binary_output */
+ nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE);
+
+ return g_task_propagate_pointer(task, error);
+}
+
+GBytes *
+nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error)
{
GTask *task = G_TASK(result);
nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+ /* Check binary_output */
+ nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE);
+
+ return g_task_propagate_pointer(task, error);
+}
+
+const char *
+nm_utils_get_connection_first_permissions_user(NMConnection *connection)
+{
+ NMSettingConnection *s_con;
+
+ s_con = nm_connection_get_setting_connection(connection);
+ nm_assert(s_con);
+
+ return _nm_setting_connection_get_first_permissions_user(s_con);
+}
+
+/*****************************************************************************/
+
+const char **
+nm_utils_get_connection_private_files_paths(NMConnection *connection)
+{
+ GPtrArray *files;
+ gs_free NMSetting **settings = NULL;
+ guint num_settings;
+ guint i;
+
+ files = g_ptr_array_new();
+ settings = nm_connection_get_settings(connection, &num_settings);
+ for (i = 0; i < num_settings; i++) {
+ _nm_setting_get_private_files(settings[i], files);
+ }
+ g_ptr_array_add(files, NULL);
+
+ return (const char **) g_ptr_array_free(files, files->len == 1);
+}
+
+typedef struct _ReadInfo ReadInfo;
+
+typedef struct {
+ char *path;
+ ReadInfo *read_info;
+} FileInfo;
+
+struct _ReadInfo {
+ GTask *task;
+ GHashTable *table;
+ GPtrArray *file_infos; /* of FileInfo */
+ GError *first_error;
+ guint num_pending;
+};
+
+static void
+read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+ FileInfo *file_info = user_data;
+ ReadInfo *read_info = file_info->read_info;
+ gs_unref_bytes GBytes *output = NULL;
+ gs_free_error GError *error = NULL;
+
+ output = nm_utils_spawn_helper_finish_binary(result, &error);
+
+ nm_assert(read_info->num_pending > 0);
+ read_info->num_pending--;
+
+ if (nm_utils_error_is_cancelled(error)) {
+ /* nop */
+ } else if (error) {
+ nm_log_dbg(LOGD_CORE,
+ "read-private-files: failed to read file '%s': %s",
+ file_info->path,
+ error->message);
+ if (!read_info->first_error) {
+ /* @error just says "helper process exited with status X".
+ * Return a more human-friendly one. */
+ read_info->first_error = g_error_new(NM_UTILS_ERROR,
+ NM_UTILS_ERROR_UNKNOWN,
+ "error reading file '%s'",
+ file_info->path);
+ }
+ } else {
+ nm_log_dbg(LOGD_SUPPLICANT,
+ "read-private-files: successfully read file '%s'",
+ file_info->path);
+
+ /* Store the file contents in the hash table */
+ if (!read_info->table) {
+ read_info->table = g_hash_table_new_full(nm_str_hash,
+ g_str_equal,
+ g_free,
+ (GDestroyNotify) g_bytes_unref);
+ }
+ g_hash_table_insert(read_info->table,
+ g_steal_pointer(&file_info->path),
+ g_steal_pointer(&output));
+ }
+
+ g_clear_pointer(&file_info->path, g_free);
+
+ /* If all operations are completed, return */
+ if (read_info->num_pending == 0) {
+ if (read_info->first_error) {
+ g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error));
+ } else {
+ g_task_return_pointer(read_info->task,
+ g_steal_pointer(&read_info->table),
+ (GDestroyNotify) g_hash_table_unref);
+ }
+
+ if (read_info->table)
+ g_hash_table_unref(read_info->table);
+ if (read_info->file_infos)
+ g_ptr_array_unref(read_info->file_infos);
+
+ g_object_unref(read_info->task);
+ g_free(read_info);
+ }
+}
+
+/**
+ * nm_utils_read_private_files:
+ * @paths: array of file paths to be read
+ * @user: name of the user to impersonate when reading the files
+ * @cancellable: cancellable to cancel the operation
+ * @callback: callback to invoke on completion
+ * @cb_data: data for @callback
+ *
+ * Reads the given list of files @paths on behalf of user @user. Invokes
+ * @callback asynchronously on completion. The callback must use
+ * nm_utils_read_private_files_finish() to obtain the result.
+ */
+void
+nm_utils_read_private_files(const char *const *paths,
+ const char *user,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer cb_data)
+{
+ ReadInfo *read_info;
+ FileInfo *file_info;
+ guint i;
+
+ g_return_if_fail(paths && paths[0]);
+ g_return_if_fail(cancellable);
+ g_return_if_fail(callback);
+ g_return_if_fail(cb_data);
+
+ read_info = g_new(ReadInfo, 1);
+ *read_info = (ReadInfo) {
+ .task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data),
+ .file_infos = g_ptr_array_new_with_free_func(g_free),
+ };
+
+ for (i = 0; paths[i]; i++) {
+ file_info = g_new(FileInfo, 1);
+ *file_info = (FileInfo) {
+ .path = g_strdup(paths[i]),
+ .read_info = read_info,
+ };
+ g_ptr_array_add(read_info->file_infos, file_info);
+ read_info->num_pending++;
+
+ nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]),
+ TRUE,
+ cancellable,
+ read_file_helper_cb,
+ file_info);
+ }
+}
+
+/**
+ * nm_utils_read_private_files_finish:
+ * @result: the GAsyncResult
+ * @error: on return, the error
+ *
+ * Returns the files read by nm_utils_read_private_files(). The return value
+ * is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref().
+ */
+GHashTable *
+nm_utils_read_private_files_finish(GAsyncResult *result, GError **error)
+{
+ GTask *task = G_TASK(result);
+
+ nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files));
return g_task_propagate_pointer(task, error);
}
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index 76c340d148..ac2ddc9177 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -427,10 +427,27 @@ guint8 nm_wifi_utils_level_to_quality(int val);
/*****************************************************************************/
void nm_utils_spawn_helper(const char *const * args,
+ gboolean binary_output,
GCancellable * cancellable,
GAsyncReadyCallback callback,
gpointer cb_data);
-char *nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error);
+char *nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error);
+GBytes *nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error);
+
+/*****************************************************************************/
+
+const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection);
+
+/*****************************************************************************/
+
+const char **nm_utils_get_connection_private_files_paths(NMConnection *connection);
+
+void nm_utils_read_private_files(const char *const *paths,
+ const char *user,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer cb_data);
+GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error);
#endif /* __NM_CORE_UTILS_H__ */
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 1959a1619a..4304b29343 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -30,6 +30,7 @@ typedef struct {
typedef struct {
GHashTable * config;
GHashTable * blobs;
+ char *private_user;
NMSupplCapMask capabilities;
guint32 ap_scan;
bool fast_required : 1;
@@ -60,7 +61,7 @@ _get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type)
}
NMSupplicantConfig *
-nm_supplicant_config_new(NMSupplCapMask capabilities)
+nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user)
{
NMSupplicantConfigPrivate *priv;
NMSupplicantConfig * self;
@@ -69,6 +70,7 @@ nm_supplicant_config_new(NMSupplCapMask capabilities)
priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
priv->capabilities = capabilities;
+ priv->private_user = g_strdup(private_user);
return self;
}
@@ -258,19 +260,19 @@ static gboolean
nm_supplicant_config_add_blob_for_connection(NMSupplicantConfig *self,
GBytes * field,
const char * name,
- const char * con_uid,
+ const char * con_uuid,
GError ** error)
{
if (field && g_bytes_get_size(field)) {
- gs_free char *uid = NULL;
+ gs_free char *blob_id = NULL;
char * p;
- uid = g_strdup_printf("%s-%s", con_uid, name);
- for (p = uid; *p; p++) {
+ blob_id = g_strdup_printf("%s-%s", con_uuid, name);
+ for (p = blob_id; *p; p++) {
if (*p == '/')
*p = '-';
}
- if (!nm_supplicant_config_add_blob(self, name, field, uid, error))
+ if (!nm_supplicant_config_add_blob(self, name, field, blob_id, error))
return FALSE;
}
return TRUE;
@@ -283,6 +285,7 @@ nm_supplicant_config_finalize(GObject *object)
g_hash_table_destroy(priv->config);
nm_clear_pointer(&priv->blobs, g_hash_table_destroy);
+ nm_clear_pointer(&priv->private_user, g_free);
G_OBJECT_CLASS(nm_supplicant_config_parent_class)->finalize(object);
}
@@ -808,6 +811,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig *
guint32 mtu,
NMSettingWirelessSecurityPmf pmf,
NMSettingWirelessSecurityFils fils,
+ GHashTable *files,
GError ** error)
{
NMSupplicantConfigPrivate *priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
@@ -1118,6 +1122,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig *
con_uuid,
mtu,
FALSE,
+ files,
error))
return FALSE;
}
@@ -1199,6 +1204,7 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
const char * con_uuid,
guint32 mtu,
gboolean wired,
+ GHashTable *files,
GError ** error)
{
NMSupplicantConfigPrivate *priv;
@@ -1416,24 +1422,21 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
}
/* CA certificate */
+ path = NULL;
+ bytes = NULL;
if (ca_cert_override) {
- if (!add_string_val(self, ca_cert_override, "ca_cert", FALSE, NULL, error))
- return FALSE;
+ /* This is a build-time-configured system-wide file path, no need to pass
+ * it as a blob */
+ path = ca_cert_override;
} else {
switch (nm_setting_802_1x_get_ca_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_ca_cert_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "ca_cert",
- con_uuid,
- error))
- return FALSE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_ca_cert_path(setting);
- if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(self,
@@ -1449,26 +1452,32 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
break;
}
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert", con_uuid, error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths other than the system CA store */
+ g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+ if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
+ return FALSE;
+ }
/* Phase 2 CA certificate */
+ path = NULL;
+ bytes = NULL;
if (ca_cert_override) {
- if (!add_string_val(self, ca_cert_override, "ca_cert2", FALSE, NULL, error))
- return FALSE;
+ /* This is a build-time-configured system-wide file path, no need to pass
+ * it as a blob */
+ path = ca_cert_override;
} else {
switch (nm_setting_802_1x_get_phase2_ca_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_ca_cert_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "ca_cert2",
- con_uuid,
- error))
- return FALSE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_ca_cert_path(setting);
- if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
@@ -1485,6 +1494,15 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
break;
}
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert2", con_uuid, error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths other than the system CA store */
+ g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+ if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
+ return FALSE;
+ }
/* Subject match */
value = nm_setting_802_1x_get_subject_match(setting);
@@ -1536,21 +1554,17 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
/* Private key */
added = FALSE;
+ path = NULL;
+ bytes = NULL;
switch (nm_setting_802_1x_get_private_key_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_private_key_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "private_key",
- con_uuid,
- error))
- return FALSE;
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_private_key_path(setting);
- if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1567,6 +1581,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
default:
break;
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self,
+ bytes,
+ "private_key",
+ con_uuid,
+ error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths */
+ g_return_val_if_fail(!priv->private_user, FALSE);
+ if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
+ return FALSE;
+ }
if (added) {
NMSetting8021xCKFormat format;
@@ -1590,20 +1617,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
/* Only add the client cert if the private key is not PKCS#12, as
* wpa_supplicant configuration directs us to do.
*/
+ path = NULL;
+ bytes = NULL;
switch (nm_setting_802_1x_get_client_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_client_cert_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "client_cert",
- con_uuid,
- error))
- return FALSE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_client_cert_path(setting);
- if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
@@ -1619,26 +1642,35 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
default:
break;
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self,
+ bytes,
+ "client_cert",
+ con_uuid,
+ error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths */
+ g_return_val_if_fail(!priv->private_user, FALSE);
+ if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
+ return FALSE;
+ }
}
}
/* Phase 2 private key */
added = FALSE;
+ path = NULL;
+ bytes = NULL;
switch (nm_setting_802_1x_get_phase2_private_key_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_private_key_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "private_key2",
- con_uuid,
- error))
- return FALSE;
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_private_key_path(setting);
- if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
added = TRUE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1656,6 +1688,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
default:
break;
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self,
+ bytes,
+ "private_key2",
+ con_uuid,
+ error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths */
+ g_return_val_if_fail(!priv->private_user, FALSE);
+ if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
+ return FALSE;
+ }
if (added) {
NMSetting8021xCKFormat format;
@@ -1679,20 +1724,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
/* Only add the client cert if the private key is not PKCS#12, as
* wpa_supplicant configuration directs us to do.
*/
+ path = NULL;
+ bytes = NULL;
switch (nm_setting_802_1x_get_phase2_client_cert_scheme(setting)) {
case NM_SETTING_802_1X_CK_SCHEME_BLOB:
bytes = nm_setting_802_1x_get_phase2_client_cert_blob(setting);
- if (!nm_supplicant_config_add_blob_for_connection(self,
- bytes,
- "client_cert2",
- con_uuid,
- error))
- return FALSE;
break;
case NM_SETTING_802_1X_CK_SCHEME_PATH:
path = nm_setting_802_1x_get_phase2_client_cert_path(setting);
- if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
- return FALSE;
+ if (priv->private_user)
+ bytes = nm_g_hash_table_lookup(files, path);
break;
case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
if (!add_pkcs11_uri_with_pin(
@@ -1708,6 +1749,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
default:
break;
}
+ if (bytes) {
+ if (!nm_supplicant_config_add_blob_for_connection(self,
+ bytes,
+ "client_cert2",
+ con_uuid,
+ error))
+ return FALSE;
+ } else if (path) {
+ /* Private connections cannot use paths */
+ g_return_val_if_fail(!priv->private_user, FALSE);
+ if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
+ return FALSE;
+ }
}
}
diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h
index b561936239..dcc89d0726 100644
--- a/src/core/supplicant/nm-supplicant-config.h
+++ b/src/core/supplicant/nm-supplicant-config.h
@@ -29,7 +29,7 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass;
GType nm_supplicant_config_get_type(void);
-NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities);
+NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user);
guint32 nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self);
@@ -54,6 +54,7 @@ gboolean nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig *
guint32 mtu,
NMSettingWirelessSecurityPmf pmf,
NMSettingWirelessSecurityFils fils,
+ GHashTable *files,
GError ** error);
gboolean nm_supplicant_config_add_no_security(NMSupplicantConfig *self, GError **error);
@@ -63,6 +64,7 @@ gboolean nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
const char * con_uuid,
guint32 mtu,
gboolean wired,
+ GHashTable *files,
GError ** error);
gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index 1556e290b6..31740bb184 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -45,6 +45,7 @@ typedef struct {
gpointer user_data;
guint fail_on_idle_id;
guint blobs_left;
+ guint remove_blobs_left;
guint calls_left;
struct _AddNetworkData * add_network_data;
} AssocData;
@@ -2213,6 +2214,7 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
return;
}
+ nm_assert(priv->assoc_data->blobs_left > 0);
priv->assoc_data->blobs_left--;
_LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)",
NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
@@ -2221,6 +2223,162 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
assoc_call_select_network(self);
}
+static GVariant *
+nm_g_bytes_to_variant_ay(const GBytes *bytes)
+{
+ const guint8 *p = NULL;
+ gsize l = 0;
+
+ if (!bytes) {
+ /* for convenience, accept NULL to return an empty variant */
+ } else
+ p = g_bytes_get_data((GBytes *) bytes, &l);
+
+ return nm_g_variant_new_ay(p, l);
+}
+
+static void
+assoc_add_blobs(NMSupplicantInterface *self)
+{
+ NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+ GHashTable *blobs;
+ GHashTableIter iter;
+ const char *blob_name;
+ GBytes *blob_data;
+
+ blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
+ priv->assoc_data->blobs_left = nm_g_hash_table_size(blobs);
+
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to add %u blobs",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ priv->assoc_data->blobs_left);
+
+ if (priv->assoc_data->blobs_left == 0) {
+ assoc_call_select_network(self);
+ return;
+ }
+
+ g_hash_table_iter_init(&iter, blobs);
+ while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: adding blob '%s'",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ blob_name);
+ _dbus_connection_call(
+ self,
+ NM_WPAS_DBUS_IFACE_INTERFACE,
+ "AddBlob",
+ g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)),
+ G_VARIANT_TYPE("()"),
+ G_DBUS_CALL_FLAGS_NONE,
+ DBUS_TIMEOUT_MSEC,
+ priv->assoc_data->cancellable,
+ assoc_add_blob_cb,
+ self);
+ }
+}
+
+static void
+assoc_remove_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+ NMSupplicantInterface *self;
+ NMSupplicantInterfacePrivate *priv;
+ gs_free_error GError *error = NULL;
+ gs_unref_variant GVariant *res = NULL;
+
+ res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+ if (nm_utils_error_is_cancelled(error))
+ return;
+
+ self = NM_SUPPLICANT_INTERFACE(user_data);
+ priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+ /* We don't consider a failure fatal. The new association might be able
+ * to proceed even with the existing blobs, if they don't conflict with new
+ * ones. */
+
+ nm_assert(priv->assoc_data->remove_blobs_left > 0);
+ priv->assoc_data->remove_blobs_left--;
+
+ if (error) {
+ g_dbus_error_strip_remote_error(error);
+ _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to delete blob: %s",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ error->message);
+ } else {
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob removed (%u left)",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ priv->assoc_data->remove_blobs_left);
+ }
+
+ if (priv->assoc_data->remove_blobs_left == 0)
+ assoc_add_blobs(self);
+}
+
+static void
+assoc_get_blobs_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+ NMSupplicantInterface *self;
+ NMSupplicantInterfacePrivate *priv;
+ gs_free_error GError *error = NULL;
+ gs_unref_variant GVariant *res = NULL;
+ gs_unref_variant GVariant *value = NULL;
+ GVariantIter iter;
+ const char *blob_name;
+ GVariant *blob_data;
+
+ res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+ if (nm_utils_error_is_cancelled(error))
+ return;
+
+ self = NM_SUPPLICANT_INTERFACE(user_data);
+ priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+ if (error) {
+ _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: %s",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ error->message);
+ assoc_add_blobs(self);
+ return;
+ }
+
+ g_variant_get(res, "(v)", &value);
+
+ /* While the "Blobs" property is documented as type "as", it is actually "a{say}" */
+ if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE("a{say}"))) {
+ _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: wrong return type %s",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ value ? g_variant_get_type_string(value) : "NULL");
+ assoc_add_blobs(self);
+ return;
+ }
+
+ g_variant_iter_init(&iter, value);
+ priv->assoc_data->remove_blobs_left = g_variant_iter_n_children(&iter);
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to delete %u blobs",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ priv->assoc_data->remove_blobs_left);
+
+ if (priv->assoc_data->remove_blobs_left == 0) {
+ assoc_add_blobs(self);
+ } else {
+ while (g_variant_iter_loop(&iter, "{&s@ay}", &blob_name, &blob_data)) {
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: removing blob '%s'",
+ NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+ blob_name);
+ _dbus_connection_call(self,
+ NM_WPAS_DBUS_IFACE_INTERFACE,
+ "RemoveBlob",
+ g_variant_new("(s)", blob_name),
+ G_VARIANT_TYPE("()"),
+ G_DBUS_CALL_FLAGS_NONE,
+ DBUS_TIMEOUT_MSEC,
+ priv->assoc_data->cancellable,
+ assoc_remove_blob_cb,
+ self);
+ }
+ }
+}
+
static void
assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
@@ -2230,10 +2388,6 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
NMSupplicantInterfacePrivate *priv;
gs_unref_variant GVariant *res = NULL;
gs_free_error GError *error = NULL;
- GHashTable * blobs;
- GHashTableIter iter;
- const char * blob_name;
- GBytes * blob_data;
nm_auto_ref_string NMRefString *name_owner = NULL;
nm_auto_ref_string NMRefString *object_path = NULL;
@@ -2285,34 +2439,21 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
nm_assert(!priv->net_path);
g_variant_get(res, "(o)", &priv->net_path);
- /* Send blobs first; otherwise jump to selecting the network */
- blobs = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
- priv->assoc_data->blobs_left = blobs ? g_hash_table_size(blobs) : 0u;
-
- _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)",
+ _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)",
NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
- priv->net_path,
- priv->assoc_data->blobs_left);
-
- if (priv->assoc_data->blobs_left == 0) {
- assoc_call_select_network(self);
- return;
- }
+ priv->net_path);
- g_hash_table_iter_init(&iter, blobs);
- while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
- _dbus_connection_call(
- self,
- NM_WPAS_DBUS_IFACE_INTERFACE,
- "AddBlob",
- g_variant_new("(s@ay)", blob_name, nm_utils_gbytes_to_variant_ay(blob_data)),
- G_VARIANT_TYPE("()"),
- G_DBUS_CALL_FLAGS_NONE,
- DBUS_TIMEOUT_MSEC,
- priv->assoc_data->cancellable,
- assoc_add_blob_cb,
- self);
- }
+ /* Delete any existing blobs before adding new ones */
+ _dbus_connection_call(self,
+ DBUS_INTERFACE_PROPERTIES,
+ "Get",
+ g_variant_new("(ss)", NM_WPAS_DBUS_IFACE_INTERFACE, "Blobs"),
+ G_VARIANT_TYPE("(v)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ DBUS_TIMEOUT_MSEC,
+ priv->assoc_data->cancellable,
+ assoc_get_blobs_cb,
+ self);
}
static void
diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c
index 0cf35d0e5d..f85b2c9ee4 100644
--- a/src/core/supplicant/tests/test-supplicant-config.c
+++ b/src/core/supplicant/tests/test-supplicant-config.c
@@ -98,7 +98,8 @@ build_supplicant_config(NMConnection * connection,
NMSetting8021x * s_8021x;
gboolean success;
- config = nm_supplicant_config_new(capabilities);
+ config = nm_supplicant_config_new(capabilities,
+ nm_utils_get_connection_first_permissions_user(connection));
s_wifi = nm_connection_get_setting_wireless(connection);
g_assert(s_wifi);
@@ -119,6 +120,7 @@ build_supplicant_config(NMConnection * connection,
mtu,
pmf,
fils,
+ NULL,
&error);
} else {
success = nm_supplicant_config_add_no_security(config, &error);
diff --git a/src/libnm-core-impl/nm-setting-8021x.c b/src/libnm-core-impl/nm-setting-8021x.c
index 6ddbd68066..25b19e41e2 100644
--- a/src/libnm-core-impl/nm-setting-8021x.c
+++ b/src/libnm-core-impl/nm-setting-8021x.c
@@ -3106,6 +3106,86 @@ need_secrets(NMSetting *setting)
/*****************************************************************************/
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+ const struct {
+ const char *property;
+ NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *);
+ const char *(*get_path_func)(NMSetting8021x *);
+ } cert_props[] = {
+ {NM_SETTING_802_1X_CA_CERT,
+ nm_setting_802_1x_get_ca_cert_scheme,
+ nm_setting_802_1x_get_ca_cert_path},
+ {NM_SETTING_802_1X_CLIENT_CERT,
+ nm_setting_802_1x_get_client_cert_scheme,
+ nm_setting_802_1x_get_client_cert_path},
+ {NM_SETTING_802_1X_PRIVATE_KEY,
+ nm_setting_802_1x_get_private_key_scheme,
+ nm_setting_802_1x_get_private_key_path},
+ {NM_SETTING_802_1X_PHASE2_CA_CERT,
+ nm_setting_802_1x_get_phase2_ca_cert_scheme,
+ nm_setting_802_1x_get_phase2_ca_cert_path},
+ {NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
+ nm_setting_802_1x_get_phase2_client_cert_scheme,
+ nm_setting_802_1x_get_phase2_client_cert_path},
+ {NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
+ nm_setting_802_1x_get_phase2_private_key_scheme,
+ nm_setting_802_1x_get_phase2_private_key_path},
+ };
+ NMSetting8021x *s_8021x = NM_SETTING_802_1X(setting);
+ const char *path;
+ guint i;
+
+ if (NM_MORE_ASSERT_ONCE(5)) {
+ GObjectClass *klass;
+ gs_free GParamSpec **properties = NULL;
+ guint n_properties;
+ gboolean found;
+ guint j;
+
+ /* Check that all the properties in the setting with flag CERT_KEY_FILE
+ * are listed in the table, and vice versa. */
+
+ klass = G_OBJECT_GET_CLASS(setting);
+
+ properties = g_object_class_list_properties(klass, &n_properties);
+ for (i = 0; i < n_properties; i++) {
+ if (!(properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE))
+ continue;
+
+ found = FALSE;
+ for (j = 0; j < G_N_ELEMENTS(cert_props); j++) {
+ if (nm_streq0(properties[i]->name, cert_props[j].property)) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ nm_assert(found);
+ }
+
+ for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+ GParamSpec *prop;
+
+ prop = g_object_class_find_property(klass, cert_props[i].property);
+ nm_assert(prop);
+ nm_assert(prop->flags & NM_SETTING_PARAM_CERT_KEY_FILE);
+ }
+ }
+
+ for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+ if (cert_props[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) {
+ path = cert_props[i].get_path_func(s_8021x);
+ if (path) {
+ g_ptr_array_add(files, (gpointer) path);
+ }
+ }
+ }
+}
+
+/*****************************************************************************/
+
static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
@@ -3523,8 +3603,9 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
object_class->set_property = set_property;
object_class->finalize = finalize;
- setting_class->verify = verify;
- setting_class->need_secrets = need_secrets;
+ setting_class->verify = verify;
+ setting_class->need_secrets = need_secrets;
+ setting_class->get_private_files = get_private_files;
/**
* NMSetting8021x:eap:
@@ -3638,7 +3719,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:ca-cert-password:
@@ -3822,7 +3903,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:client-cert-password:
@@ -4046,7 +4127,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:phase2-ca-cert-password:
@@ -4236,7 +4317,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:phase2-client-cert-password:
@@ -4401,7 +4482,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:private-key-password:
@@ -4485,7 +4566,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
"",
"",
G_TYPE_BYTES,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | NM_SETTING_PARAM_CERT_KEY_FILE);
/**
* NMSetting8021x:phase2-private-key-password:
diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c
index 3d57bc3e2e..c0dcac06b1 100644
--- a/src/libnm-core-impl/nm-setting-connection.c
+++ b/src/libnm-core-impl/nm-setting-connection.c
@@ -387,6 +387,47 @@ nm_setting_connection_permissions_user_allowed(NMSettingConnection *setting, con
return FALSE;
}
+guint
+_nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting)
+{
+ NMSettingConnectionPrivate *priv;
+ guint i;
+ guint count = 0;
+
+ nm_assert(NM_IS_SETTING_CONNECTION(setting));
+ priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+ for (i = 0; priv->permissions && i < priv->permissions->len; i++) {
+ const Permission *permission = &g_array_index(priv->permissions, Permission, i);
+
+ if (permission->ptype == PERM_TYPE_USER) {
+ count++;
+ }
+ }
+
+ return count;
+}
+
+const char *
+_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting)
+{
+ NMSettingConnectionPrivate *priv;
+ guint i;
+
+ nm_assert(NM_IS_SETTING_CONNECTION(setting));
+ priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+ for (i = 0; priv->permissions && i < priv->permissions->len; i++) {
+ const Permission *permission = &g_array_index(priv->permissions, Permission, i);
+
+ if (permission->ptype == PERM_TYPE_USER) {
+ return permission->item;
+ }
+ }
+
+ return NULL;
+}
+
/**
* nm_setting_connection_add_permission:
* @setting: the #NMSettingConnection
diff --git a/src/libnm-core-impl/nm-setting-private.h b/src/libnm-core-impl/nm-setting-private.h
index 061fc901ff..82963673ed 100644
--- a/src/libnm-core-impl/nm-setting-private.h
+++ b/src/libnm-core-impl/nm-setting-private.h
@@ -117,6 +117,11 @@ struct _NMSettingClass {
gpointer padding[1];
+ /* returns a list of certificate/key files referenced in the connection.
+ * When the connection is private, we need to verify that the owner of
+ * the connection has access to them. */
+ void (*get_private_files)(NMSetting *setting, GPtrArray *files);
+
const struct _NMMetaSettingInfo *setting_info;
};
@@ -246,6 +251,11 @@ gboolean _nm_setting_clear_secrets(NMSetting * setting,
*/
#define NM_SETTING_PARAM_TO_DBUS_IGNORE_FLAGS (1 << (7 + G_PARAM_USER_SHIFT))
+/* The property can refer to a certificate or key stored on disk. As such,
+ * special care is needed when accessing the file for private connections.
+ */
+#define NM_SETTING_PARAM_CERT_KEY_FILE (1 << (8 + G_PARAM_USER_SHIFT))
+
extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_interface_name;
extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_i;
extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_u;
diff --git a/src/libnm-core-impl/nm-setting.c b/src/libnm-core-impl/nm-setting.c
index 54e1a176c1..8bf8f65329 100644
--- a/src/libnm-core-impl/nm-setting.c
+++ b/src/libnm-core-impl/nm-setting.c
@@ -1034,6 +1034,34 @@ init_from_dbus(NMSetting * setting,
return TRUE;
}
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+ if (NM_MORE_ASSERTS) {
+ GParamSpec **properties;
+ guint n_properties;
+ int i;
+
+ properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(setting), &n_properties);
+ for (i = 0; i < n_properties; i++) {
+ if (properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE) {
+ /* Certificates and keys needs special handling, see setting 802.1X */
+ nm_assert_not_reached();
+ }
+ }
+ g_free(properties);
+ }
+}
+
+void
+_nm_setting_get_private_files(NMSetting *setting, GPtrArray *files)
+{
+ g_return_if_fail(NM_IS_SETTING(setting));
+ g_return_if_fail(files);
+
+ NM_SETTING_GET_CLASS(setting)->get_private_files(setting, files);
+}
+
/**
* nm_setting_get_dbus_property_type:
* @setting: an #NMSetting
@@ -2878,6 +2906,7 @@ nm_setting_class_init(NMSettingClass *setting_class)
setting_class->enumerate_values = enumerate_values;
setting_class->aggregate = aggregate;
setting_class->init_from_dbus = init_from_dbus;
+ setting_class->get_private_files = get_private_files;
/**
* NMSetting:name:
diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h
index b1f084ed54..ae41be5e8b 100644
--- a/src/libnm-core-intern/nm-core-internal.h
+++ b/src/libnm-core-intern/nm-core-internal.h
@@ -906,4 +906,11 @@ _nm_variant_attribute_spec_find_binary_search(const NMVariantAttributeSpec *cons
gboolean _nm_ip_tunnel_mode_is_layer2(NMIPTunnelMode mode);
+/*****************************************************************************/
+
+guint _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting);
+const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting);
+
+void _nm_setting_get_private_files(NMSetting *setting, GPtrArray *files);
+
#endif
diff --git a/src/libnm-std-aux/nm-std-utils.c b/src/libnm-std-aux/nm-std-utils.c
index 18692b198c..6d37185e28 100644
--- a/src/libnm-std-aux/nm-std-utils.c
+++ b/src/libnm-std-aux/nm-std-utils.c
@@ -4,9 +4,13 @@
#include "nm-std-utils.h"
-#include <stdint.h>
#include <assert.h>
+#include <fcntl.h>
+#include <grp.h>
#include <limits.h>
+#include <pwd.h>
+#include <stdint.h>
+#include <sys/types.h>
/*****************************************************************************/
@@ -88,3 +92,111 @@ out_huge:
}
return SIZE_MAX;
}
+
+/*****************************************************************************/
+
+bool
+nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len)
+{
+ struct passwd *pwentry;
+ int errsv;
+ char error[1024];
+
+ errno = 0;
+ pwentry = getpwnam(user);
+ if (!pwentry) {
+ errsv = errno;
+ if (errsv == 0) {
+ snprintf(errbuf, errbuf_len, "user not found");
+ } else {
+ snprintf(errbuf,
+ errbuf_len,
+ "error getting user entry: %d (%s)\n",
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ }
+ return false;
+ }
+
+ if (setgid(pwentry->pw_gid) != 0) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "failed to change group to %u: %d (%s)\n",
+ pwentry->pw_gid,
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+
+ if (initgroups(user, pwentry->pw_gid) != 0) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "failed to reset supplementary group list to %u: %d (%s)\n",
+ pwentry->pw_gid,
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+
+ if (setuid(pwentry->pw_uid) != 0) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "failed to change user to %u: %d (%s)\n",
+ pwentry->pw_uid,
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+
+ return true;
+}
+
+/*****************************************************************************/
+
+bool
+nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len)
+{
+ nm_auto_close int fd = -1;
+ char buffer[4096];
+ char error[1024];
+ ssize_t bytes_read;
+ int errsv;
+
+ fd = open(filename, O_RDONLY);
+ if (fd == -1) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "error opening the file: %d (%s)",
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+
+ while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
+ if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "error writing to stdout: %d (%s)",
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+ }
+
+ if (bytes_read < 0) {
+ errsv = errno;
+ snprintf(errbuf,
+ errbuf_len,
+ "error reading the file: %d (%s)",
+ errsv,
+ strerror_r(errsv, error, sizeof(error)));
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/libnm-std-aux/nm-std-utils.h b/src/libnm-std-aux/nm-std-utils.h
index 76d49d2f5f..63b7e6674d 100644
--- a/src/libnm-std-aux/nm-std-utils.h
+++ b/src/libnm-std-aux/nm-std-utils.h
@@ -34,4 +34,8 @@
size_t nm_utils_get_next_realloc_size(bool true_realloc, size_t requested);
+bool nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_size);
+
+bool nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len);
+
#endif /* __NM_STD_UTILS_H__ */
diff --git a/src/meson.build b/src/meson.build
index 39bfe7ef78..1baf5353ac 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -93,7 +93,7 @@ if enable_nmtui
endif
subdir('nmcli')
subdir('nm-dispatcher')
-subdir('nm-daemon-helper')
+subdir('nm-helpers')
subdir('nm-online')
if enable_nmtui
subdir('nmtui')
diff --git a/src/nm-daemon-helper/meson.build b/src/nm-daemon-helper/meson.build
deleted file mode 100644
index da0d6571e1..0000000000
--- a/src/nm-daemon-helper/meson.build
+++ /dev/null
@@ -1,15 +0,0 @@
-executable(
- 'nm-daemon-helper',
- 'nm-daemon-helper.c',
- include_directories : [
- src_inc,
- top_inc,
- ],
- link_with: [
- libnm_std_aux,
- ],
- link_args: ldflags_linker_script_binary,
- link_depends: linker_script_binary,
- install: true,
- install_dir: nm_libexecdir,
-)
diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md
new file mode 100644
index 0000000000..ab0ea02444
--- /dev/null
+++ b/src/nm-helpers/README.md
@@ -0,0 +1,43 @@
+nm-helpers
+==========
+
+This directory contains stand-alone helper programs used by various
+components.
+
+nm-daemon-helper
+----------------
+
+A internal helper application that is spawned by NetworkManager to
+perform certain actions which can't be done in the daemon.
+
+Currently it's used to do a reverse DNS lookup after reconfiguring the
+libc resolver (which is a process-wide operation), and to read files
+on behalf of unprivileged users (which requires a seteuid that affects
+all the threads of the process).
+
+This is not directly useful to the user.
+
+nm-priv-helper
+--------------
+
+This is a D-Bus activatable, exit-on-idle service, which
+provides an internal API to NetworkManager daemon.
+
+This has no purpose for the user, it is an implementation detail
+of the daemon.
+
+The purpose is that `nm-priv-helper` can execute certain
+privileged operations which NetworkManager process is not
+allowed to. We want to sandbox NetworkManager as much as
+possible, and nm-priv-helper provides a controlled way to
+perform some very specific operations.
+
+As such, nm-priv-helper should still be sandboxed too to only
+being able to execute the operations that are necessary for
+NetworkManager.
+
+nm-priv-helper will reject all D-Bus requests that are not
+originating from the current name owner of
+"org.freedesktop.NetworkManager". That is, it is supposed to
+only reply to NetworkManager daemon and as such is not useful to
+the user directly.
diff --git a/src/nm-helpers/meson.build b/src/nm-helpers/meson.build
new file mode 100644
index 0000000000..5f330cbc94
--- /dev/null
+++ b/src/nm-helpers/meson.build
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# nm-daemon-helper
+
+executable(
+ 'nm-daemon-helper',
+ 'nm-daemon-helper.c',
+ include_directories : [
+ src_inc,
+ top_inc,
+ ],
+ link_with: [
+ libnm_std_aux,
+ ],
+ link_args: ldflags_linker_script_binary,
+ link_depends: linker_script_binary,
+ install: true,
+ install_dir: nm_libexecdir,
+)
+
+# nm-priv-helper
+
+configure_file(
+ input: 'org.freedesktop.nm_priv_helper.service.in',
+ output: '@BASENAME@',
+ install_dir: dbus_system_bus_services_dir,
+ configuration: data_conf,
+)
+
+install_data(
+ 'nm-priv-helper.conf',
+ install_dir: dbus_conf_dir,
+)
+
+executable(
+ 'nm-priv-helper',
+ 'nm-priv-helper.c',
+ include_directories : [
+ src_inc,
+ top_inc,
+ ],
+ dependencies: [
+ glib_dep,
+ ],
+ link_with: [
+ libnm_base,
+ libnm_log_null,
+ libnm_glib_aux,
+ libnm_std_aux,
+ libc_siphash,
+ ],
+ link_args: ldflags_linker_script_binary,
+ link_depends: linker_script_binary,
+ install: true,
+ install_dir: nm_libexecdir,
+)
diff --git a/src/nm-daemon-helper/nm-daemon-helper.c b/src/nm-helpers/nm-daemon-helper.c
similarity index 73%
rename from src/nm-daemon-helper/nm-daemon-helper.c
rename to src/nm-helpers/nm-daemon-helper.c
index e91e71845b..f23ecbc8f4 100644
--- a/src/nm-daemon-helper/nm-daemon-helper.c
+++ b/src/nm-helpers/nm-daemon-helper.c
@@ -98,6 +98,37 @@ cmd_resolve_address(void)
return RETURN_SUCCESS;
}
+static int
+cmd_read_file_as_user(void)
+{
+ nm_auto_free char *user = NULL;
+ nm_auto_free char *filename = NULL;
+ char error[1024];
+
+ user = read_arg();
+ if (!user)
+ return RETURN_INVALID_ARGS;
+
+ filename = read_arg();
+ if (!filename)
+ return RETURN_INVALID_ARGS;
+
+ if (more_args())
+ return RETURN_INVALID_ARGS;
+
+ if (!nm_utils_set_effective_user(user, error, sizeof(error))) {
+ fprintf(stderr, "Failed to set effective user '%s': %s", user, error);
+ return RETURN_ERROR;
+ }
+
+ if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) {
+ fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error);
+ return RETURN_ERROR;
+ }
+
+ return RETURN_SUCCESS;
+}
+
int
main(int argc, char **argv)
{
@@ -111,6 +142,8 @@ main(int argc, char **argv)
return cmd_version();
if (nm_streq(cmd, "resolve-address"))
return cmd_resolve_address();
+ if (nm_streq(cmd, "read-file-as-user"))
+ return cmd_read_file_as_user();
return RETURN_INVALID_CMD;
}
diff --git a/src/nm-helpers/nm-priv-helper.c b/src/nm-helpers/nm-priv-helper.c
new file mode 100644
index 0000000000..2fe9bcf5ef
--- /dev/null
+++ b/src/nm-helpers/nm-priv-helper.c
@@ -0,0 +1,695 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
+
+#include <gio/gunixfdlist.h>
+
+#include "c-list/src/c-list.h"
+#include "libnm-base/nm-priv-helper-utils.h"
+#include "libnm-glib-aux/nm-dbus-aux.h"
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-glib-aux/nm-logging-base.h"
+#include "libnm-glib-aux/nm-time-utils.h"
+
+/* nm-priv-helper doesn't link with libnm-core nor libnm-base, but these
+ * headers can be used independently. */
+#include "libnm-core-public/nm-dbus-interface.h"
+
+/*****************************************************************************/
+
+#define IDLE_TIMEOUT_MSEC 2000
+#define IDLE_TIMEOUT_INFINITY G_MAXINT32
+
+/*****************************************************************************/
+
+/* Serves only the purpose to mark environment variables that are honored by
+ * the application. You can search for this macro, and find what options are supported. */
+#define _ENV(var) ("" var "")
+
+/*****************************************************************************/
+
+typedef struct _GlobalData GlobalData;
+
+typedef struct {
+ CList pending_jobs_lst;
+ GlobalData *gl;
+} PendingJobData;
+
+struct _GlobalData {
+ GDBusConnection *dbus_connection;
+ GCancellable *quit_cancellable;
+
+ GSource *source_sigterm;
+
+ CList pending_jobs_lst_head;
+
+ GSource *source_idle_timeout;
+
+ char *name_owner;
+
+ gint64 start_timestamp_msec;
+
+ guint name_owner_changed_id;
+ guint service_regist_id;
+
+ guint32 timeout_msec;
+
+ bool name_owner_initialized;
+
+ /* This is controlled by $NM_PRIV_HELPER_NO_AUTH_FOR_TESTING. It disables authentication
+ * of the request, so it is ONLY for testing. */
+ bool no_auth_for_testing;
+
+ bool name_requested;
+ bool reject_new_requests;
+
+ bool shutdown_quitting;
+ bool shutdown_timeout;
+};
+
+/*****************************************************************************/
+
+static void _pending_job_register_object(GlobalData *gl, GObject *obj);
+
+/*****************************************************************************/
+
+#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__)
+
+#define _NMLOG(level, ...) \
+ G_STMT_START \
+ { \
+ const NMLogLevel _level = (level); \
+ \
+ if (_nm_logging_enabled(_level)) { \
+ _nm_log(_level, __VA_ARGS__); \
+ } \
+ } \
+ G_STMT_END
+
+/*****************************************************************************/
+
+static void
+_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg)
+{
+ gs_free char *msg = NULL;
+ gint64 running_msec;
+
+ running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec;
+
+ msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%" G_GINT64_FORMAT
+ ".%03d%s, pong=%s",
+ (unsigned long) getpid(),
+ g_dbus_connection_get_unique_name(gl->dbus_connection),
+ gl->name_owner ?: "(none)",
+ (gint64) (running_msec / 1000),
+ (int) (running_msec % 1000),
+ gl->no_auth_for_testing ? ", no-auth-for-testing" : "",
+ arg);
+ g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg));
+}
+
+static void
+_handle_get_fd(GlobalData *gl, GDBusMethodInvocation *invocation, guint32 fd_type)
+{
+ nm_auto_close int fd = -1;
+ gs_unref_object GUnixFDList *fd_list = NULL;
+ gs_free_error GError *error = NULL;
+
+ if (fd_type != (NMPrivHelperGetFDType) fd_type)
+ fd_type = NM_PRIV_HELPER_GET_FD_TYPE_NONE;
+
+ fd = nm_priv_helper_utils_open_fd(fd_type, &error);
+ if (fd < 0) {
+ g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error));
+ return;
+ }
+
+ fd_list = g_unix_fd_list_new_from_array(&fd, 1);
+ nm_steal_fd(&fd);
+
+ g_dbus_method_invocation_return_value_with_unix_fd_list(invocation, NULL, fd_list);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_signal_callback_term(gpointer user_data)
+{
+ GlobalData *gl = user_data;
+
+ _LOGD("sigterm received (%s)",
+ c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations");
+
+ gl->shutdown_quitting = TRUE;
+ g_cancellable_cancel(gl->quit_cancellable);
+ return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+static GDBusConnection *
+_bus_get(GCancellable *cancellable, int *out_exit_code)
+{
+ gs_free_error GError *error = NULL;
+ gs_unref_object GDBusConnection *dbus_connection = NULL;
+
+ dbus_connection = nm_g_bus_get_blocking(cancellable, &error);
+
+ if (!dbus_connection) {
+ gboolean was_cancelled = nm_utils_error_is_cancelled(error);
+
+ NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE);
+ if (!was_cancelled)
+ _LOGE("dbus: failure to get D-Bus connection: %s", error->message);
+ return NULL;
+ }
+
+ /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a
+ * regular request to quit. */
+ g_dbus_connection_set_exit_on_close(dbus_connection, TRUE);
+
+ _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection));
+
+ return g_steal_pointer(&dbus_connection);
+}
+
+/*****************************************************************************/
+
+static void
+_name_owner_changed_cb(GDBusConnection *connection,
+ const char *sender_name,
+ const char *object_path,
+ const char *interface_name,
+ const char *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ GlobalData *gl = user_data;
+ const char *new_owner;
+
+ if (!gl->name_owner_initialized)
+ return;
+
+ if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
+ return;
+
+ g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
+ new_owner = nm_str_not_empty(new_owner);
+
+ _LOGD("%s name-owner changed: %s -> %s",
+ NM_DBUS_SERVICE,
+ gl->name_owner ?: "(null)",
+ new_owner ?: "(null)");
+
+ nm_strdup_reset(&gl->name_owner, new_owner);
+}
+
+typedef struct {
+ GlobalData *gl;
+ char **p_name_owner;
+ gboolean is_cancelled;
+} BusFindNMNameOwnerData;
+
+static void
+_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+ BusFindNMNameOwnerData *data = user_data;
+
+ *data->p_name_owner = nm_strdup_not_empty(name_owner);
+ data->is_cancelled = nm_utils_error_is_cancelled(error);
+ data->gl->name_owner_initialized = TRUE;
+}
+
+static gboolean
+_bus_find_nm_nameowner(GlobalData *gl)
+{
+ BusFindNMNameOwnerData data;
+ guint name_owner_changed_id;
+ gs_free char *name_owner = NULL;
+
+ name_owner_changed_id =
+ nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection,
+ NM_DBUS_SERVICE,
+ _name_owner_changed_cb,
+ gl,
+ NULL);
+
+ data = (BusFindNMNameOwnerData) {
+ .gl = gl,
+ .is_cancelled = FALSE,
+ .p_name_owner = &name_owner,
+ };
+ nm_dbus_connection_call_get_name_owner(gl->dbus_connection,
+ NM_DBUS_SERVICE,
+ 10000,
+ gl->quit_cancellable,
+ _bus_find_nm_nameowner_cb,
+ &data);
+ while (!gl->name_owner_initialized)
+ g_main_context_iteration(NULL, TRUE);
+
+ if (data.is_cancelled) {
+ g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id);
+ return FALSE;
+ }
+
+ gl->name_owner_changed_id = name_owner_changed_id;
+ gl->name_owner = g_steal_pointer(&name_owner);
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_bus_method_call(GDBusConnection *connection,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ GlobalData *gl = user_data;
+ const char *arg_s;
+ guint32 arg_u;
+
+ nm_assert(nm_streq(object_path, NM_PRIV_HELPER_DBUS_OBJECT_PATH));
+ nm_assert(nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME));
+
+ if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) {
+ _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED",
+ sender,
+ method_name,
+ g_variant_get_type_string(parameters));
+ g_dbus_method_invocation_return_error(invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Access denied");
+ return;
+ }
+
+ if (gl->reject_new_requests) {
+ /* after the name was released, we must not accept new requests. This new
+ * request was probably targeted against the unique-name. But we already
+ * gave up the well-known name. If we'd accept new request now, they would
+ * keep the service running indefinitely (and thus preventing the service
+ * to restart and serve the well-known name. */
+ _LOGT("dbus: request sender=%s, %s%s, SERVER SHUTTING DOWN",
+ sender,
+ method_name,
+ g_variant_get_type_string(parameters));
+ g_dbus_method_invocation_return_error(invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_NO_SERVER,
+ "Server is exiting");
+ return;
+ }
+
+ _LOGT("dbus: request sender=%s, %s%s",
+ sender,
+ method_name,
+ g_variant_get_type_string(parameters));
+
+ if (!nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME))
+ goto out_unknown_method;
+
+ if (nm_streq(method_name, "GetFD")) {
+ g_variant_get(parameters, "(u)", &arg_u);
+ _handle_get_fd(gl, invocation, arg_u);
+ return;
+ }
+ if (nm_streq(method_name, "Ping")) {
+ g_variant_get(parameters, "(&s)", &arg_s);
+ _handle_ping(gl, invocation, arg_s);
+ return;
+ }
+
+out_unknown_method:
+ g_dbus_method_invocation_return_error(invocation,
+ G_DBUS_ERROR,
+ G_DBUS_ERROR_UNKNOWN_METHOD,
+ "Unknown method %s",
+ method_name);
+}
+
+static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
+ NM_PRIV_HELPER_DBUS_IFACE_NAME,
+ .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+ NM_DEFINE_GDBUS_METHOD_INFO(
+ "Ping",
+ .in_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ),
+ .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ),
+ NM_DEFINE_GDBUS_METHOD_INFO("GetFD",
+ .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+ NM_DEFINE_GDBUS_ARG_INFO("fd_type", "u"), ), ), ), );
+
+static gboolean
+_bus_register_service(GlobalData *gl)
+{
+ static const GDBusInterfaceVTable interface_vtable = {
+ .method_call = _bus_method_call,
+ };
+ gs_free_error GError *error = NULL;
+ NMDBusConnectionCallBlockingData data = {
+ .result = NULL,
+ };
+ gs_unref_variant GVariant *ret = NULL;
+ guint32 ret_val;
+
+ gl->service_regist_id =
+ g_dbus_connection_register_object(gl->dbus_connection,
+ NM_PRIV_HELPER_DBUS_OBJECT_PATH,
+ interface_info,
+ NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
+ gl,
+ NULL,
+ &error);
+ if (gl->service_regist_id == 0) {
+ _LOGE("dbus: error registering object %s: %s",
+ NM_PRIV_HELPER_DBUS_OBJECT_PATH,
+ error->message);
+ return FALSE;
+ }
+
+ _LOGD("dbus: object %s registered", NM_PRIV_HELPER_DBUS_OBJECT_PATH);
+
+ /* regardless whether the request is successful, after we start calling
+ * RequestName, we remember that we need to ReleaseName it. */
+ gl->name_requested = TRUE;
+
+ nm_dbus_connection_call_request_name(gl->dbus_connection,
+ NM_PRIV_HELPER_DBUS_BUS_NAME,
+ DBUS_NAME_FLAG_ALLOW_REPLACEMENT
+ | DBUS_NAME_FLAG_REPLACE_EXISTING,
+ 10000,
+ gl->quit_cancellable,
+ nm_dbus_connection_call_blocking_callback,
+ &data);
+
+ /* Note that with D-Bus activation, the first request will already hit us before RequestName
+ * completes. So when we start iterating the main context, the first request may already come
+ * in. */
+
+ ret = nm_dbus_connection_call_blocking(&data, &error);
+
+ if (nm_utils_error_is_cancelled(error))
+ return FALSE;
+
+ if (error) {
+ _LOGE("d-bus: failed to request name %s: %s", NM_PRIV_HELPER_DBUS_BUS_NAME, error->message);
+ return FALSE;
+ }
+
+ g_variant_get(ret, "(u)", &ret_val);
+
+ if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
+ _LOGW("dbus: request name for %s failed to take name (response %u)",
+ NM_PRIV_HELPER_DBUS_BUS_NAME,
+ ret_val);
+ return FALSE;
+ }
+
+ _LOGD("dbus: request name for %s succeeded", NM_PRIV_HELPER_DBUS_BUS_NAME);
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_idle_timeout_cb(gpointer user_data)
+{
+ GlobalData *gl = user_data;
+
+ _LOGT("idle-timeout: expired");
+ nm_clear_g_source_inst(&gl->source_idle_timeout);
+ gl->shutdown_timeout = TRUE;
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+_idle_timeout_restart(GlobalData *gl)
+{
+ nm_clear_g_source_inst(&gl->source_idle_timeout);
+
+ if (gl->shutdown_quitting)
+ return;
+
+ if (!c_list_is_empty(&gl->pending_jobs_lst_head))
+ return;
+
+ if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY)
+ return;
+
+ nm_assert(gl->timeout_msec < G_MAXINT32);
+ G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT);
+
+ _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec);
+ gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_pending_job_register_object_release_on_idle_cb(gpointer data)
+{
+ PendingJobData *idle_data = data;
+ GlobalData *gl = idle_data->gl;
+
+ c_list_unlink_stale(&idle_data->pending_jobs_lst);
+ nm_g_slice_free(idle_data);
+
+ _idle_timeout_restart(gl);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was)
+{
+ /* The object might be destroyed on another thread. We need
+ * to sync with the main GMainContext by scheduling an idle action
+ * there. */
+ nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data);
+}
+
+static void
+_pending_job_register_object(GlobalData *gl, GObject *obj)
+{
+ PendingJobData *idle_data;
+
+ /* if we just hit the timeout, we can ignore it. */
+ gl->shutdown_timeout = FALSE;
+
+ if (nm_clear_g_source_inst(&gl->source_idle_timeout))
+ _LOGT("idle-timeout: suspend timeout for pending request");
+
+ idle_data = g_slice_new(PendingJobData);
+
+ idle_data->gl = gl;
+ c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst);
+
+ g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data);
+}
+
+/*****************************************************************************/
+
+static void
+_bus_release_name_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+ _nm_unused gs_unref_object GObject *keep_alive_object = NULL;
+ GlobalData *gl;
+
+ nm_utils_user_data_unpack(user_data, &gl, &keep_alive_object);
+
+ gl->reject_new_requests = TRUE;
+ g_main_context_wakeup(NULL);
+}
+
+static gboolean
+_bus_release_name(GlobalData *gl)
+{
+ gs_unref_object GObject *keep_alive_object = NULL;
+ int r;
+
+ /* We already requested a name. To exit-on-idle without race, we need to dance.
+ * See https://lists.freedesktop.org/archives/dbus/2015-May/016671.html . */
+
+ if (!gl->name_requested)
+ return FALSE;
+
+ gl->name_requested = FALSE;
+ gl->shutdown_quitting = TRUE;
+
+ _LOGT("shutdown: release-name");
+
+ keep_alive_object = g_object_new(G_TYPE_OBJECT, NULL);
+
+ /* we use the _pending_job_register_object() mechanism to make the loop busy during
+ * shutdown. */
+ _pending_job_register_object(gl, keep_alive_object);
+
+ r = nm_sd_notify("STOPPING=1");
+ if (r < 0)
+ _LOGW("shutdown: sd_notifiy(STOPPING=1) failed: %s", nm_strerror_native(-r));
+ else
+ _LOGT("shutdown: sd_notifiy(STOPPING=1) succeeded");
+
+ g_dbus_connection_call(gl->dbus_connection,
+ DBUS_SERVICE_DBUS,
+ DBUS_PATH_DBUS,
+ DBUS_INTERFACE_DBUS,
+ "ReleaseName",
+ g_variant_new("(s)", NM_PRIV_HELPER_DBUS_BUS_NAME),
+ G_VARIANT_TYPE("(u)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ 10000,
+ NULL,
+ _bus_release_name_cb,
+ nm_utils_user_data_pack(gl, g_steal_pointer(&keep_alive_object)));
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_initial_setup(GlobalData *gl)
+{
+ gl->no_auth_for_testing =
+ _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_NO_AUTH_FOR_TESTING")),
+ 0,
+ 0,
+ 1,
+ 0);
+ gl->timeout_msec =
+ _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_IDLE_TIMEOUT_MSEC")),
+ 0,
+ 0,
+ G_MAXINT32,
+ IDLE_TIMEOUT_MSEC);
+
+ gl->quit_cancellable = g_cancellable_new();
+
+ signal(SIGPIPE, SIG_IGN);
+ gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl);
+}
+
+/*****************************************************************************/
+
+int
+main(int argc, char **argv)
+{
+ GlobalData _gl = {
+ .quit_cancellable = NULL,
+ .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head),
+ };
+ GlobalData *const gl = &_gl;
+ int exit_code;
+ int r = 0;
+
+ _nm_logging_enabled_init(g_getenv(_ENV("NM_PRIV_HELPER_LOG")));
+
+ gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME);
+
+ _LOGD("starting nm-priv-helper (%s)", NM_DIST_VERSION);
+
+ _initial_setup(gl);
+
+ if (gl->no_auth_for_testing) {
+ _LOGW("WARNING: running in debug mode without authentication "
+ "(NM_PRIV_HELPER_NO_AUTH_FOR_TESTING). ");
+ }
+
+ if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY)
+ _LOGT("idle-timeout: %u msec", gl->timeout_msec);
+ else
+ _LOGT("idle-timeout: disabled");
+
+ gl->dbus_connection = _bus_get(gl->quit_cancellable, &r);
+ if (!gl->dbus_connection) {
+ exit_code = r;
+ goto done;
+ }
+
+ if (!_bus_find_nm_nameowner(gl)) {
+ /* abort due to cancellation. That is success. */
+ exit_code = EXIT_SUCCESS;
+ goto done;
+ }
+ _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)");
+
+ _idle_timeout_restart(gl);
+
+ exit_code = EXIT_SUCCESS;
+
+ if (!_bus_register_service(gl)) {
+ /* We failed to RequestName, but due to D-Bus activation we
+ * might have a pending request still (on the unique name).
+ * Process it below.
+ *
+ * Let's fake a shutdown signal, and still process the request below. */
+ if (!g_cancellable_is_cancelled(gl->quit_cancellable))
+ exit_code = EXIT_FAILURE;
+ gl->shutdown_quitting = TRUE;
+
+ if (gl->name_requested) {
+ /* We requested a name, but something went wrong. Below we will release
+ * the name right away. */
+ } else {
+ /* In case we didn't even went as far to request the name. New requests
+ * can only come via the unique name, and as we are shutting down, they
+ * are rejected. */
+ gl->reject_new_requests = TRUE;
+ }
+ }
+
+ while (TRUE) {
+ if (gl->shutdown_quitting)
+ _bus_release_name(gl);
+
+ if (!c_list_is_empty(&gl->pending_jobs_lst_head)) {
+ /* we must first reply to all requests. No matter what. */
+ } else if (gl->shutdown_quitting || gl->shutdown_timeout) {
+ /* we either hit the idle timeout or received SIGTERM. Note that
+ * if we received an idle-timeout and the very moment afterwards
+ * a new request, then _bus_method_call() will clear gl->shutdown_timeout
+ * (via _pending_job_register_object()). */
+ if (!_bus_release_name(gl))
+ break;
+ }
+
+ g_main_context_iteration(NULL, TRUE);
+ }
+
+done:
+ _LOGD("shutdown: cleanup");
+
+ gl->shutdown_quitting = TRUE;
+ g_cancellable_cancel(gl->quit_cancellable);
+
+ nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head));
+
+ if (gl->service_regist_id != 0) {
+ g_dbus_connection_unregister_object(gl->dbus_connection,
+ nm_steal_int(&gl->service_regist_id));
+ }
+ if (gl->name_owner_changed_id != 0) {
+ g_dbus_connection_signal_unsubscribe(gl->dbus_connection,
+ nm_steal_int(&gl->name_owner_changed_id));
+ }
+
+ if (gl->dbus_connection) {
+ g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL);
+ g_clear_object(&gl->dbus_connection);
+ }
+
+ nm_g_main_context_iterate_ready(NULL);
+
+ nm_clear_g_free(&gl->name_owner);
+
+ nm_clear_g_source_inst(&gl->source_sigterm);
+ nm_clear_g_source_inst(&gl->source_idle_timeout);
+ g_clear_object(&gl->quit_cancellable);
+
+ _LOGD("exit (%d)", exit_code);
+ return exit_code;
+}
diff --git a/src/nm-helpers/nm-priv-helper.conf b/src/nm-helpers/nm-priv-helper.conf
new file mode 100644
index 0000000000..c0d046178b
--- /dev/null
+++ b/src/nm-helpers/nm-priv-helper.conf
@@ -0,0 +1,13 @@
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <policy user="root">
+ <allow own="org.freedesktop.nm_priv_helper"/>
+ <allow send_destination="org.freedesktop.nm_priv_helper"/>
+ </policy>
+ <policy context="default">
+ <deny own="org.freedesktop.nm_priv_helper"/>
+ <deny send_destination="org.freedesktop.nm_priv_helper"/>
+ </policy>
+</busconfig>
diff --git a/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in
new file mode 100644
index 0000000000..9e86a635b2
--- /dev/null
+++ b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.nm_priv_helper
+Exec=@libexecdir@/nm-priv-helper
+User=root
+SystemdService=nm-priv-helper.service
--
2.52.0