CVE-2022-4968: Insufficient Information (NVD-CWE-noinfo)
value: netplan leaks the private key of wireguard to local users. Versions after 1.0 are not affected.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-07T01:15Z
Repository: https://github.com/canonical/netplan
Language: C
Files changed: src/networkd.c, src/networkd.h, src/nm.c, src/openvswitch.c, src/sriov.c, src/util-internal.h, src/util.c, tests/generator/test_auth.py, tests/generator/test_wifis.py, tests/integration/base.py
Commit date: 2024-06-27 09:21:15 +0200
Commit message: libnetplan: use more restrictive file permissions

A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
and accepts the owner, group and file mode as arguments. When these
properties can't be set, when the generator is called by a non-root user
for example, it will not hard-fail. This function is called by unit
tests where we can't set the owner to a privileged account for example.

When generating backend files, use more restrictive permissions:

networkd related files will be owned by root:systemd-network and have
mode 0640.

service unit files will be owned by root:root and have mode 0640.
udevd files will be owned by root:root with mode 0640.

wpa_supplicant and Network Manager files will continue with the existing
permissions.

Autopkgtests will check if the permissions are set as expected when
calling the generator.

This fix addresses CVE-2022-4968

Diff: diff --git a/src/networkd.c b/src/networkd.c
index babde5d..3ea7531 100644
--- a/src/networkd.c
+++ b/src/networkd.c
@@ -300,11 +300,10 @@ write_wireguard_params(GString* s, const NetplanNetDefinition* def)
 
 STATIC void
 write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
 {
     GString* s = NULL;
-    mode_t orig_umask;
 
     /* Don't write .link files for virtual devices; they use .netdev instead.
      * Don't write .link files for MODEM devices, as they aren't supported by networkd.
      */
     if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL || def->type == NETPLAN_DEF_TYPE_MODEM)
@@ -372,13 +371,11 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char
 
     if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET)
         g_string_append_printf(s, "LargeReceiveOffload=%s\n",
         (def->large_receive_offload ? "true" : "false"));
 
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, ".link");
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
 }
 
 STATIC gboolean
 write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** error)
 {
@@ -392,11 +389,11 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
     g_string_append(s, "After=network.target\n");
     g_string_append(s, "ConditionFileIsExecutable="SBINDIR"/iw\n");
     g_string_append(s, "\n[Service]\nType=oneshot\n");
     g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {
         // LCOV_EXCL_START
         g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m\n");
         return FALSE;
@@ -584,11 +581,10 @@ write_vxlan_parameters(const NetplanNetDefinition* def, GString* s)
 
 STATIC void
 write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
 {
     GString* s = NULL;
-    mode_t orig_umask;
 
     g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL);
 
     if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) {
         g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id);
@@ -683,15 +679,11 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
             break;
 
         default: g_assert_not_reached(); // LCOV_EXCL_LINE
     }
 
-    /* these do not contain secrets and need to be readable by
-     * systemd-networkd - LP: #1736965 */
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, ".netdev");
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
 }
 
 STATIC void
 write_route(NetplanIPRoute* r, GString* s)
 {
@@ -831,11 +823,10 @@ _netplan_netdef_write_network_file(
         GError** error)
 {
     g_autoptr(GString) network = NULL;
     g_autoptr(GString) link = NULL;
     GString* s = NULL;
-    mode_t orig_umask;
 
     SET_OPT_OUT_PTR(has_been_written, FALSE);
 
     if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) {
         g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id);
@@ -1097,15 +1088,11 @@ _netplan_netdef_write_network_file(
         if (link->len > 0)
             g_string_append_printf(s, "\n[Link]\n%s", link->str);
         if (network->len > 0)
             g_string_append_printf(s, "\n[Network]\n%s", network->str);
 
-        /* these do not contain secrets and need to be readable by
-         * systemd-networkd - LP: #1736965 */
-        orig_umask = umask(022);
-        _netplan_g_string_free_to_file(s, rootdir, path, ".network");
-        umask(orig_umask);
+        _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
     }
 
     SET_OPT_OUT_PTR(has_been_written, TRUE);
     return TRUE;
 }
@@ -1113,11 +1100,10 @@ _netplan_netdef_write_network_file(
 STATIC void
 write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
 {
     GString* s = NULL;
     g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
-    mode_t orig_umask;
 
     /* do we need to write a .rules file?
      * It's only required for reliably setting the name of a physical device
      * until systemd issue #9006 is resolved. */
     if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL)
@@ -1147,13 +1133,11 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
     if (def->match.mac)
         g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac);
 
     g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
 
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 }
 
 STATIC gboolean
 append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, const char* id, GError** error)
 {
@@ -1298,11 +1282,10 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
 /* netplan-feature: generated-supplicant */
 STATIC void
 write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
 {
     g_autofree gchar *stdouth = NULL;
-    mode_t orig_umask;
 
     stdouth = systemd_escape(def->id);
 
     GString* s = g_string_new("[Unit]\n");
     g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", stdouth, ".service", NULL);
@@ -1317,22 +1300,19 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
     if (def->type != NETPLAN_DEF_TYPE_WIFI) {
         g_string_append(s, " -Dwired\n");
     } else {
         g_string_append(s, " -Dnl80211,wext\n");
     }
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 }
 
 STATIC gboolean
 write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** error)
 {
     GHashTableIter iter;
     GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
     g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
-    mode_t orig_umask;
 
     g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
     if (def->type == NETPLAN_DEF_TYPE_WIFI) {
         if (!def->access_points) {
             g_string_free(s, TRUE);
@@ -1421,13 +1401,11 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
         }
         g_string_append(s, "}\n");
     }
 
     /* use tight permissions as this contains secrets */
-    orig_umask = umask(077);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
     return TRUE;
 }
 
 /**
  * Generate networkd configuration in @rootdir/run/systemd/network/ from the
@@ -1567,11 +1545,11 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
     // The "ConditionPathIsSymbolicLink" is Netplan's s-n-wait-online enablement symlink,
     // as we want to run -wait-online only if enabled by Netplan.
     GString* content = g_string_new("[Unit]\n"
         "ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service\n");
     if (g_hash_table_size(non_optional_interfaces) == 0) {
-        _netplan_g_string_free_to_file(content, rootdir, override, NULL);
+        _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
         g_hash_table_destroy(non_optional_interfaces);
         return FALSE;
     }
 
     // We have non-optional interface, so let's wait for those explicitly
@@ -1591,11 +1569,11 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
         else if (min_oper_state)
             g_string_append_printf(content, ":%s", min_oper_state);
     }
     g_string_append(content, "\n");
 
-    _netplan_g_string_free_to_file(content, rootdir, override, NULL);
+    _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
     g_hash_table_destroy(non_optional_interfaces);
     return TRUE;
 }
 
 /**
diff --git a/src/networkd.h b/src/networkd.h
index 8332b7b..9f80585 100644
--- a/src/networkd.h
+++ b/src/networkd.h
@@ -18,10 +18,12 @@
 #pragma once
 
 #include "netplan.h"
 #include <glib.h>
 
+#define NETWORKD_GROUP "systemd-network"
+
 NETPLAN_INTERNAL gboolean
 _netplan_netdef_write_networkd(
         const NetplanState* np_state,
         const NetplanNetDefinition* def,
         const char *rootdir,
diff --git a/src/nm.c b/src/nm.c
index c5135aa..81be997 100644
--- a/src/nm.c
+++ b/src/nm.c
@@ -1150,17 +1150,17 @@ netplan_state_finish_nm_write(
         iter = iter->next;
     }
 
     /* write generated NetworkManager drop-in config */
     if (nm_conf->len > 0)
-        _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
+        _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
     else
         g_string_free(nm_conf, TRUE);
 
     /* write generated udev rules */
     if (udev_rules->len > 0)
-        _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
+        _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
     else
         g_string_free(udev_rules, TRUE);
 
     return TRUE;
 }
diff --git a/src/openvswitch.c b/src/openvswitch.c
index 6eb0688..2ab77e7 100644
--- a/src/openvswitch.c
+++ b/src/openvswitch.c
@@ -64,11 +64,11 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
      */
     if (cleanup)
         g_string_append(s, "StartLimitBurst=0\n");
     g_string_append(s, cmds->str);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {
         // LCOV_EXCL_START
         g_set_error(error, NETPLAN_FILE_ERROR, errno, "failed to create enablement symlink: %m\n");
diff --git a/src/sriov.c b/src/sriov.c
index 53c0b86..63ef736 100644
--- a/src/sriov.c
+++ b/src/sriov.c
@@ -52,11 +52,11 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
 
     g_string_append(s, "\n[Service]\nType=oneshot\n");
     g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
     g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind --debug %s\n", interfaces->str);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
     g_string_free(interfaces, TRUE);
 
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {
         // LCOV_EXCL_START
@@ -88,11 +88,11 @@ write_sriov_apply_systemd_unit(GHashTable* pfs, const char* rootdir, GError** er
     }
 
     g_string_append(s, "\n[Service]\nType=oneshot\n");
     g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan apply --sriov-only\n");
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {
         // LCOV_EXCL_START
         g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT,
diff --git a/src/util-internal.h b/src/util-internal.h
index 0fa0e00..8278e85 100644
--- a/src/util-internal.h
+++ b/src/util-internal.h
@@ -38,10 +38,13 @@ NETPLAN_INTERNAL void
 _netplan_safe_mkdir_p_dir(const char* file_path);
 
 NETPLAN_INTERNAL void
 _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
 
+void
+_netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode);
+
 NETPLAN_INTERNAL void
 _netplan_unlink_glob(const char* rootdir, const char* _glob);
 
 NETPLAN_INTERNAL int
 _netplan_find_yaml_glob(const char* rootdir, glob_t* out_glob);
diff --git a/src/util.c b/src/util.c
index cabc293..af3e166 100644
--- a/src/util.c
+++ b/src/util.c
@@ -21,10 +21,13 @@
 #include <fnmatch.h>
 #include <errno.h>
 #include <regex.h>
 #include <string.h>
 #include <sys/mman.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
 
 #include <glib.h>
 #include <glib/gprintf.h>
 #include <yaml.h>
 
@@ -85,10 +88,53 @@ void _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char*
         exit(1);
         // LCOV_EXCL_STOP
     }
 }
 
+void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode)
+{
+    g_autofree char* full_path = NULL;
+    g_autofree char* path_suffix = NULL;
+    g_autofree char* contents = g_string_free(s, FALSE);
+    GError* error = NULL;
+    struct passwd* pw = NULL;
+    struct group* gr = NULL;
+    int ret = 0;
+
+    path_suffix = g_strjoin(NULL, path, suffix, NULL);
+    full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
+    _netplan_safe_mkdir_p_dir(full_path);
+    if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
+        /* the mkdir() just succeeded, there is no sensible
+         * method to test this without root privileges, bind mounts, and
+         * simulating ENOSPC */
+        // LCOV_EXCL_START
+        g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
+        exit(1);
+        // LCOV_EXCL_STOP
+    }
+
+    /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
+     * It's OK to fail to set the owners and mode as this code will be called from unit tests.
+     * The autopkgtests will check if the owner/group and mode are correctly set.
+     */
+    pw = getpwnam(owner);
+    if (!pw) {
+        g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
+    }
+    gr = getgrnam(group);
+    if (!gr) {
+        g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
+    }
+    if (pw && gr) {
+        ret = chown(full_path, pw->pw_uid, gr->gr_gid);
+        if (ret != 0) {
+            g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
+        }
+    }
+}
+
 /**
  * Remove all files matching given glob.
  */
 void
 _netplan_unlink_glob(const char* rootdir, const char* _glob)
diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
index de23adb..d3d886c 100644
--- a/tests/generator/test_auth.py
+++ b/tests/generator/test_auth.py
@@ -224,11 +224,11 @@ network={
         self.assertTrue(os.path.isfile(os.path.join(
             self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')))
 
         with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f:
             self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'})
-            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
+            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
         self.assertTrue(os.path.islink(os.path.join(
             self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service')))
 
 
 class TestNetworkManager(TestBase):
diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
index 4b151d8..0962a88 100644
--- a/tests/generator/test_wifis.py
+++ b/tests/generator/test_wifis.py
@@ -138,11 +138,11 @@ network={
             self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600)
         self.assertTrue(os.path.isfile(os.path.join(
             self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
         with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f:
             self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'})
-            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
+            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
         self.assertTrue(os.path.islink(os.path.join(
             self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
 
     def test_wifi_route(self):
         self.generate('''network:
diff --git a/tests/integration/base.py b/tests/integration/base.py
index 83f16f3..3c64ad6 100644
--- a/tests/integration/base.py
+++ b/tests/integration/base.py
@@ -30,10 +30,12 @@ import tempfile
 import unittest
 import shutil
 import gi
 import glob
 import json
+import pwd
+import grp
 
 # make sure we point to libnetplan properly.
 os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))})
 
 test_backends = "networkd NetworkManager" if "NETPLAN_TEST_BACKENDS" not in os.environ else os.environ["NETPLAN_TEST_BACKENDS"]
@@ -373,10 +375,93 @@ class IntegrationTestsBase(unittest.TestCase):
                 self.networkd_wait_connected(iface, 60)
             # wait for iproute2 state change
             if state:
                 self.wait_output(['ip', 'addr', 'show', iface], state, 30)
 
+        # Assert file permissions
+        self.assert_file_permissions()
+
+    def assert_file_permissions(self):
+        """ Check if the generated files have the expected permissions """
+
+        nd_expected_mode = 0o100640
+        nd_expected_owner = 'root'
+        nd_expected_group = 'systemd-network'
+
+        sd_expected_mode = 0o100640
+        sd_expected_owner = 'root'
+        sd_expected_group = 'root'
+
+        udev_expected_mode = 0o100640
+        udev_expected_owner = 'root'
+        udev_expected_group = 'root'
+
+        nm_expected_mode = 0o100600
+        nm_expected_owner = 'root'
+        nm_expected_group = 'root'
+
+        wpa_expected_mode = 0o100600
+        wpa_expected_owner = 'root'
+        wpa_expected_group = 'root'
+
+        # Check systemd-networkd files
+        base_path = '/run/systemd/network'
+        files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')
+
+        # Check Network Manager files
+        base_path = '/run/NetworkManager/system-connections'
+        files = glob.glob(f'{base_path}/*.nmconnection')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')
+
+        # Check wpa_supplicant configuration files
+        base_path = '/run/netplan'
+        files = glob.glob(f'{base_path}/wpa-*.conf')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')
+
+        # Check systemd service unit files
+        base_path = '/run/systemd/system/'
+        files = glob.glob(f'{base_path}/netplan-*.service')
+        files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')
+
+        # Check systemd-udevd files
+        udev_path = '/run/udev/rules.d'
+        link_path = '/run/systemd/network'
+        files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')
+
     def state(self, iface, state):
         '''Tell generate_and_settle() to wait for a specific state'''
         return iface + '/' + state
 
     def state_up(self, iface):

Vulnerable code:     mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, ".link");
    umask(orig_umask);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    mode_t orig_umask;
    /* these do not contain secrets and need to be readable by
     * systemd-networkd - LP: #1736965 */
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, ".netdev");
    umask(orig_umask);
    mode_t orig_umask;
        /* these do not contain secrets and need to be readable by
         * systemd-networkd - LP: #1736965 */
        orig_umask = umask(022);
        _netplan_g_string_free_to_file(s, rootdir, path, ".network");
        umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(022);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
    mode_t orig_umask;
    orig_umask = umask(077);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    umask(orig_umask);
        _netplan_g_string_free_to_file(content, rootdir, override, NULL);
    _netplan_g_string_free_to_file(content, rootdir, override, NULL);
        _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
        _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
Fixed code:     _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
        _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
        _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
#define NETWORKD_GROUP "systemd-network"

        _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
        _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
void
_netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode);

#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode)
{
    g_autofree char* full_path = NULL;
    g_autofree char* path_suffix = NULL;
    g_autofree char* contents = g_string_free(s, FALSE);
    GError* error = NULL;
    struct passwd* pw = NULL;
    struct group* gr = NULL;
    int ret = 0;

    path_suffix = g_strjoin(NULL, path, suffix, NULL);
    full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
    _netplan_safe_mkdir_p_dir(full_path);
    if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
        /* the mkdir() just succeeded, there is no sensible
         * method to test this without root privileges, bind mounts, and
         * simulating ENOSPC */
        // LCOV_EXCL_START
        g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
        exit(1);
        // LCOV_EXCL_STOP
    }

    /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
     * It's OK to fail to set the owners and mode as this code will be called from unit tests.
     * The autopkgtests will check if the owner/group and mode are correctly set.
     */
    pw = getpwnam(owner);
    if (!pw) {
        g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
    }
    gr = getgrnam(group);
    if (!gr) {
        g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
    }
    if (pw && gr) {
        ret = chown(full_path, pw->pw_uid, gr->gr_gid);
        if (ret != 0) {
            g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
        }
    }
}

            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
import pwd
import grp
        # Assert file permissions
        self.assert_file_permissions()

    def assert_file_permissions(self):
        """ Check if the generated files have the expected permissions """

        nd_expected_mode = 0o100640
        nd_expected_owner = 'root'
        nd_expected_group = 'systemd-network'

        sd_expected_mode = 0o100640
        sd_expected_owner = 'root'
        sd_expected_group = 'root'

        udev_expected_mode = 0o100640
        udev_expected_owner = 'root'
        udev_expected_group = 'root'

        nm_expected_mode = 0o100600
        nm_expected_owner = 'root'
        nm_expected_group = 'root'

        wpa_expected_mode = 0o100600
        wpa_expected_owner = 'root'
        wpa_expected_group = 'root'

        # Check systemd-networkd files
        base_path = '/run/systemd/network'
        files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')

        # Check Network Manager files
        base_path = '/run/NetworkManager/system-connections'
        files = glob.glob(f'{base_path}/*.nmconnection')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')

        # Check wpa_supplicant configuration files
        base_path = '/run/netplan'
        files = glob.glob(f'{base_path}/wpa-*.conf')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')

        # Check systemd service unit files
        base_path = '/run/systemd/system/'
        files = glob.glob(f'{base_path}/netplan-*.service')
        files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')

        # Check systemd-udevd files
        udev_path = '/run/udev/rules.d'
        link_path = '/run/systemd/network'
        files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
        for file in files:
            res = os.stat(file)
            user = pwd.getpwuid(res.st_uid)
            group = grp.getgrgid(res.st_gid)
            self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
            self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
            self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')