Merge ~danilogondolfo/netplan/+git/ubuntu:mantic_0_107_1_sru_with_security_fixes into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-mantic

Proposed by Danilo Egea Gondolfo
Status: Merged
Merged at revision: ed684b8a3eb282b9bc7c0f18ad6b2249e7f3ef30
Proposed branch: ~danilogondolfo/netplan/+git/ubuntu:mantic_0_107_1_sru_with_security_fixes
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu-mantic
Diff against target: 2060 lines (+2000/-1)
8 files modified
debian/changelog (+20/-1)
debian/netplan-generator.postinst (+15/-0)
debian/patches/lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch (+82/-0)
debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch (+435/-0)
debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch (+863/-0)
debian/patches/lp2066258/0015-backends-escape-file-paths.patch (+288/-0)
debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch (+292/-0)
debian/patches/series (+5/-0)
Reviewer Review Type Date Requested Status
Lukas Märdian Approve
Ubuntu Core Development Team Pending
Review via email: mp+468523@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) wrote :

LGTM. The new security patches match the noble patches (ignoring patch fuzz).

I've just applied some tiny fixes to the version number (removing ~ppa suffix) and update d/changelog to not drop the previous 0.107-5ubuntu0.3 and 0.107-5ubuntu0.4 updates.

review: Approve
Revision history for this message
Danilo Egea Gondolfo (danilogondolfo) wrote :

I can't believe I left the ~ppa in the changelog again........

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index d28e7c5..ad8fa95 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,4 +1,4 @@
6-netplan.io (0.107.1-3ubuntu0.23.10.1) mantic; urgency=medium
7+netplan.io (0.107.1-3ubuntu0.23.10.1~ppa10) mantic; urgency=medium
8
9 * Backport netplan.io 0.107.1-3 to 23.10 (LP: #2058051):
10 - wifi: add support for WPA3-Enterprise (LP: 2029876) (!402)
11@@ -14,6 +14,25 @@ netplan.io (0.107.1-3ubuntu0.23.10.1) mantic; urgency=medium
12 - doc: create libnetplan apidoc structure (!423)
13 - inc: Start documenting public API (!423)
14 - doc: Update 'Netplan everywhere' for 23.10 (!418)
15+ SECURITY UPDATE: weak permissions on secret files, command injection
16+ - d/p/lp2065738/0014-libnetplan-use-more-restrictive-file-permissions.patch:
17+ Use more restrictive file permissions to prevent unprivileged users to
18+ read sensitive data from back end files (LP: 2065738, 1987842)
19+ - CVE-2022-4968
20+ - d/p/lp2066258/0015-libnetplan-escape-control-characters.patch:
21+ Escape control characters in the parser and double quotes in backend
22+ files.
23+ - d/p/lp2066258/0016-backends-escape-file-paths.patch:
24+ Escape special characters in file paths.
25+ - d/p/lp2066258/0017-backends-escape-semicolons-in-service-units.patch:
26+ Escape isolated semicolons in systemd service units. (LP: 2066258)
27+ - debian/netplan-generator.postinst: Add a postinst maintainer script to
28+ call the generator. It's needed so the file permissions fixes will be
29+ applied automatically.
30+ - d/p/lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch:
31+ Call daemon-reload after generate. This is required due to the NM
32+ integration. NM doesn't have the capability required to set file owners
33+ and now we set the systemd-network group to networkd related files.
34 Bug fixes:
35 - test:ovs: Avoid NetworkManager taking contol, breaking a test
36 - parse: allow COMMON_LINK_HANDLERS for VRFs (!401)
37diff --git a/debian/netplan-generator.postinst b/debian/netplan-generator.postinst
38new file mode 100644
39index 0000000..f805c24
40--- /dev/null
41+++ b/debian/netplan-generator.postinst
42@@ -0,0 +1,15 @@
43+#!/bin/sh
44+
45+set -e
46+
47+# Calling the generator after installation to mitigate CVE-2022-4968
48+# We avoid calling the generator if the system doesn't have networkd files to be fixed (LP: #2071333)
49+if [ "$1" = configure ]; then
50+ FILES=$(find /run/systemd/network/ -type f -regex ".*-netplan.*\.\(network\|netdev\)" 2>/dev/null || true)
51+ if [ -n "${FILES}" ]; then
52+ /usr/libexec/netplan/generate 2>/dev/null || echo "WARNING: Netplan could not re-generate network configuration. Please run 'netplan generate' to see details."
53+ fi
54+fi
55+
56+#DEBHELPER#
57+
58diff --git a/debian/patches/lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch b/debian/patches/lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch
59new file mode 100644
60index 0000000..acafe2b
61--- /dev/null
62+++ b/debian/patches/lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch
63@@ -0,0 +1,82 @@
64+From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
65+Date: Wed, 17 Apr 2024 10:23:16 +0200
66+Subject: cli/generate: call daemon-reload after generate
67+
68+This is required due to the Network Manager integration. NM doesn't have
69+the capability required to change the configuration files owners.
70+Because of that, networkd files that are regenerated when NM is called
71+will have root:root as owner and group. As we changed the default mode
72+to 0640, networkd wouldn't be able to read the files and the
73+configuration would be ignored.
74+
75+Origin: https://github.com/canonical/netplan/commit/1892487a44f06d3af9620a0ca54ed6e32d1ae963
76+Origin: https://github.com/canonical/netplan/commit/223e3d3dd10c44149e2c9b9741900d11ebec6f97
77+Origin: https://github.com/canonical/netplan/commit/ea60510cf741afe73b59470af9f812a36140dae0
78+
79+---
80+ netplan_cli/cli/commands/generate.py | 11 ++++++++++-
81+ netplan_cli/cli/utils.py | 2 +-
82+ tests/cli_legacy.py | 3 +--
83+ tests/test_utils.py | 2 +-
84+ 4 files changed, 13 insertions(+), 5 deletions(-)
85+
86+diff --git a/netplan_cli/cli/commands/generate.py b/netplan_cli/cli/commands/generate.py
87+index f84b171..ba51eb0 100644
88+--- a/netplan_cli/cli/commands/generate.py
89++++ b/netplan_cli/cli/commands/generate.py
90+@@ -81,5 +81,14 @@ class NetplanGenerate(utils.NetplanCommand):
91+ if self.mapping:
92+ argv += ['--mapping', self.mapping]
93+ logging.debug('command generate: running %s', argv)
94++ res = subprocess.call(argv)
95++ # reload systemd, as we might have changed service units, such as
96++ # /run/systemd/system/systemd-networkd-wait-online.service.d/10-netplan.conf
97++ # Skip it if --mapping is used as nothing will be generated
98++ if self.mapping is None:
99++ try:
100++ utils.systemctl_daemon_reload()
101++ except subprocess.CalledProcessError as e:
102++ logging.warning(e)
103+ # FIXME: os.execv(argv[0], argv) would be better but fails coverage
104+- sys.exit(subprocess.call(argv))
105++ sys.exit(res)
106+diff --git a/netplan_cli/cli/utils.py b/netplan_cli/cli/utils.py
107+index f913630..237e006 100644
108+--- a/netplan_cli/cli/utils.py
109++++ b/netplan_cli/cli/utils.py
110+@@ -160,7 +160,7 @@ def systemctl_is_installed(unit_pattern):
111+
112+ def systemctl_daemon_reload():
113+ '''Reload systemd unit files from disk and re-calculate its dependencies'''
114+- subprocess.check_call(['systemctl', 'daemon-reload'])
115++ subprocess.check_call(['systemctl', 'daemon-reload', '--no-ask-password'])
116+
117+
118+ def ip_addr_flush(iface):
119+diff --git a/tests/cli_legacy.py b/tests/cli_legacy.py
120+index 897043e..decde81 100755
121+--- a/tests/cli_legacy.py
122++++ b/tests/cli_legacy.py
123+@@ -98,8 +98,7 @@ class TestGenerate(unittest.TestCase):
124+ enlol: {dhcp4: yes}''')
125+ os.chmod(path_a, mode=0o600)
126+ os.chmod(path_b, mode=0o600)
127+- out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT)
128+- self.assertEqual(out, b'')
129++ subprocess.check_call(exe_cli + ['generate', '--root-dir', self.workdir.name])
130+ self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')),
131+ ['10-netplan-enlol.network'])
132+
133+diff --git a/tests/test_utils.py b/tests/test_utils.py
134+index eefd334..37cf262 100644
135+--- a/tests/test_utils.py
136++++ b/tests/test_utils.py
137+@@ -380,7 +380,7 @@ class TestUtils(unittest.TestCase):
138+ os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env
139+ utils.systemctl_daemon_reload()
140+ self.assertEqual(self.mock_cmd.calls(), [
141+- ['systemctl', 'daemon-reload']
142++ ['systemctl', 'daemon-reload', '--no-ask-password']
143+ ])
144+
145+ def test_ip_addr_flush(self):
146diff --git a/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch b/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
147new file mode 100644
148index 0000000..9ebce53
149--- /dev/null
150+++ b/debian/patches/lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
151@@ -0,0 +1,435 @@
152+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
153+Date: Wed, 22 May 2024 15:44:16 +0100
154+Subject: libnetplan: use more restrictive file permissions
155+
156+A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
157+and accepts the owner, group and file mode as arguments. When these
158+properties can't be set, when the generator is called by a non-root user
159+for example, it will not hard-fail. This function is called by unit
160+tests where we can't set the owner to a privileged account for example.
161+
162+When generating backend files, use more restrictive permissions:
163+
164+networkd related files will be owned by root:systemd-network and have
165+mode 0640.
166+
167+service unit files will be owned by root:root and have mode 0640.
168+udevd files will be owned by root:root with mode 0640.
169+
170+wpa_supplicant and Network Manager files will continue with the existing
171+permissions.
172+
173+Autopkgtests will check if the permissions are set as expected when
174+calling the generator.
175+
176+This fix addresses CVE-2022-4968
177+---
178+ src/networkd.c | 36 ++++--------------
179+ src/networkd.h | 2 +
180+ src/nm.c | 4 +-
181+ src/openvswitch.c | 2 +-
182+ src/sriov.c | 2 +-
183+ src/util-internal.h | 3 ++
184+ src/util.c | 46 +++++++++++++++++++++++
185+ tests/generator/test_auth.py | 2 +-
186+ tests/generator/test_wifis.py | 2 +-
187+ tests/integration/base.py | 85 +++++++++++++++++++++++++++++++++++++++++++
188+ 10 files changed, 149 insertions(+), 35 deletions(-)
189+
190+diff --git a/src/networkd.c b/src/networkd.c
191+index 5554c99..1677381 100644
192+--- a/src/networkd.c
193++++ b/src/networkd.c
194+@@ -222,7 +222,6 @@ static void
195+ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
196+ {
197+ GString* s = NULL;
198+- mode_t orig_umask;
199+
200+ /* Don't write .link files for virtual devices; they use .netdev instead.
201+ * Don't write .link files for MODEM devices, as they aren't supported by networkd.
202+@@ -284,9 +283,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char
203+ g_string_append_printf(s, "LargeReceiveOffload=%s\n",
204+ (def->large_receive_offload ? "true" : "false"));
205+
206+- orig_umask = umask(022);
207+- g_string_free_to_file(s, rootdir, path, ".link");
208+- umask(orig_umask);
209++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
210+ }
211+
212+ static gboolean
213+@@ -304,7 +301,7 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
214+ g_string_append(s, "\n[Service]\nType=oneshot\n");
215+ g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
216+
217+- g_string_free_to_file(s, rootdir, path, NULL);
218++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
219+ safe_mkdir_p_dir(link);
220+ if (symlink(path, link) < 0 && errno != EEXIST) {
221+ // LCOV_EXCL_START
222+@@ -484,7 +481,6 @@ static void
223+ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
224+ {
225+ GString* s = NULL;
226+- mode_t orig_umask;
227+
228+ g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL);
229+
230+@@ -580,11 +576,7 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
231+ default: g_assert_not_reached(); // LCOV_EXCL_LINE
232+ }
233+
234+- /* these do not contain secrets and need to be readable by
235+- * systemd-networkd - LP: #1736965 */
236+- orig_umask = umask(022);
237+- g_string_free_to_file(s, rootdir, path, ".netdev");
238+- umask(orig_umask);
239++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
240+ }
241+
242+ static void
243+@@ -728,7 +720,6 @@ netplan_netdef_write_network_file(
244+ g_autoptr(GString) network = NULL;
245+ g_autoptr(GString) link = NULL;
246+ GString* s = NULL;
247+- mode_t orig_umask;
248+ gboolean is_optional = def->optional;
249+
250+ SET_OPT_OUT_PTR(has_been_written, FALSE);
251+@@ -979,11 +970,7 @@ netplan_netdef_write_network_file(
252+ if (network->len > 0)
253+ g_string_append_printf(s, "\n[Network]\n%s", network->str);
254+
255+- /* these do not contain secrets and need to be readable by
256+- * systemd-networkd - LP: #1736965 */
257+- orig_umask = umask(022);
258+- g_string_free_to_file(s, rootdir, path, ".network");
259+- umask(orig_umask);
260++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
261+ }
262+
263+ SET_OPT_OUT_PTR(has_been_written, TRUE);
264+@@ -995,7 +982,6 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
265+ {
266+ GString* s = NULL;
267+ g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
268+- mode_t orig_umask;
269+
270+ /* do we need to write a .rules file?
271+ * It's only required for reliably setting the name of a physical device
272+@@ -1029,9 +1015,7 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
273+
274+ g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
275+
276+- orig_umask = umask(022);
277+- g_string_free_to_file(s, rootdir, path, NULL);
278+- umask(orig_umask);
279++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
280+ }
281+
282+ static gboolean
283+@@ -1180,7 +1164,6 @@ static void
284+ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
285+ {
286+ g_autofree gchar *stdouth = NULL;
287+- mode_t orig_umask;
288+
289+ stdouth = systemd_escape(def->id);
290+
291+@@ -1199,9 +1182,7 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
292+ } else {
293+ g_string_append(s, " -Dnl80211,wext\n");
294+ }
295+- orig_umask = umask(022);
296+- g_string_free_to_file(s, rootdir, path, NULL);
297+- umask(orig_umask);
298++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
299+ }
300+
301+ static gboolean
302+@@ -1210,7 +1191,6 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
303+ GHashTableIter iter;
304+ GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
305+ g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
306+- mode_t orig_umask;
307+
308+ g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
309+ if (def->type == NETPLAN_DEF_TYPE_WIFI) {
310+@@ -1299,9 +1279,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
311+ }
312+
313+ /* use tight permissions as this contains secrets */
314+- orig_umask = umask(077);
315+- g_string_free_to_file(s, rootdir, path, NULL);
316+- umask(orig_umask);
317++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
318+ return TRUE;
319+ }
320+
321+diff --git a/src/networkd.h b/src/networkd.h
322+index a7092b2..0214e43 100644
323+--- a/src/networkd.h
324++++ b/src/networkd.h
325+@@ -20,6 +20,8 @@
326+ #include "netplan.h"
327+ #include <glib.h>
328+
329++#define NETWORKD_GROUP "systemd-network"
330++
331+ NETPLAN_INTERNAL gboolean
332+ netplan_netdef_write_networkd(
333+ const NetplanState* np_state,
334+diff --git a/src/nm.c b/src/nm.c
335+index 4d6f1fe..d5dad98 100644
336+--- a/src/nm.c
337++++ b/src/nm.c
338+@@ -1149,13 +1149,13 @@ netplan_state_finish_nm_write(
339+
340+ /* write generated NetworkManager drop-in config */
341+ if (nm_conf->len > 0)
342+- g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
343++ _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
344+ else
345+ g_string_free(nm_conf, TRUE);
346+
347+ /* write generated udev rules */
348+ if (udev_rules->len > 0)
349+- g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
350++ _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
351+ else
352+ g_string_free(udev_rules, TRUE);
353+
354+diff --git a/src/openvswitch.c b/src/openvswitch.c
355+index d4af861..276762e 100644
356+--- a/src/openvswitch.c
357++++ b/src/openvswitch.c
358+@@ -62,7 +62,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
359+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
360+ g_string_append(s, cmds->str);
361+
362+- g_string_free_to_file(s, rootdir, path, NULL);
363++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
364+
365+ safe_mkdir_p_dir(link);
366+ if (symlink(path, link) < 0 && errno != EEXIST) {
367+diff --git a/src/sriov.c b/src/sriov.c
368+index f8117f7..c3cd80d 100644
369+--- a/src/sriov.c
370++++ b/src/sriov.c
371+@@ -53,7 +53,7 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
372+ g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
373+ g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind %s\n", interfaces->str);
374+
375+- g_string_free_to_file(s, rootdir, path, NULL);
376++ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
377+ g_string_free(interfaces, TRUE);
378+
379+ safe_mkdir_p_dir(link);
380+diff --git a/src/util-internal.h b/src/util-internal.h
381+index fe41c77..b30745b 100644
382+--- a/src/util-internal.h
383++++ b/src/util-internal.h
384+@@ -40,6 +40,9 @@ safe_mkdir_p_dir(const char* file_path);
385+ NETPLAN_INTERNAL void
386+ g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
387+
388++void
389++_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);
390++
391+ NETPLAN_INTERNAL void
392+ unlink_glob(const char* rootdir, const char* _glob);
393+
394+diff --git a/src/util.c b/src/util.c
395+index cbd5ac2..edecdab 100644
396+--- a/src/util.c
397++++ b/src/util.c
398+@@ -22,6 +22,9 @@
399+ #include <errno.h>
400+ #include <string.h>
401+ #include <sys/mman.h>
402++#include <sys/types.h>
403++#include <pwd.h>
404++#include <grp.h>
405+
406+ #include <glib.h>
407+ #include <glib/gprintf.h>
408+@@ -87,6 +90,49 @@ void g_string_free_to_file(GString* s, const char* rootdir, const char* path, co
409+ }
410+ }
411+
412++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)
413++{
414++ g_autofree char* full_path = NULL;
415++ g_autofree char* path_suffix = NULL;
416++ g_autofree char* contents = g_string_free(s, FALSE);
417++ GError* error = NULL;
418++ struct passwd* pw = NULL;
419++ struct group* gr = NULL;
420++ int ret = 0;
421++
422++ path_suffix = g_strjoin(NULL, path, suffix, NULL);
423++ full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
424++ safe_mkdir_p_dir(full_path);
425++ if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
426++ /* the mkdir() just succeeded, there is no sensible
427++ * method to test this without root privileges, bind mounts, and
428++ * simulating ENOSPC */
429++ // LCOV_EXCL_START
430++ g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
431++ exit(1);
432++ // LCOV_EXCL_STOP
433++ }
434++
435++ /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
436++ * It's OK to fail to set the owners and mode as this code will be called from unit tests.
437++ * The autopkgtests will check if the owner/group and mode are correctly set.
438++ */
439++ pw = getpwnam(owner);
440++ if (!pw) {
441++ g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
442++ }
443++ gr = getgrnam(group);
444++ if (!gr) {
445++ g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
446++ }
447++ if (pw && gr) {
448++ ret = chown(full_path, pw->pw_uid, gr->gr_gid);
449++ if (ret != 0) {
450++ g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
451++ }
452++ }
453++}
454++
455+ /**
456+ * Remove all files matching given glob.
457+ */
458+diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
459+index de23adb..d3d886c 100644
460+--- a/tests/generator/test_auth.py
461++++ b/tests/generator/test_auth.py
462+@@ -226,7 +226,7 @@ network={
463+
464+ with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f:
465+ self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'})
466+- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
467++ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
468+ self.assertTrue(os.path.islink(os.path.join(
469+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service')))
470+
471+diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
472+index b875172..610782a 100644
473+--- a/tests/generator/test_wifis.py
474++++ b/tests/generator/test_wifis.py
475+@@ -140,7 +140,7 @@ network={
476+ self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
477+ with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f:
478+ self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'})
479+- self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
480++ self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
481+ self.assertTrue(os.path.islink(os.path.join(
482+ self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
483+
484+diff --git a/tests/integration/base.py b/tests/integration/base.py
485+index 81e8420..948b1c5 100644
486+--- a/tests/integration/base.py
487++++ b/tests/integration/base.py
488+@@ -32,6 +32,8 @@ import shutil
489+ import gi
490+ import glob
491+ import json
492++import pwd
493++import grp
494+
495+ # make sure we point to libnetplan properly.
496+ os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))})
497+@@ -367,6 +369,89 @@ class IntegrationTestsBase(unittest.TestCase):
498+ if state:
499+ self.wait_output(['ip', 'addr', 'show', iface], state, 30)
500+
501++ # Assert file permissions
502++ self.assert_file_permissions()
503++
504++ def assert_file_permissions(self):
505++ """ Check if the generated files have the expected permissions """
506++
507++ nd_expected_mode = 0o100640
508++ nd_expected_owner = 'root'
509++ nd_expected_group = 'systemd-network'
510++
511++ sd_expected_mode = 0o100640
512++ sd_expected_owner = 'root'
513++ sd_expected_group = 'root'
514++
515++ udev_expected_mode = 0o100640
516++ udev_expected_owner = 'root'
517++ udev_expected_group = 'root'
518++
519++ nm_expected_mode = 0o100600
520++ nm_expected_owner = 'root'
521++ nm_expected_group = 'root'
522++
523++ wpa_expected_mode = 0o100600
524++ wpa_expected_owner = 'root'
525++ wpa_expected_group = 'root'
526++
527++ # Check systemd-networkd files
528++ base_path = '/run/systemd/network'
529++ files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
530++ for file in files:
531++ res = os.stat(file)
532++ user = pwd.getpwuid(res.st_uid)
533++ group = grp.getgrgid(res.st_gid)
534++ self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
535++ self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
536++ self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')
537++
538++ # Check Network Manager files
539++ base_path = '/run/NetworkManager/system-connections'
540++ files = glob.glob(f'{base_path}/*.nmconnection')
541++ for file in files:
542++ res = os.stat(file)
543++ user = pwd.getpwuid(res.st_uid)
544++ group = grp.getgrgid(res.st_gid)
545++ self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
546++ self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
547++ self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')
548++
549++ # Check wpa_supplicant configuration files
550++ base_path = '/run/netplan'
551++ files = glob.glob(f'{base_path}/wpa-*.conf')
552++ for file in files:
553++ res = os.stat(file)
554++ user = pwd.getpwuid(res.st_uid)
555++ group = grp.getgrgid(res.st_gid)
556++ self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
557++ self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
558++ self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')
559++
560++ # Check systemd service unit files
561++ base_path = '/run/systemd/system/'
562++ files = glob.glob(f'{base_path}/netplan-*.service')
563++ files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
564++ for file in files:
565++ res = os.stat(file)
566++ user = pwd.getpwuid(res.st_uid)
567++ group = grp.getgrgid(res.st_gid)
568++ self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
569++ self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
570++ self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')
571++
572++ # Check systemd-udevd files
573++ udev_path = '/run/udev/rules.d'
574++ link_path = '/run/systemd/network'
575++ files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
576++ for file in files:
577++ res = os.stat(file)
578++ user = pwd.getpwuid(res.st_uid)
579++ group = grp.getgrgid(res.st_gid)
580++ self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
581++ self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
582++ self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')
583++
584+ def state(self, iface, state):
585+ '''Tell generate_and_settle() to wait for a specific state'''
586+ return iface + '/' + state
587diff --git a/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch b/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch
588new file mode 100644
589index 0000000..5c7cad8
590--- /dev/null
591+++ b/debian/patches/lp2066258/0014-libnetplan-escape-control-characters.patch
592@@ -0,0 +1,863 @@
593+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
594+Date: Wed, 29 May 2024 14:50:55 +0100
595+Subject: libnetplan: escape control characters
596+
597+Control characters are escaped in the parser using glib's g_strescape.
598+Quotes and backslashes were added to the list of exception.
599+
600+In places where double quotes are not escaped, such as netdef IDs as it
601+is allowed as interface names, they are escaped as needed when
602+generating back end configuration.
603+
604+To support escaping in wpa_supplicant configuration, the syntax for
605+setting the SSID was changed to 'ssid=P"string here"'. With that,
606+escaping is support in a printf-style.
607+---
608+ src/networkd.c | 32 ++++++++++-----
609+ src/nm.c | 21 ++++++----
610+ src/parse.c | 92 +++++++++++++++++++++++++++---------------
611+ src/util-internal.h | 3 ++
612+ src/util.c | 11 +++++
613+ tests/generator/test_auth.py | 20 ++++-----
614+ tests/generator/test_common.py | 42 +++++++++++++++++--
615+ tests/generator/test_wifis.py | 78 ++++++++++++++++++++++++++---------
616+ 8 files changed, 216 insertions(+), 83 deletions(-)
617+
618+diff --git a/src/networkd.c b/src/networkd.c
619+index 1677381..ac54112 100644
620+--- a/src/networkd.c
621++++ b/src/networkd.c
622+@@ -1005,7 +1005,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
623+ g_string_append(s, "SUBSYSTEM==\"net\", ACTION==\"add\", ");
624+
625+ if (def->match.driver) {
626+- g_string_append_printf(s,"DRIVERS==\"%s\", ", def->match.driver);
627++ g_autofree char* driver = _netplan_scrub_string(def->match.driver);
628++ g_string_append_printf(s,"DRIVERS==\"%s\", ", driver);
629+ } else {
630+ g_string_append(s, "DRIVERS==\"?*\", ");
631+ }
632+@@ -1013,7 +1014,8 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
633+ if (def->match.mac)
634+ g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac);
635+
636+- g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
637++ g_autofree char* set_name = _netplan_scrub_string(def->set_name);
638++ g_string_append_printf(s, "NAME=\"%s\"\n", set_name);
639+
640+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
641+ }
642+@@ -1101,10 +1103,12 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
643+ }
644+
645+ if (auth->identity) {
646+- g_string_append_printf(s, " identity=\"%s\"\n", auth->identity);
647++ g_autofree char* identity = _netplan_scrub_string(auth->identity);
648++ g_string_append_printf(s, " identity=\"%s\"\n", identity);
649+ }
650+ if (auth->anonymous_identity) {
651+- g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity);
652++ g_autofree char* anonymous_identity = _netplan_scrub_string(auth->anonymous_identity);
653++ g_string_append_printf(s, " anonymous_identity=\"%s\"\n", anonymous_identity);
654+ }
655+
656+ char* psk = NULL;
657+@@ -1142,19 +1146,23 @@ append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, cons
658+ }
659+ }
660+ if (auth->ca_certificate) {
661+- g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate);
662++ g_autofree char* ca_certificate = _netplan_scrub_string(auth->ca_certificate);
663++ g_string_append_printf(s, " ca_cert=\"%s\"\n", ca_certificate);
664+ }
665+ if (auth->client_certificate) {
666+- g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate);
667++ g_autofree char* client_certificate = _netplan_scrub_string(auth->client_certificate);
668++ g_string_append_printf(s, " client_cert=\"%s\"\n", client_certificate);
669+ }
670+ if (auth->client_key) {
671+- g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key);
672++ g_autofree char* client_key = _netplan_scrub_string(auth->client_key);
673++ g_string_append_printf(s, " private_key=\"%s\"\n", client_key);
674+ }
675+ if (auth->client_key_password) {
676+ g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password);
677+ }
678+ if (auth->phase2_auth) {
679+- g_string_append_printf(s, " phase2=\"auth=%s\"\n", auth->phase2_auth);
680++ g_autofree char* phase2_auth = _netplan_scrub_string(auth->phase2_auth);
681++ g_string_append_printf(s, " phase2=\"auth=%s\"\n", phase2_auth);
682+ }
683+ return TRUE;
684+ }
685+@@ -1203,14 +1211,16 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
686+ }
687+ /* available as of wpa_supplicant version 0.6.7 */
688+ if (def->regulatory_domain) {
689+- g_string_append_printf(s, "country=%s\n", def->regulatory_domain);
690++ g_autofree char* regdom = _netplan_scrub_string(def->regulatory_domain);
691++ g_string_append_printf(s, "country=%s\n", regdom);
692+ }
693+ NetplanWifiAccessPoint* ap;
694+ g_hash_table_iter_init(&iter, def->access_points);
695+ while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) {
696+ gchar* freq_config_str = ap->mode == NETPLAN_WIFI_MODE_ADHOC ? "frequency" : "freq_list";
697++ g_autofree char* ssid = _netplan_scrub_string(ap->ssid);
698+
699+- g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid);
700++ g_string_append_printf(s, "network={\n ssid=P\"%s\"\n", ssid);
701+ if (ap->bssid) {
702+ g_string_append_printf(s, " bssid=%s\n", ap->bssid);
703+ }
704+@@ -1257,7 +1267,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
705+
706+ /* wifi auth trumps netdef auth */
707+ if (ap->has_auth) {
708+- if (!append_wpa_auth_conf(s, &ap->auth, ap->ssid, error)) {
709++ if (!append_wpa_auth_conf(s, &ap->auth, ssid, error)) {
710+ g_string_free(s, TRUE);
711+ return FALSE;
712+ }
713+diff --git a/src/nm.c b/src/nm.c
714+index d5dad98..a605c38 100644
715+--- a/src/nm.c
716++++ b/src/nm.c
717+@@ -1081,28 +1081,30 @@ netplan_state_finish_nm_write(
718+ GString *tmp = NULL;
719+ guint unmanaged = nd->backend == NETPLAN_BACKEND_NM ? 0 : 1;
720+
721++ g_autofree char* netdef_id = _netplan_scrub_string(nd->id);
722+ /* Special case: manage or ignore any device of given type on empty "match: {}" stanza */
723+ if (nd->has_match && !nd->match.driver && !nd->match.mac && !nd->match.original_name) {
724+ nm_type = type_str(nd);
725+ g_assert(nm_type);
726+ g_string_append_printf(nm_conf, "[device-netplan.%s.%s]\nmatch-device=type:%s\n"
727+ "managed=%d\n\n", netplan_def_type_name(nd->type),
728+- nd->id, nm_type, !unmanaged);
729++ netdef_id, nm_type, !unmanaged);
730+ }
731+ /* Normal case: manage or ignore devices by specific udev rules */
732+ else {
733+ const gchar *prefix = "SUBSYSTEM==\"net\", ACTION==\"add|change|move\",";
734+ const gchar *suffix = nd->backend == NETPLAN_BACKEND_NM ? " ENV{NM_UNMANAGED}=\"0\"\n" : " ENV{NM_UNMANAGED}=\"1\"\n";
735+ g_string_append_printf(udev_rules, "# netplan: network.%s.%s (on NetworkManager %s)\n",
736+- netplan_def_type_name(nd->type), nd->id,
737++ netplan_def_type_name(nd->type), netdef_id,
738+ unmanaged ? "deny-list" : "allow-list");
739+ /* Match by explicit interface name, if possible */
740+ if (nd->set_name) {
741+ // simple case: explicit new interface name
742+- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->set_name, suffix);
743++ g_autofree char* set_name = _netplan_scrub_string(nd->set_name);
744++ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, set_name, suffix);
745+ } else if (!nd->has_match) {
746+ // simple case: explicit netplan ID is interface name
747+- g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, nd->id, suffix);
748++ g_string_append_printf(udev_rules, "%s ENV{ID_NET_NAME}==\"%s\",%s", prefix, netdef_id, suffix);
749+ }
750+ /* Also, match by explicit (new) MAC, if available */
751+ if (nd->set_mac) {
752+@@ -1118,9 +1120,10 @@ netplan_state_finish_nm_write(
753+ // match on original name glob
754+ // TODO: maybe support matching on multiple name globs in the future (like drivers)
755+ g_string_append(udev_rules, prefix);
756+- if (nd->match.original_name)
757+- g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", nd->match.original_name);
758+-
759++ if (nd->match.original_name) {
760++ g_autofree char* original_name = _netplan_scrub_string(nd->match.original_name);
761++ g_string_append_printf(udev_rules, " ENV{ID_NET_NAME}==\"%s\",", original_name);
762++ }
763+ // match on (explicit) MAC address. Yes this would be unique on its own, but we
764+ // keep it within the "full match" to make the logic more comprehensible.
765+ if (nd->match.mac) {
766+@@ -1138,7 +1141,9 @@ netplan_state_finish_nm_write(
767+ g_strfreev(split);
768+ } else
769+ drivers = g_strdup(nd->match.driver);
770+- g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", drivers);
771++
772++ g_autofree char* escaped_drivers = _netplan_scrub_string(drivers);
773++ g_string_append_printf(udev_rules, " ENV{ID_NET_DRIVER}==\"%s\",", escaped_drivers);
774+ g_free(drivers);
775+ }
776+ g_string_append(udev_rules, suffix);
777+diff --git a/src/parse.c b/src/parse.c
778+index d70b564..e37648d 100644
779+--- a/src/parse.c
780++++ b/src/parse.c
781+@@ -59,6 +59,15 @@ extern NetplanState global_state;
782+
783+ NetplanParser global_parser = {0};
784+
785++/*
786++ * We use g_strescape to escape control characters from the input.
787++ * Besides control characters, g_strescape will also escape double quotes and backslashes.
788++ * Quotes are escaped at configuration generation time as needed, as they might be part of passwords for example.
789++ * Escaping backslashes in the parser affects "netplan set" as it will always escape \'s from
790++ * the input and update YAMLs with all the \'s escaped again.
791++*/
792++static char* STRESCAPE_EXCEPTIONS = "\"\\";
793++
794+ static gboolean
795+ insert_kv_into_hash(void *key, void *value, void *hash);
796+
797+@@ -365,7 +374,7 @@ handle_generic_str(NetplanParser* npp, yaml_node_t* node, void* entryptr, const
798+ guint offset = GPOINTER_TO_UINT(data);
799+ char** dest = (char**) ((void*) entryptr + offset);
800+ g_free(*dest);
801+- *dest = g_strdup(scalar(node));
802++ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
803+ mark_data_as_dirty(npp, dest);
804+ return TRUE;
805+ }
806+@@ -480,22 +489,25 @@ handle_generic_map(NetplanParser *npp, yaml_node_t* node, const char* key_prefix
807+ assert_type(npp, key, YAML_SCALAR_NODE);
808+ assert_type(npp, value, YAML_SCALAR_NODE);
809+
810++ g_autofree char* escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
811++ g_autofree char* escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
812++
813+ if (key_prefix && npp->null_fields) {
814+ g_autofree char* full_key = NULL;
815+- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
816++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
817+ if (g_hash_table_contains(npp->null_fields, full_key))
818+ continue;
819+ }
820+
821+ char* stored_value = NULL;
822+- if (g_hash_table_lookup_extended(*map, scalar(key), NULL, (void**)&stored_value)) {
823++ if (g_hash_table_lookup_extended(*map, escaped_key, NULL, (void**)&stored_value)) {
824+ /* We can safely skip this if it is the exact key/value match
825+ * (probably caused by multi-pass processing) */
826+- if (g_strcmp0(stored_value, scalar(value)) == 0)
827++ if (g_strcmp0(stored_value, escaped_value) == 0)
828+ continue;
829+- return yaml_error(npp, node, error, "duplicate map entry '%s'", scalar(key));
830++ return yaml_error(npp, node, error, "duplicate map entry '%s'", escaped_key);
831+ } else
832+- g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value)));
833++ g_hash_table_insert(*map, g_strdup(escaped_key), g_strdup(escaped_value));
834+ }
835+ mark_data_as_dirty(npp, map);
836+
837+@@ -518,20 +530,26 @@ handle_generic_datalist(NetplanParser *npp, yaml_node_t* node, const char* key_p
838+ for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
839+ yaml_node_t* key, *value;
840+ g_autofree char* full_key = NULL;
841++ g_autofree char* escaped_key = NULL;
842++ g_autofree char* escaped_value = NULL;
843+
844+ key = yaml_document_get_node(&npp->doc, entry->key);
845+ value = yaml_document_get_node(&npp->doc, entry->value);
846+
847+ assert_type(npp, key, YAML_SCALAR_NODE);
848+ assert_type(npp, value, YAML_SCALAR_NODE);
849++
850++ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
851++ escaped_value = g_strescape(scalar(value), STRESCAPE_EXCEPTIONS);
852++
853+ if (npp->null_fields && key_prefix) {
854+- full_key = g_strdup_printf("%s\t%s", key_prefix, scalar(key));
855++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
856+ if (g_hash_table_contains(npp->null_fields, full_key))
857+ continue;
858+ }
859+
860+- g_datalist_id_set_data_full(list, g_quark_from_string(scalar(key)),
861+- g_strdup(scalar(value)), g_free);
862++ g_datalist_id_set_data_full(list, g_quark_from_string(escaped_key),
863++ g_strdup(escaped_value), g_free);
864+ }
865+ mark_data_as_dirty(npp, list);
866+
867+@@ -864,13 +882,14 @@ handle_match_driver(NetplanParser* npp, yaml_node_t* node, __unused const char*
868+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
869+ elem = yaml_document_get_node(&npp->doc, *iter);
870+ assert_type(npp, elem, YAML_SCALAR_NODE);
871+- if (g_strrstr(scalar(elem), " "))
872++ g_autofree char* escaped_elem = g_strescape(scalar(elem), STRESCAPE_EXCEPTIONS);
873++ if (g_strrstr(escaped_elem, " "))
874+ return yaml_error(npp, node, error, "A 'driver' glob cannot contain whitespace");
875+
876+ if (!sequence)
877+- sequence = g_string_new(scalar(elem));
878++ sequence = g_string_new(escaped_elem);
879+ else
880+- g_string_append_printf(sequence, "\t%s", scalar(elem)); /* tab separated */
881++ g_string_append_printf(sequence, "\t%s", escaped_elem); /* tab separated */
882+ }
883+
884+ if (!sequence)
885+@@ -902,7 +921,7 @@ handle_auth_str(NetplanParser* npp, yaml_node_t* node, const void* data, __unuse
886+ guint offset = GPOINTER_TO_UINT(data);
887+ char** dest = (char**) ((void*) npp->current.auth + offset);
888+ g_free(*dest);
889+- *dest = g_strdup(scalar(node));
890++ *dest = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
891+ mark_data_as_dirty(npp, dest);
892+ return TRUE;
893+ }
894+@@ -1050,7 +1069,7 @@ handle_access_point_password(NetplanParser* npp, yaml_node_t* node, __unused con
895+
896+ access_point->auth.pmf_mode = NETPLAN_AUTH_PMF_MODE_OPTIONAL;
897+ g_free(access_point->auth.psk);
898+- access_point->auth.psk = g_strdup(scalar(node));
899++ access_point->auth.psk = g_strescape(scalar(node), STRESCAPE_EXCEPTIONS);
900+ return TRUE;
901+ }
902+
903+@@ -1434,6 +1453,7 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
904+ for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) {
905+ NetplanWifiAccessPoint *access_point = NULL;
906+ g_autofree char* full_key = NULL;
907++ g_autofree char* escaped_key = NULL;
908+ yaml_node_t* key, *value;
909+ const gchar* ssid;
910+
911+@@ -1442,13 +1462,15 @@ handle_wifi_access_points(NetplanParser* npp, yaml_node_t* node, const char* key
912+ value = yaml_document_get_node(&npp->doc, entry->value);
913+ assert_type(npp, value, YAML_MAPPING_NODE);
914+
915++ escaped_key = g_strescape(scalar(key), STRESCAPE_EXCEPTIONS);
916++
917+ if (key_prefix && npp->null_fields) {
918+- full_key = g_strdup_printf("%s\t%s", key_prefix, key->data.scalar.value);
919++ full_key = g_strdup_printf("%s\t%s", key_prefix, escaped_key);
920+ if (g_hash_table_contains(npp->null_fields, full_key))
921+ continue;
922+ }
923+
924+- ssid = scalar(key);
925++ ssid = escaped_key;
926+
927+ /*
928+ * Delete the access-point if it already exists in the netdef and let the new
929+@@ -1606,15 +1628,16 @@ handle_nameservers_search(NetplanParser* npp, yaml_node_t* node, __unused const
930+ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
931+ yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i);
932+ assert_type(npp, entry, YAML_SCALAR_NODE);
933++ g_autofree char* escaped_entry = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
934+
935+ if (!npp->current.netdef->search_domains)
936+ npp->current.netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*));
937+
938+- if (!is_string_in_array(npp->current.netdef->search_domains, scalar(entry))) {
939+- char* s = g_strdup(scalar(entry));
940++ if (!is_string_in_array(npp->current.netdef->search_domains, escaped_entry)) {
941++ char* s = g_strdup(escaped_entry);
942+ g_array_append_val(npp->current.netdef->search_domains, s);
943+ } else {
944+- g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, scalar(entry));
945++ g_debug("%s: Search domain '%s' has already been added", npp->current.netdef->id, escaped_entry);
946+ }
947+ }
948+ mark_data_as_dirty(npp, &npp->current.netdef->search_domains);
949+@@ -2720,19 +2743,19 @@ handle_ovs_bridge_controller_addresses(NetplanParser* npp, yaml_node_t* node, __
950+
951+ /* Format: [p]unix:file */
952+ if (is_unix && vec[1] != NULL && vec[2] == NULL) {
953+- char* s = g_strdup(scalar(entry));
954++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
955+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
956+ g_strfreev(vec);
957+ continue;
958+ /* Format tcp:host[:port] or ssl:host[:port] */
959+ } else if (is_host && validate_ovs_target(TRUE, vec[1])) {
960+- char* s = g_strdup(scalar(entry));
961++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
962+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
963+ g_strfreev(vec);
964+ continue;
965+ /* Format ptcp:[port][:host] or pssl:[port][:host] */
966+ } else if (is_port && validate_ovs_target(FALSE, vec[1])) {
967+- char* s = g_strdup(scalar(entry));
968++ char* s = g_strescape(scalar(entry), STRESCAPE_EXCEPTIONS);
969+ g_array_append_val(npp->current.netdef->ovs_settings.controller.addresses, s);
970+ g_strfreev(vec);
971+ continue;
972+@@ -3044,6 +3067,8 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
973+ NetplanNetDefinition *component2 = NULL;
974+
975+ for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) {
976++ g_autofree char* escaped_port = NULL;
977++ g_autofree char* escaped_peer = NULL;
978+ pair = yaml_document_get_node(&npp->doc, *iter);
979+ assert_type(npp, pair, YAML_SEQUENCE_NODE);
980+
981+@@ -3061,11 +3086,14 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
982+ if (!g_strcmp0(scalar(port), scalar(peer)))
983+ return yaml_error(npp, peer, error, "Open vSwitch patch ports must be of different name");
984+
985++ escaped_port = g_strescape(scalar(port), STRESCAPE_EXCEPTIONS);
986++ escaped_peer = g_strescape(scalar(peer), STRESCAPE_EXCEPTIONS);
987++
988+ /* Create port 1 netdef */
989+- component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(port)) : NULL;
990++ component1 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_port) : NULL;
991+ if (!component1) {
992+- component1 = netplan_netdef_new(npp, scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
993+- if (g_hash_table_remove(npp->missing_id, scalar(port)))
994++ component1 = netplan_netdef_new(npp, escaped_port, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
995++ if (g_hash_table_remove(npp->missing_id, escaped_port))
996+ npp->missing_ids_found++;
997+ }
998+
999+@@ -3076,15 +3104,15 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
1000+ component1->filepath = g_strdup(npp->current.filepath);
1001+ }
1002+
1003+- if (component1->peer && g_strcmp0(component1->peer, scalar(peer)))
1004++ if (component1->peer && g_strcmp0(component1->peer, escaped_peer))
1005+ return yaml_error(npp, port, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
1006+ component1->id, component1->peer);
1007+
1008+ /* Create port 2 (peer) netdef */
1009+- component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, scalar(peer)) : NULL;
1010++ component2 = npp->parsed_defs ? g_hash_table_lookup(npp->parsed_defs, escaped_peer) : NULL;
1011+ if (!component2) {
1012+- component2 = netplan_netdef_new(npp, scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
1013+- if (g_hash_table_remove(npp->missing_id, scalar(peer)))
1014++ component2 = netplan_netdef_new(npp, escaped_peer, NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS);
1015++ if (g_hash_table_remove(npp->missing_id, escaped_peer))
1016+ npp->missing_ids_found++;
1017+ }
1018+
1019+@@ -3095,16 +3123,16 @@ handle_network_ovs_settings_global_ports(NetplanParser* npp, yaml_node_t* node,
1020+ component2->filepath = g_strdup(npp->current.filepath);
1021+ }
1022+
1023+- if (component2->peer && g_strcmp0(component2->peer, scalar(port)))
1024++ if (component2->peer && g_strcmp0(component2->peer, escaped_port))
1025+ return yaml_error(npp, peer, error, "Open vSwitch port '%s' is already assigned to peer '%s'",
1026+ component2->id, component2->peer);
1027+
1028+ if (!component1->peer) {
1029+- component1->peer = g_strdup(scalar(peer));
1030++ component1->peer = g_strdup(escaped_peer);
1031+ component1->peer_link = component2;
1032+ }
1033+ if (!component2->peer) {
1034+- component2->peer = g_strdup(scalar(port));
1035++ component2->peer = g_strdup(escaped_port);
1036+ component2->peer_link = component1;
1037+ }
1038+ }
1039+diff --git a/src/util-internal.h b/src/util-internal.h
1040+index b30745b..f6f36fc 100644
1041+--- a/src/util-internal.h
1042++++ b/src/util-internal.h
1043+@@ -182,3 +182,6 @@ _netplan_netdef_pertype_iter_next(struct netdef_pertype_iter* it);
1044+
1045+ NETPLAN_INTERNAL void
1046+ _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
1047++
1048++gchar*
1049++_netplan_scrub_string(const char* content);
1050+diff --git a/src/util.c b/src/util.c
1051+index edecdab..d8d3a57 100644
1052+--- a/src/util.c
1053++++ b/src/util.c
1054+@@ -1223,3 +1223,14 @@ _is_auth_key_management_psk(const NetplanAuthenticationSettings* auth)
1055+ return ( auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK
1056+ || auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_SAE);
1057+ }
1058++
1059++gchar*
1060++_netplan_scrub_string(const char* content)
1061++{
1062++ GString* s = g_string_new(content);
1063++
1064++ // Escape double quotes
1065++ g_string_replace(s, "\"", "\\\"", 0);
1066++
1067++ return g_string_free(s, FALSE);
1068++}
1069+diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py
1070+index d3d886c..bf0108a 100644
1071+--- a/tests/generator/test_auth.py
1072++++ b/tests/generator/test_auth.py
1073+@@ -92,21 +92,21 @@ class TestNetworkd(TestBase):
1074+ self.assertIn('ctrl_interface=/run/wpa_supplicant', new_config)
1075+ self.assertIn('''
1076+ network={
1077+- ssid="peer2peer"
1078++ ssid=P"peer2peer"
1079+ mode=1
1080+ key_mgmt=NONE
1081+ }
1082+ ''', new_config)
1083+ self.assertIn('''
1084+ network={
1085+- ssid="Luke's Home"
1086++ ssid=P"Luke's Home"
1087+ key_mgmt=WPA-PSK
1088+ psk="4lsos3kr1t"
1089+ }
1090+ ''', new_config)
1091+ self.assertIn('''
1092+ network={
1093+- ssid="BobsHome"
1094++ ssid=P"BobsHome"
1095+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1096+ ieee80211w=1
1097+ psk=e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e
1098+@@ -114,14 +114,14 @@ network={
1099+ ''', new_config)
1100+ self.assertIn('''
1101+ network={
1102+- ssid="BillsHome"
1103++ ssid=P"BillsHome"
1104+ key_mgmt=WPA-PSK
1105+ psk=db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04
1106+ }
1107+ ''', new_config)
1108+ self.assertIn('''
1109+ network={
1110+- ssid="workplace2"
1111++ ssid=P"workplace2"
1112+ key_mgmt=WPA-EAP
1113+ eap=PEAP
1114+ identity="joe@internal.example.com"
1115+@@ -131,7 +131,7 @@ network={
1116+ ''', new_config)
1117+ self.assertIn('''
1118+ network={
1119+- ssid="workplace"
1120++ ssid=P"workplace"
1121+ key_mgmt=WPA-EAP
1122+ eap=TTLS
1123+ identity="joe@internal.example.com"
1124+@@ -141,7 +141,7 @@ network={
1125+ ''', new_config)
1126+ self.assertIn('''
1127+ network={
1128+- ssid="workplacehashed"
1129++ ssid=P"workplacehashed"
1130+ key_mgmt=WPA-EAP
1131+ eap=TTLS
1132+ identity="joe@internal.example.com"
1133+@@ -151,7 +151,7 @@ network={
1134+ ''', new_config)
1135+ self.assertIn('''
1136+ network={
1137+- ssid="customernet"
1138++ ssid=P"customernet"
1139+ key_mgmt=WPA-EAP
1140+ eap=TLS
1141+ identity="cert-joe@cust.example.com"
1142+@@ -164,13 +164,13 @@ network={
1143+ ''', new_config)
1144+ self.assertIn('''
1145+ network={
1146+- ssid="opennet"
1147++ ssid=P"opennet"
1148+ key_mgmt=NONE
1149+ }
1150+ ''', new_config)
1151+ self.assertIn('''
1152+ network={
1153+- ssid="Joe's Home"
1154++ ssid=P"Joe's Home"
1155+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1156+ ieee80211w=1
1157+ psk="s0s3kr1t"
1158+diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
1159+index 785f6f4..3aab3f7 100644
1160+--- a/tests/generator/test_common.py
1161++++ b/tests/generator/test_common.py
1162+@@ -19,7 +19,7 @@
1163+ import os
1164+ import textwrap
1165+
1166+-from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
1167++from .base import UDEV_NO_MAC_RULE, TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED
1168+
1169+
1170+ class TestNetworkd(TestBase):
1171+@@ -815,6 +815,18 @@ RouteMetric=100
1172+ UseMTU=true
1173+ '''})
1174+
1175++ def test_nd_udev_rules_escaped(self):
1176++ self.generate('''network:
1177++ version: 2
1178++ renderer: NetworkManager
1179++ ethernets:
1180++ def1:
1181++ match:
1182++ driver: "abc\\"xyz\\n0\\n\\n1"
1183++ set-name: "eth\\"\\n\\nxyz\\n0"''', skip_generated_yaml_validation=True)
1184++
1185++ self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
1186++
1187+
1188+ class TestNetworkManager(TestBase):
1189+
1190+@@ -1320,6 +1332,28 @@ dns=8.8.8.8;
1191+ method=ignore
1192+ '''})
1193+
1194++ def test_nm_udev_rules_escaped(self):
1195++ self.generate('''network:
1196++ version: 2
1197++ renderer: networkd
1198++ ethernets:
1199++ eth0:
1200++ match:
1201++ name: "eth\\"0"
1202++ dhcp4: true''')
1203++ self.assert_nm_udev(NM_UNMANAGED % 'eth\\"0')
1204++
1205++ self.generate('''network:
1206++ version: 2
1207++ renderer: networkd
1208++ ethernets:
1209++ eth0:
1210++ match:
1211++ name: "eth0"
1212++ set-name: "eth\\n0"
1213++ dhcp4: true''', skip_generated_yaml_validation=True)
1214++ self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
1215++
1216+
1217+ class TestForwardDeclaration(TestBase):
1218+
1219+@@ -1782,13 +1816,13 @@ LinkLocalAddressing=ipv6
1220+ self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
1221+
1222+ network={
1223+- ssid="mynewwifi"
1224++ ssid=P"mynewwifi"
1225+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1226+ ieee80211w=1
1227+ psk="aaaaaaaa"
1228+ }
1229+ network={
1230+- ssid="mywifi"
1231++ ssid=P"mywifi"
1232+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1233+ ieee80211w=1
1234+ psk="aaaaaaaa"
1235+@@ -1819,7 +1853,7 @@ network={
1236+ self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant
1237+
1238+ network={
1239+- ssid="mywifi"
1240++ ssid=P"mywifi"
1241+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1242+ ieee80211w=1
1243+ psk="bbbbbbbb"
1244+diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py
1245+index 610782a..9f7359b 100644
1246+--- a/tests/generator/test_wifis.py
1247++++ b/tests/generator/test_wifis.py
1248+@@ -65,7 +65,7 @@ class TestNetworkd(TestBase):
1249+ with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f:
1250+ new_config = f.read()
1251+
1252+- network = 'ssid="{}"\n freq_list='.format('band-no-channel2')
1253++ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel2')
1254+ freqs_5GHz = [5610, 5310, 5620, 5320, 5630, 5640, 5340, 5035, 5040, 5045, 5055, 5060, 5660, 5680, 5670, 5080, 5690,
1255+ 5700, 5710, 5720, 5825, 5745, 5755, 5805, 5765, 5160, 5775, 5170, 5480, 5180, 5795, 5190, 5500, 5200,
1256+ 5510, 5210, 5520, 5220, 5530, 5230, 5540, 5240, 5550, 5250, 5560, 5260, 5570, 5270, 5580, 5280, 5590,
1257+@@ -76,7 +76,7 @@ class TestNetworkd(TestBase):
1258+ for freq in freqs_5GHz:
1259+ self.assertRegex(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq))
1260+
1261+- network = 'ssid="{}"\n freq_list='.format('band-no-channel')
1262++ network = 'ssid=P"{}"\n freq_list='.format('band-no-channel')
1263+ freqs_24GHz = [2412, 2417, 2422, 2427, 2432, 2442, 2447, 2437, 2452, 2457, 2462, 2467, 2472, 2484]
1264+ freqs = new_config.split(network)
1265+ freqs = freqs[1].split('\n')[0]
1266+@@ -86,20 +86,20 @@ class TestNetworkd(TestBase):
1267+
1268+ self.assertIn('''
1269+ network={
1270+- ssid="channel-no-band"
1271++ ssid=P"channel-no-band"
1272+ key_mgmt=NONE
1273+ }
1274+ ''', new_config)
1275+ self.assertIn('''
1276+ network={
1277+- ssid="peer2peer"
1278++ ssid=P"peer2peer"
1279+ mode=1
1280+ key_mgmt=NONE
1281+ }
1282+ ''', new_config)
1283+ self.assertIn('''
1284+ network={
1285+- ssid="hidden-y"
1286++ ssid=P"hidden-y"
1287+ scan_ssid=1
1288+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1289+ ieee80211w=1
1290+@@ -108,7 +108,7 @@ network={
1291+ ''', new_config)
1292+ self.assertIn('''
1293+ network={
1294+- ssid="hidden-n"
1295++ ssid=P"hidden-n"
1296+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1297+ ieee80211w=1
1298+ psk="5ecur1ty"
1299+@@ -116,7 +116,7 @@ network={
1300+ ''', new_config)
1301+ self.assertIn('''
1302+ network={
1303+- ssid="workplace"
1304++ ssid=P"workplace"
1305+ bssid=de:ad:be:ef:ca:fe
1306+ freq_list=5500
1307+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1308+@@ -126,7 +126,7 @@ network={
1309+ ''', new_config)
1310+ self.assertIn('''
1311+ network={
1312+- ssid="Joe's Home"
1313++ ssid=P"Joe's Home"
1314+ bssid=00:11:22:33:44:55
1315+ freq_list=2462
1316+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1317+@@ -303,7 +303,7 @@ LinkLocalAddressing=ipv6
1318+ self.assertIn('''
1319+ wowlan_triggers=any disconnect magic_pkt gtk_rekey_failure eap_identity_req four_way_handshake rfkill_release
1320+ network={
1321+- ssid="homenet"
1322++ ssid=P"homenet"
1323+ key_mgmt=NONE
1324+ }
1325+ ''', new_config)
1326+@@ -336,7 +336,7 @@ LinkLocalAddressing=ipv6
1327+ new_config = f.read()
1328+ self.assertIn('''
1329+ network={
1330+- ssid="homenet"
1331++ ssid=P"homenet"
1332+ key_mgmt=NONE
1333+ }
1334+ ''', new_config)
1335+@@ -360,7 +360,7 @@ network={
1336+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1337+
1338+ network={
1339+- ssid="homenet"
1340++ ssid=P"homenet"
1341+ key_mgmt=SAE
1342+ ieee80211w=2
1343+ psk="********"
1344+@@ -387,7 +387,7 @@ network={
1345+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1346+
1347+ network={
1348+- ssid="homenet"
1349++ ssid=P"homenet"
1350+ key_mgmt=WPA-EAP WPA-EAP-SHA256
1351+ eap=TLS
1352+ ieee80211w=1
1353+@@ -420,7 +420,7 @@ network={
1354+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1355+
1356+ network={
1357+- ssid="homenet"
1358++ ssid=P"homenet"
1359+ key_mgmt=WPA-EAP-SUITE-B-192
1360+ eap=TLS
1361+ ieee80211w=2
1362+@@ -449,7 +449,7 @@ network={
1363+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1364+
1365+ network={
1366+- ssid="homenet"
1367++ ssid=P"homenet"
1368+ key_mgmt=IEEE8021X
1369+ eap=LEAP
1370+ identity="some-id"
1371+@@ -473,7 +473,7 @@ network={
1372+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1373+
1374+ network={
1375+- ssid="homenet"
1376++ ssid=P"homenet"
1377+ key_mgmt=IEEE8021X
1378+ eap=PWD
1379+ identity="some-id"
1380+@@ -498,7 +498,7 @@ network={
1381+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1382+
1383+ network={
1384+- ssid="homenet"
1385++ ssid=P"homenet"
1386+ key_mgmt=WPA-EAP
1387+ eap=LEAP
1388+ ieee80211w=1
1389+@@ -508,6 +508,48 @@ network={
1390+ }
1391+ """)
1392+
1393++ def test_escaping_special_characters(self):
1394++ self.generate('''network:
1395++ version: 2
1396++ wifis:
1397++ wl0:
1398++ regulatory-domain: "abc\\n\\n321\\n\\"123"
1399++ access-points:
1400++ "abc\\n\\n123\\"x\\ry\\bz":
1401++ password: "abc\\n\\n\\n\\"123"
1402++ auth:
1403++ key-management: eap
1404++ method: leap
1405++ anonymous-identity: "abc\\n\\n321\\n\\"123"
1406++ identity: "abc\\n\\n321\\n\\"123"
1407++ password: "abc\\n\\n\\n\\"123"
1408++ ca-certificate: "abc\\n\\n321\\n\\"123"
1409++ client-certificate: "abc\\n\\n321\\n\\"123"
1410++ client-key: "abc\\n\\n321\\n\\"123"
1411++ client-key-password: "abc\\n\\n321\\n\\"123"
1412++ phase2-auth: "abc\\n\\n321\\n\\"123"
1413++ ''', skip_generated_yaml_validation=True)
1414++
1415++ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1416++
1417++country=abc\\n\\n321\\n\\"123
1418++network={
1419++ ssid=P"abc\\n\\n123\\\"x\\ry\\bz"
1420++ key_mgmt=WPA-EAP
1421++ eap=LEAP
1422++ ieee80211w=1
1423++ identity="abc\\n\\n321\\n\\\"123"
1424++ anonymous_identity="abc\\n\\n321\\n\\\"123"
1425++ psk="abc\\n\\n\\n\"123"
1426++ password="abc\\n\\n\\n\"123"
1427++ ca_cert="abc\\n\\n321\\n\\\"123"
1428++ client_cert="abc\\n\\n321\\n\\\"123"
1429++ private_key="abc\\n\\n321\\n\\\"123"
1430++ private_key_passwd="abc\\n\\n321\\n\"123"
1431++ phase2="auth=abc\\n\\n321\\n\\\"123"
1432++}
1433++""")
1434++
1435+
1436+ class TestNetworkManager(TestBase):
1437+
1438+@@ -790,7 +832,7 @@ mode=adhoc
1439+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1440+
1441+ network={
1442+- ssid="homenet"
1443++ ssid=P"homenet"
1444+ frequency=2442
1445+ mode=1
1446+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1447+@@ -814,7 +856,7 @@ network={
1448+ self.assert_wpa_supplicant("wl0", """ctrl_interface=/run/wpa_supplicant
1449+
1450+ network={
1451+- ssid="homenet"
1452++ ssid=P"homenet"
1453+ frequency=5035
1454+ mode=1
1455+ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1456diff --git a/debian/patches/lp2066258/0015-backends-escape-file-paths.patch b/debian/patches/lp2066258/0015-backends-escape-file-paths.patch
1457new file mode 100644
1458index 0000000..6898b75
1459--- /dev/null
1460+++ b/debian/patches/lp2066258/0015-backends-escape-file-paths.patch
1461@@ -0,0 +1,288 @@
1462+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
1463+Date: Thu, 23 May 2024 15:54:43 +0100
1464+Subject: backends: escape file paths
1465+
1466+Escape strings used to build paths with g_uri_escape_string().
1467+systemd_escape() could also be used but it has the downside of calling
1468+an external program and, by default, it escapes dashes (which are
1469+present in files generated from Network Manager for example).
1470+---
1471+ src/networkd.c | 13 +++++----
1472+ src/nm.c | 5 ++--
1473+ src/openvswitch.c | 19 ++++++------
1474+ src/util.c | 7 +++--
1475+ tests/generator/test_common.py | 66 ++++++++++++++++++++++++++++++++++++++++++
1476+ tests/generator/test_ovs.py | 24 +++++++++++++++
1477+ 6 files changed, 115 insertions(+), 19 deletions(-)
1478+
1479+diff --git a/src/networkd.c b/src/networkd.c
1480+index ac54112..e66ab4c 100644
1481+--- a/src/networkd.c
1482++++ b/src/networkd.c
1483+@@ -981,7 +981,8 @@ static void
1484+ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
1485+ {
1486+ GString* s = NULL;
1487+- g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
1488++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1489++ g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", escaped_netdef_id, ".rules", NULL);
1490+
1491+ /* do we need to write a .rules file?
1492+ * It's only required for reliably setting the name of a physical device
1493+@@ -1198,7 +1199,8 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
1494+ {
1495+ GHashTableIter iter;
1496+ GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
1497+- g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
1498++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1499++ g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", escaped_netdef_id, ".conf", NULL);
1500+
1501+ g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
1502+ if (def->type == NETPLAN_DEF_TYPE_WIFI) {
1503+@@ -1310,7 +1312,8 @@ netplan_netdef_write_networkd(
1504+ GError** error)
1505+ {
1506+ /* TODO: make use of netplan_netdef_get_output_filename() */
1507+- g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
1508++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1509++ g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
1510+ SET_OPT_OUT_PTR(has_been_written, FALSE);
1511+
1512+ /* We want this for all backends when renaming, as *.link and *.rules files are
1513+@@ -1332,8 +1335,8 @@ netplan_netdef_write_networkd(
1514+ }
1515+
1516+ if (def->type == NETPLAN_DEF_TYPE_WIFI || def->has_auth) {
1517+- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", def->id, ".service", NULL);
1518+- g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", def->id, ".service", NULL);
1519++ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", escaped_netdef_id, ".service", NULL);
1520++ g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", escaped_netdef_id, ".service", NULL);
1521+ if (def->type == NETPLAN_DEF_TYPE_WIFI && def->has_match) {
1522+ g_set_error(error, NETPLAN_BACKEND_ERROR, NETPLAN_ERROR_UNSUPPORTED, "ERROR: %s: networkd backend does not support wifi with match:, only by interface name\n", def->id);
1523+ return FALSE;
1524+diff --git a/src/nm.c b/src/nm.c
1525+index a605c38..380b7fd 100644
1526+--- a/src/nm.c
1527++++ b/src/nm.c
1528+@@ -931,10 +931,11 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
1529+ g_datalist_foreach((GData**)&def->backend_settings.passthrough, write_fallback_key_value, kf);
1530+ }
1531+
1532++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1533+ if (ap) {
1534+ g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE);
1535+ /* TODO: make use of netplan_netdef_get_output_filename() */
1536+- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL);
1537++ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
1538+
1539+ g_key_file_set_string(kf, "wifi", "ssid", ap->ssid);
1540+ if (ap->mode < NETPLAN_WIFI_MODE_OTHER)
1541+@@ -969,7 +970,7 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
1542+ }
1543+ } else {
1544+ /* TODO: make use of netplan_netdef_get_output_filename() */
1545+- conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL);
1546++ conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
1547+ if (def->has_auth) {
1548+ write_dot1x_auth_parameters(&def->auth, kf);
1549+ }
1550+diff --git a/src/openvswitch.c b/src/openvswitch.c
1551+index 276762e..b33769d 100644
1552+--- a/src/openvswitch.c
1553++++ b/src/openvswitch.c
1554+@@ -32,9 +32,9 @@
1555+ static gboolean
1556+ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency, GError** error)
1557+ {
1558+- g_autofree gchar* id_escaped = NULL;
1559+- g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL);
1560+- g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL);
1561++ g_autofree char* escaped_netdef_id = g_uri_escape_string(id, NULL, TRUE);
1562++ g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", escaped_netdef_id, ".service", NULL);
1563++ g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", escaped_netdef_id, ".service", NULL);
1564+
1565+ GString* s = g_string_new("[Unit]\n");
1566+ g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id);
1567+@@ -43,9 +43,8 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1568+ g_string_append_printf(s, "Wants=ovsdb-server.service\n");
1569+ g_string_append_printf(s, "After=ovsdb-server.service\n");
1570+ if (physical) {
1571+- id_escaped = systemd_escape((char*) id);
1572+- g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped);
1573+- g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped);
1574++ g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
1575++ g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", escaped_netdef_id);
1576+ }
1577+ if (!cleanup) {
1578+ g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n");
1579+@@ -55,8 +54,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1580+ }
1581+ g_string_append(s, "Before=network.target\nWants=network.target\n");
1582+ if (dependency) {
1583+- g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency);
1584+- g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency);
1585++ g_autofree char* escaped_dependency = g_uri_escape_string(dependency, NULL, TRUE);
1586++ g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", escaped_dependency);
1587++ g_string_append_printf(s, "After=netplan-ovs-%s.service\n", escaped_dependency);
1588+ }
1589+
1590+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
1591+@@ -321,6 +321,7 @@ netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefinitio
1592+ gchar* dependency = NULL;
1593+ const char* type = netplan_type_to_table_name(def->type);
1594+ g_autofree char* base_config_path = NULL;
1595++ g_autofree char* escaped_netdef_id = g_uri_escape_string(def->id, NULL, TRUE);
1596+ char* value = NULL;
1597+ const NetplanOVSSettings* settings = &np_state->ovs_settings;
1598+
1599+@@ -411,7 +412,7 @@ netplan_netdef_write_ovs(const NetplanState* np_state, const NetplanNetDefinitio
1600+
1601+ /* Try writing out a base config */
1602+ /* TODO: make use of netplan_netdef_get_output_filename() */
1603+- base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL);
1604++ base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", escaped_netdef_id, NULL);
1605+ if (!netplan_netdef_write_network_file(np_state, def, rootdir, base_config_path, has_been_written, error))
1606+ return FALSE;
1607+ } else {
1608+diff --git a/src/util.c b/src/util.c
1609+index d8d3a57..a3dae98 100644
1610+--- a/src/util.c
1611++++ b/src/util.c
1612+@@ -691,17 +691,18 @@ ssize_t
1613+ netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const char* ssid, char* out_buffer, size_t out_buf_size)
1614+ {
1615+ g_autofree gchar* conf_path = NULL;
1616++ g_autofree char* escaped_netdef_id = g_uri_escape_string(netdef->id, NULL, TRUE);
1617+
1618+ if (netdef->backend == NETPLAN_BACKEND_NM) {
1619+ if (ssid) {
1620+ g_autofree char* escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE);
1621+- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, "-", escaped_ssid, ".nmconnection", NULL);
1622++ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
1623+ } else {
1624+- conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", netdef->id, ".nmconnection", NULL);
1625++ conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
1626+ }
1627+
1628+ } else if (netdef->backend == NETPLAN_BACKEND_NETWORKD || netdef->backend == NETPLAN_BACKEND_OVS) {
1629+- conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", netdef->id, ".network", NULL);
1630++ conf_path = g_strjoin(NULL, "/run/systemd/network/10-netplan-", escaped_netdef_id, ".network", NULL);
1631+ }
1632+
1633+ if (conf_path)
1634+diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py
1635+index 3aab3f7..f145104 100644
1636+--- a/tests/generator/test_common.py
1637++++ b/tests/generator/test_common.py
1638+@@ -827,6 +827,47 @@ UseMTU=true
1639+
1640+ self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})
1641+
1642++ def test_nd_file_paths_escaped(self):
1643++ self.generate('''network:
1644++ version: 2
1645++ ethernets:
1646++ "abc/../../xyz0":
1647++ match:
1648++ driver: "drv"
1649++ set-name: "eth123"''')
1650++
1651++ self.assert_networkd_udev({'abc%2F..%2F..%2Fxyz0.rules': (UDEV_NO_MAC_RULE % ('drv', 'eth123'))})
1652++ self.assert_networkd({'abc%2F..%2F..%2Fxyz0.network': '''[Match]\nDriver=drv
1653++Name=eth123
1654++
1655++[Network]
1656++LinkLocalAddressing=ipv6
1657++''',
1658++ 'abc%2F..%2F..%2Fxyz0.link': '''[Match]\nDriver=drv\n
1659++[Link]
1660++Name=eth123
1661++WakeOnLan=off
1662++'''})
1663++
1664++ self.generate('''network:
1665++ version: 2
1666++ wifis:
1667++ "abc/../../xyz0":
1668++ dhcp4: true
1669++ access-points:
1670++ "mywifi":
1671++ password: "aaaaaaaa"''')
1672++
1673++ self.assert_wpa_supplicant("abc%2F..%2F..%2Fxyz0", """ctrl_interface=/run/wpa_supplicant
1674++
1675++network={
1676++ ssid=P"mywifi"
1677++ key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
1678++ ieee80211w=1
1679++ psk="aaaaaaaa"
1680++}
1681++""")
1682++
1683+
1684+ class TestNetworkManager(TestBase):
1685+
1686+@@ -1354,6 +1395,31 @@ method=ignore
1687+ dhcp4: true''', skip_generated_yaml_validation=True)
1688+ self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')
1689+
1690++ def test_nm_file_paths_escaped(self):
1691++ self.generate('''network:
1692++ version: 2
1693++ renderer: NetworkManager
1694++ ethernets:
1695++ "abc/../../xyz0":
1696++ match:
1697++ driver: "drv"
1698++ set-name: "eth123"''')
1699++
1700++ self.assert_nm({'abc%2F..%2F..%2Fxyz0': '''[connection]
1701++id=netplan-abc/../../xyz0
1702++type=ethernet
1703++interface-name=eth123
1704++
1705++[ethernet]
1706++wake-on-lan=0
1707++
1708++[ipv4]
1709++method=link-local
1710++
1711++[ipv6]
1712++method=ignore
1713++'''})
1714++
1715+
1716+ class TestForwardDeclaration(TestBase):
1717+
1718+diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
1719+index 1dad3cb..e60d497 100644
1720+--- a/tests/generator/test_ovs.py
1721++++ b/tests/generator/test_ovs.py
1722+@@ -1104,3 +1104,27 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br123 external-ids:netplan/global/set-co
1723+ - [portname, portname]
1724+ ''', expect_fail=True)
1725+ self.assertIn('Open vSwitch patch ports must be of different name', err)
1726++
1727++ def test_file_paths_escaped(self):
1728++ self.generate('''network:
1729++ version: 2
1730++ bridges:
1731++ "abc/../../123":
1732++ openvswitch: {}
1733++ vlans:
1734++ "abc/../../123.100":
1735++ id: 100
1736++ link: "abc/../../123"
1737++''')
1738++ self.assert_ovs({'abc%2F..%2F..%2F123.service': OVS_BR_EMPTY % {'iface': 'abc/../../123'},
1739++ 'abc%2F..%2F..%2F123.100.service': OVS_VIRTUAL % {'iface': 'abc/../../123.100', 'extra':
1740++ '''Requires=netplan-ovs-abc%2F..%2F..%2F123.service
1741++After=netplan-ovs-abc%2F..%2F..%2F123.service
1742++
1743++[Service]
1744++Type=oneshot
1745++TimeoutStartSec=10s
1746++ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123 100
1747++ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
1748++'''},
1749++ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1750diff --git a/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch b/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch
1751new file mode 100644
1752index 0000000..ef7e7af
1753--- /dev/null
1754+++ b/debian/patches/lp2066258/0016-backends-escape-semicolons-in-service-units.patch
1755@@ -0,0 +1,292 @@
1756+From: Danilo Egea Gondolfo <danilogondolfo@gmail.com>
1757+Date: Fri, 24 May 2024 14:11:12 +0100
1758+Subject: backends: escape semicolons in service units
1759+
1760+Semicolons separated from other words by a combination of spaces and/or
1761+tabs will be escaped.
1762+---
1763+ src/networkd.c | 7 +++++
1764+ src/openvswitch.c | 3 +++
1765+ src/sriov.c | 3 +++
1766+ src/util-internal.h | 3 +++
1767+ src/util.c | 34 +++++++++++++++++++++++
1768+ tests/ctests/test_netplan_misc.c | 45 +++++++++++++++++++++++++++++++
1769+ tests/generator/test_ovs.py | 58 ++++++++++++++++++++++++++++++++++++++++
1770+ tests/test_sriov.py | 29 ++++++++++++++++++++
1771+ 8 files changed, 182 insertions(+)
1772+
1773+diff --git a/src/networkd.c b/src/networkd.c
1774+index e66ab4c..f9fefe9 100644
1775+--- a/src/networkd.c
1776++++ b/src/networkd.c
1777+@@ -301,6 +301,9 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
1778+ g_string_append(s, "\n[Service]\nType=oneshot\n");
1779+ g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
1780+
1781++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1782++ g_string_free(s, TRUE);
1783++ s = g_string_new(new_s);
1784+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1785+ safe_mkdir_p_dir(link);
1786+ if (symlink(path, link) < 0 && errno != EEXIST) {
1787+@@ -1191,6 +1194,10 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
1788+ } else {
1789+ g_string_append(s, " -Dnl80211,wext\n");
1790+ }
1791++
1792++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1793++ g_string_free(s, TRUE);
1794++ s = g_string_new(new_s);
1795+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1796+ }
1797+
1798+diff --git a/src/openvswitch.c b/src/openvswitch.c
1799+index b33769d..7d31e7f 100644
1800+--- a/src/openvswitch.c
1801++++ b/src/openvswitch.c
1802+@@ -62,6 +62,9 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
1803+ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n");
1804+ g_string_append(s, cmds->str);
1805+
1806++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1807++ g_string_free(s, TRUE);
1808++ s = g_string_new(new_s);
1809+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1810+
1811+ safe_mkdir_p_dir(link);
1812+diff --git a/src/sriov.c b/src/sriov.c
1813+index c3cd80d..710f1a8 100644
1814+--- a/src/sriov.c
1815++++ b/src/sriov.c
1816+@@ -53,6 +53,9 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
1817+ g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
1818+ g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind %s\n", interfaces->str);
1819+
1820++ g_autofree char* new_s = _netplan_scrub_systemd_unit_contents(s->str);
1821++ g_string_free(s, TRUE);
1822++ s = g_string_new(new_s);
1823+ _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
1824+ g_string_free(interfaces, TRUE);
1825+
1826+diff --git a/src/util-internal.h b/src/util-internal.h
1827+index f6f36fc..e9c2d13 100644
1828+--- a/src/util-internal.h
1829++++ b/src/util-internal.h
1830+@@ -185,3 +185,6 @@ _netplan_netdef_pertype_iter_free(struct netdef_pertype_iter* it);
1831+
1832+ gchar*
1833+ _netplan_scrub_string(const char* content);
1834++
1835++gchar*
1836++_netplan_scrub_systemd_unit_contents(const char* content);
1837+diff --git a/src/util.c b/src/util.c
1838+index a3dae98..30d05cb 100644
1839+--- a/src/util.c
1840++++ b/src/util.c
1841+@@ -1235,3 +1235,37 @@ _netplan_scrub_string(const char* content)
1842+
1843+ return g_string_free(s, FALSE);
1844+ }
1845++
1846++static gboolean
1847++_is_space_or_tab(char c)
1848++{
1849++ return c == ' ' || c == '\t';
1850++}
1851++
1852++char*
1853++_netplan_scrub_systemd_unit_contents(const char* content)
1854++{
1855++ size_t content_len = strlen(content);
1856++ // Assume a few replacements will happen to reduce reallocation
1857++ GString* s = g_string_sized_new(content_len + 8);
1858++
1859++ // Append the first character of "content" to the result string
1860++ g_string_append_len(s, content, 1);
1861++
1862++ // Walk from the second element to the one before the last looking for isolated semicolons
1863++ // A semicolon is isolated if it's surrounded by either tabs or spaces
1864++ const char* p = content + 1;
1865++ while (p < (content + content_len - 1)) {
1866++ if (*p == ';' && _is_space_or_tab(*(p - 1)) && _is_space_or_tab(*(p + 1))) {
1867++ g_string_append_len(s, "\\;", 2);
1868++ } else {
1869++ g_string_append_len(s, p, 1);
1870++ }
1871++ p++;
1872++ }
1873++
1874++ // Append the last character of "content" to the result string
1875++ g_string_append_len(s, p, 1);
1876++
1877++ return g_string_free(s, FALSE);
1878++}
1879+diff --git a/tests/ctests/test_netplan_misc.c b/tests/ctests/test_netplan_misc.c
1880+index 35bf4f8..aa14a71 100644
1881+--- a/tests/ctests/test_netplan_misc.c
1882++++ b/tests/ctests/test_netplan_misc.c
1883+@@ -348,6 +348,50 @@ test_normalize_ip_address(__unused void** state)
1884+ assert_string_equal(normalize_ip_address("0.0.0.0/0", AF_INET), "0.0.0.0/0");
1885+ }
1886+
1887++void
1888++test_scrub_systemd_unit_content(__unused void** state)
1889++{
1890++ char* str = ";abc;";
1891++ char* res = _netplan_scrub_systemd_unit_contents(str);
1892++ assert_string_equal(res, ";abc;");
1893++ g_free(res);
1894++
1895++ str = ";;;;";
1896++ res = _netplan_scrub_systemd_unit_contents(str);
1897++ assert_string_equal(res, ";;;;");
1898++ g_free(res);
1899++
1900++ str = " ;;;; ";
1901++ res = _netplan_scrub_systemd_unit_contents(str);
1902++ assert_string_equal(res, " ;;;; ");
1903++ g_free(res);
1904++
1905++ str = "; ; ; ; ;";
1906++ res = _netplan_scrub_systemd_unit_contents(str);
1907++ assert_string_equal(res, "; \\; \\; \\; ;");
1908++ g_free(res);
1909++
1910++ str = " ; ; ; ; ; ";
1911++ res = _netplan_scrub_systemd_unit_contents(str);
1912++ assert_string_equal(res, " \\; \\; \\; \\; \\; ");
1913++ g_free(res);
1914++
1915++ str = "a ; ; ; ; ; b";
1916++ res = _netplan_scrub_systemd_unit_contents(str);
1917++ assert_string_equal(res, "a \\; \\; \\; \\; \\; b");
1918++ g_free(res);
1919++
1920++ str = "\t;\t; ;\t; \t ;\t ";
1921++ res = _netplan_scrub_systemd_unit_contents(str);
1922++ assert_string_equal(res, "\t\\;\t\\; \\;\t\\; \t \\;\t ");
1923++ g_free(res);
1924++
1925++ str = "\t;\t;\t;\t;\t;\t";
1926++ res = _netplan_scrub_systemd_unit_contents(str);
1927++ assert_string_equal(res, "\t\\;\t\\;\t\\;\t\\;\t\\;\t");
1928++ g_free(res);
1929++}
1930++
1931+ int
1932+ setup(__unused void** state)
1933+ {
1934+@@ -380,6 +424,7 @@ main()
1935+ cmocka_unit_test(test_util_is_route_rule_present),
1936+ cmocka_unit_test(test_util_is_string_in_array),
1937+ cmocka_unit_test(test_normalize_ip_address),
1938++ cmocka_unit_test(test_scrub_systemd_unit_content),
1939+ };
1940+
1941+ return cmocka_run_group_tests(tests, setup, tear_down);
1942+diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py
1943+index e60d497..c40fa47 100644
1944+--- a/tests/generator/test_ovs.py
1945++++ b/tests/generator/test_ovs.py
1946+@@ -1128,3 +1128,61 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-br abc/../../123.100 abc/../../123
1947+ ExecStart=/usr/bin/ovs-vsctl set Interface abc/../../123.100 external-ids:netplan=true
1948+ '''},
1949+ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
1950++
1951++ def test_control_characters_and_semicolons_escaping(self):
1952++ self.generate('''network:
1953++ version: 2
1954++ bridges: # bridges first, to trigger multi-pass processing
1955++ ovs0:
1956++ interfaces: [eth0, eth1]
1957++ openvswitch: {}
1958++ ethernets:
1959++ eth0:
1960++ openvswitch:
1961++ external-ids:
1962++ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
1963++ other-config:
1964++ "a\\n1\\ra": " ; a ; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1"
1965++ dhcp6: true
1966++ eth1:
1967++ dhcp4: true
1968++ openvswitch:
1969++ other-config:
1970++ disable-in-band: false\n''', skip_generated_yaml_validation=True)
1971++ self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': '''
1972++[Service]
1973++Type=oneshot
1974++TimeoutStartSec=10s
1975++ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0
1976++ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1
1977++ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0
1978++''' + OVS_BR_DEFAULT % {'iface': 'ovs0'}},
1979++ 'eth0.service': OVS_PHYSICAL % {'iface': 'eth0', 'extra': '''\
1980++Requires=netplan-ovs-ovs0.service
1981++After=netplan-ovs-ovs0.service
1982++
1983++[Service]
1984++Type=oneshot
1985++TimeoutStartSec=10s
1986++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
1987++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/a\\n1\\ra=,;,a,;,1,;a;,;b\\t;\\t3,;\\ta\\t;,1
1988++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:a\\n1\\ra= \\; a \\; 1 ;a; ;b\\t;\\t3 ;\\ta\\t; 1
1989++ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/other-config/a\\n1\\ra=,;,a,;,1,;a;,;b\\t;\\t3,;\\ta\\t;,1
1990++'''},
1991++ 'eth1.service': OVS_PHYSICAL % {'iface': 'eth1', 'extra': '''\
1992++Requires=netplan-ovs-ovs0.service
1993++After=netplan-ovs-ovs0.service
1994++
1995++[Service]
1996++Type=oneshot
1997++TimeoutStartSec=10s
1998++ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false
1999++ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false
2000++'''},
2001++ 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}})
2002++ # Confirm that the networkd config is still sane
2003++ self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6'),
2004++ 'eth0.network': (ND_DHCP6 % 'eth0')
2005++ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0'),
2006++ 'eth1.network': (ND_DHCP4 % 'eth1')
2007++ .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0')})
2008+diff --git a/tests/test_sriov.py b/tests/test_sriov.py
2009+index 3c06e43..054500c 100644
2010+--- a/tests/test_sriov.py
2011++++ b/tests/test_sriov.py
2012+@@ -913,6 +913,35 @@ After=sys-subsystem-net-devices-engreen.device
2013+ [Service]
2014+ Type=oneshot
2015+ ExecStart=/usr/sbin/netplan rebind enblue engreen
2016++'''})
2017++
2018++ def test_escaping_semicolons_from_unit_file(self):
2019++ ''' Check if semicolons and line breaks are properly escaped in the generated
2020++ systemd service unit.
2021++ '''
2022++ self.generate('''network:
2023++ version: 2
2024++ ethernets:
2025++ engreen:
2026++ embedded-switch-mode: switchdev
2027++ delay-virtual-functions-rebind: true
2028++ enblue:
2029++ match: {driver: dummy_driver}
2030++ set-name: ";en ; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc"
2031++ embedded-switch-mode: legacy
2032++ delay-virtual-functions-rebind: true
2033++ virtual-function-count: 4
2034++ sriov_vf0:
2035++ link: engreen''', skip_generated_yaml_validation=True)
2036++ self.assert_sriov({'rebind.service': '''[Unit]
2037++Description=(Re-)bind SR-IOV Virtual Functions to their driver
2038++After=network.target
2039++After=sys-subsystem-net-devices-;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc.device
2040++After=sys-subsystem-net-devices-engreen.device
2041++
2042++[Service]
2043++Type=oneshot
2044++ExecStart=/usr/sbin/netplan rebind ;en \\; a\\t;\\tb ;\\tc\\t; d; \\n;\\nabc engreen
2045+ '''})
2046+
2047+ def test_rebind_not_delayed(self):
2048diff --git a/debian/patches/series b/debian/patches/series
2049index 8021670..f09ed4e 100644
2050--- a/debian/patches/series
2051+++ b/debian/patches/series
2052@@ -4,3 +4,8 @@ lp2041727/0005-Update-ovs.py-to-check-if-ovsdb-server.service-is-in.patch
2053 0007-tests-assert-generated-.service-files-in-assert_srio.patch
2054 0008-tests-sriov-test-if-the-generated-netplan-rebind-ser.patch
2055 0009-sriov-don-t-generate-duplicate-entries-in-the-rebind.patch
2056+lp2065738/0012-cli-generate-call-daemon-reload-after-generate.patch
2057+lp2065738/0013-libnetplan-use-more-restrictive-file-permissions.patch
2058+lp2066258/0014-libnetplan-escape-control-characters.patch
2059+lp2066258/0015-backends-escape-file-paths.patch
2060+lp2066258/0016-backends-escape-semicolons-in-service-units.patch

Subscribers

People subscribed via source and target branches