diff --git a/meta-networking/recipes-connectivity/netplan/netplan/CVE-2022-4968.patch b/meta-networking/recipes-connectivity/netplan/netplan/CVE-2022-4968.patch new file mode 100644 index 0000000000..0942309cb1 --- /dev/null +++ b/meta-networking/recipes-connectivity/netplan/netplan/CVE-2022-4968.patch @@ -0,0 +1,442 @@ +From 78bfd2429a64452cb8023ec1a56837874c641f5f Mon Sep 17 00:00:00 2001 +From: Danilo Egea Gondolfo +Date: Wed, 22 May 2024 15:44:16 +0100 +Subject: [PATCH] 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 + +Fix conflicts according to: +https://git.launchpad.net/~ubuntu-core-dev/netplan/+git/ubuntu/diff/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch?id=ed684b8a3eb282b9bc7c0f18ad6b2249e7f3ef30 + +CVE: CVE-2022-4968 + +Upstream-Status: Backport [https://github.com/canonical/netplan/commit/4c39b75b5c6ae7d976bda6da68da60d9a7f085ee] + +Signed-off-by: Jinfeng Wang + +update +--- + src/networkd.c | 34 +++----------- + src/networkd.h | 2 + + src/nm.c | 4 +- + src/openvswitch.c | 2 +- + src/sriov.c | 2 +- + src/util-internal.h | 3 ++ + src/util.c | 46 +++++++++++++++++++ + tests/generator/test_auth.py | 2 +- + tests/generator/test_wifis.py | 2 +- + tests/integration/base.py | 86 +++++++++++++++++++++++++++++++++++ + 10 files changed, 149 insertions(+), 34 deletions(-) + +diff --git a/src/networkd.c b/src/networkd.c +index 6d26047c..56e6e6ed 100644 +--- a/src/networkd.c ++++ b/src/networkd.c +@@ -231,7 +231,6 @@ 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. +@@ -286,9 +285,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char + if (def->large_receive_offload) + g_string_append_printf(s, "LargeReceiveOffload=%u\n", def->large_receive_offload); + +- orig_umask = umask(022); +- 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); + } + + +@@ -387,7 +384,6 @@ 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); + +@@ -454,11 +450,7 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch + 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); +- 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 +@@ -602,7 +594,6 @@ netplan_netdef_write_network_file( + GString* network = NULL; + GString* link = NULL; + GString* s = NULL; +- mode_t orig_umask; + gboolean is_optional = def->optional; + + SET_OPT_OUT_PTR(has_been_written, FALSE); +@@ -827,11 +818,7 @@ netplan_netdef_write_network_file( + g_string_free(link, TRUE); + g_string_free(network, TRUE); + +- /* these do not contain secrets and need to be readable by +- * systemd-networkd - LP: #1736965 */ +- orig_umask = umask(022); +- 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); +@@ -843,7 +830,6 @@ 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 +@@ -877,9 +863,7 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir) + + g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name); + +- orig_umask = umask(022); +- 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 +@@ -981,7 +965,6 @@ 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); + +@@ -1000,9 +983,7 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir) + } else { + g_string_append(s, " -Dnl80211,wext\n"); + } +- orig_umask = umask(022); +- 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 +@@ -1011,7 +992,6 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er + 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) { +@@ -1087,9 +1067,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er + } + + /* use tight permissions as this contains secrets */ +- orig_umask = umask(077); +- 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; + } + +diff --git a/src/networkd.h b/src/networkd.h +index a7092b2c..0214e43b 100644 +--- a/src/networkd.h ++++ b/src/networkd.h +@@ -20,6 +20,8 @@ + #include "netplan.h" + #include + ++#define NETWORKD_GROUP "systemd-network" ++ + NETPLAN_INTERNAL gboolean + netplan_netdef_write_networkd( + const NetplanState* np_state, +diff --git a/src/nm.c b/src/nm.c +index 319a80ba..7770a574 100644 +--- a/src/nm.c ++++ b/src/nm.c +@@ -996,13 +996,13 @@ netplan_state_finish_nm_write( + len = s->len; + g_list_foreach(np_state->netdefs_ordered, nd_append_non_nm_ids, s); + if (s->len > len) +- g_string_free_to_file(s, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL); ++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640); + else + g_string_free(s, TRUE); + + /* write generated udev rules */ + if (udev_rules) +- 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); + return TRUE; + } + +diff --git a/src/openvswitch.c b/src/openvswitch.c +index 7479267d..d8b2fefb 100644 +--- a/src/openvswitch.c ++++ b/src/openvswitch.c +@@ -62,7 +62,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, + g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append(s, cmds->str); + +- g_string_free_to_file(s, rootdir, path, NULL); ++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640); + + safe_mkdir_p_dir(link); + if (symlink(path, link) < 0 && errno != EEXIST) { +diff --git a/src/sriov.c b/src/sriov.c +index 966718f4..1b8af8a6 100644 +--- a/src/sriov.c ++++ b/src/sriov.c +@@ -48,7 +48,7 @@ write_sriov_rebind_systemd_unit(const GString* pfs, const char* rootdir, GError* + g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind %s\n", pfs->str); + +- g_string_free_to_file(s, rootdir, path, NULL); ++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640); + + safe_mkdir_p_dir(link); + if (symlink(path, link) < 0 && errno != EEXIST) { +diff --git a/src/util-internal.h b/src/util-internal.h +index d7875530..4a17219e 100644 +--- a/src/util-internal.h ++++ b/src/util-internal.h +@@ -39,6 +39,9 @@ safe_mkdir_p_dir(const char* file_path); + NETPLAN_INTERNAL void + 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 + unlink_glob(const char* rootdir, const char* _glob); + +diff --git a/src/util.c b/src/util.c +index 841ec120..e6d8c99b 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -19,6 +19,9 @@ + #include + #include + #include ++#include ++#include ++#include + + #include + #include +@@ -81,6 +84,49 @@ void g_string_free_to_file(GString* s, const char* rootdir, const char* path, co + } + } + ++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); ++ 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. + */ +diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py +index 3d201092..9bbc0b1c 100644 +--- a/tests/generator/test_auth.py ++++ b/tests/generator/test_auth.py +@@ -228,7 +228,7 @@ network={ + + 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'))) + +diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py +index 1a4ead23..0130a6eb 100644 +--- a/tests/generator/test_wifis.py ++++ b/tests/generator/test_wifis.py +@@ -136,7 +136,7 @@ network={ + 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'))) + +diff --git a/tests/integration/base.py b/tests/integration/base.py +index b0863854..0606b1e3 100644 +--- a/tests/integration/base.py ++++ b/tests/integration/base.py +@@ -31,6 +31,9 @@ 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'))}) +@@ -321,6 +324,89 @@ class IntegrationTestsBase(unittest.TestCase): + 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 +-- +2.34.1 + diff --git a/meta-networking/recipes-connectivity/netplan/netplan_0.104.bb b/meta-networking/recipes-connectivity/netplan/netplan_0.104.bb index ea944fa9db..c59797c219 100644 --- a/meta-networking/recipes-connectivity/netplan/netplan_0.104.bb +++ b/meta-networking/recipes-connectivity/netplan/netplan_0.104.bb @@ -17,6 +17,7 @@ PV = "0.104" SRC_URI = "git://github.com/CanonicalLtd/netplan.git;branch=main;protocol=https \ file://0001-Makefile-do-not-use-Werror.patch \ file://0001-Makefile-fix-parallel-build-failure.patch \ + file://CVE-2022-4968.patch \ " SRC_URI:append:libc-musl = " file://0001-don-t-fail-if-GLOB_BRACE-is-not-defined.patch"