Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <sys/stat.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <linux/limits.h>

#include <glib.h>
#include <glib/gprintf.h>
Expand Down Expand Up @@ -1005,7 +1006,16 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir,
if (ap) {
g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE);
/* TODO: make use of netplan_netdef_get_output_filename() */
conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
g_autofree char* candidate_basename = g_strjoin(NULL, "netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
const char* ssid_part = escaped_ssid;
g_autofree char* hashed_ssid = NULL;
if (strlen(candidate_basename) > NAME_MAX) {
/* SSID contains multi-byte chars (e.g. emojis) that percent-encode to too many bytes.
* Use SHA-256 of the raw SSID bytes to guarantee a valid-length unique filename. */
hashed_ssid = g_compute_checksum_for_string(G_CHECKSUM_SHA256, ap->ssid, -1);
ssid_part = hashed_ssid;
}
conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", ssid_part, ".nmconnection", NULL);

g_key_file_set_string(kf, "wifi", "ssid", ap->ssid);
if (ap->mode < NETPLAN_WIFI_MODE_OTHER)
Expand Down
27 changes: 22 additions & 5 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
#include <linux/limits.h>

#include <glib.h>
#include <glib/gprintf.h>
Expand Down Expand Up @@ -131,7 +132,7 @@ void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* roo
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));
g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno)); // LCOV_EXCL_LINE
}
}
}
Expand Down Expand Up @@ -629,7 +630,6 @@ ssize_t
netplan_get_id_from_nm_filepath(const char* filename, const char* ssid, char* out_buffer, size_t out_buf_size)
{
g_autofree gchar* escaped_ssid = NULL;
g_autofree gchar* suffix = NULL;
const char* nm_prefix = "/run/NetworkManager/system-connections/netplan-";
const char* pos = g_strrstr(filename, nm_prefix);
const char* start = NULL;
Expand All @@ -641,8 +641,15 @@ netplan_get_id_from_nm_filepath(const char* filename, const char* ssid, char* ou

if (ssid) {
escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE);
suffix = g_strdup_printf("-%s.nmconnection", escaped_ssid);
end = g_strrstr(filename, suffix);
g_autofree char* escaped_suffix = g_strdup_printf("-%s.nmconnection", escaped_ssid);
end = g_strrstr(filename, escaped_suffix);

if (!end) {
/* Escaped SSID not found; try SHA-256 hash (used when escaped form exceeded NAME_MAX) */
g_autofree char* hashed_ssid = g_compute_checksum_for_string(G_CHECKSUM_SHA256, ssid, -1);
g_autofree char* hashed_suffix = g_strdup_printf("-%s.nmconnection", hashed_ssid);
end = g_strrstr(filename, hashed_suffix);
}
} else
end = g_strrstr(filename, ".nmconnection");

Expand Down Expand Up @@ -673,7 +680,17 @@ netplan_netdef_get_output_filename(const NetplanNetDefinition* netdef, const cha
if (netdef->backend == NETPLAN_BACKEND_NM) {
if (ssid) {
g_autofree char* escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE);
conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
/* Check if the basename would exceed NAME_MAX (255 bytes) */
g_autofree char* candidate_basename = g_strjoin(NULL, "netplan-", escaped_netdef_id, "-", escaped_ssid, ".nmconnection", NULL);
const char* ssid_part = escaped_ssid;
g_autofree char* hashed_ssid = NULL;
if (strlen(candidate_basename) > NAME_MAX) {
/* SSID contains multi-byte chars (e.g. emojis) that percent-encode to too many bytes.
* Use SHA-256 of the raw SSID bytes to guarantee a valid-length unique filename. */
hashed_ssid = g_compute_checksum_for_string(G_CHECKSUM_SHA256, ssid, -1);
ssid_part = hashed_ssid;
}
conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, "-", ssid_part, ".nmconnection", NULL);
} else {
conf_path = g_strjoin(NULL, "/run/NetworkManager/system-connections/netplan-", escaped_netdef_id, ".nmconnection", NULL);
}
Expand Down
69 changes: 69 additions & 0 deletions tests/ctests/test_netplan_misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#include <stddef.h>
#include <setjmp.h>

#include <string.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <linux/limits.h>

#include <cmocka.h>

Expand Down Expand Up @@ -185,6 +187,71 @@ test_netplan_netdef_get_output_filename_invalid_backend(__unused void** state)
assert_int_equal(ret, 0);
}

void
test_netplan_netdef_get_output_filename_nm_with_long_ssid(__unused void** state)
{
NetplanNetDefinition netdef;
/* NM stores non-ASCII SSIDs as semicolon-delimited decimal bytes.
* 20x U+1F600 (😀, UTF-8: F0 9F 98 80) in NM format = "240;159;152;128;" x20
* (320 chars, 80 semicolons). g_uri_escape_string() encodes each ';' as
* '%3B', yielding a 480-char encoded SSID.
* basename = "netplan-" (8) + "wlan0" (5) + "-" (1) + 480 + ".nmconnection" (13) = 507 > NAME_MAX.
* Expects SHA-256 of raw SSID bytes used in filename instead of escaped form. */
const char ssid[] =
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;";
char out_buffer[256] = { 0 };

netdef.backend = NETPLAN_BACKEND_NM;
netdef.id = "wlan0";

ssize_t ret = netplan_netdef_get_output_filename(&netdef, ssid, out_buffer, sizeof(out_buffer) - 1);

/* Basename must NOT exceed NAME_MAX */
const char* basename = strrchr(out_buffer, '/');
assert_true(basename != NULL);
assert_true(strlen(basename + 1) <= NAME_MAX);
/* Must use the expected prefix */
assert_true(g_str_has_prefix(out_buffer,
"/run/NetworkManager/system-connections/netplan-wlan0-"));
/* Must end with .nmconnection */
assert_true(g_str_has_suffix(out_buffer, ".nmconnection"));
/* Returned size must match string length + 1 */
assert_int_equal(ret, (ssize_t)(strlen(out_buffer) + 1));
/* Must contain the SHA-256 hash of the raw SSID, not the escaped form */
g_autofree char* expected_hash = g_compute_checksum_for_string(G_CHECKSUM_SHA256, ssid, -1);
g_autofree char* expected_suffix = g_strdup_printf("-%s.nmconnection", expected_hash);
assert_true(g_str_has_suffix(out_buffer, expected_suffix));
}

void
test_netplan_get_id_from_nm_filepath_with_hashed_ssid(__unused void** state)
{
/* Same NM decimal-byte SSID as test above; escaped form exceeds NAME_MAX
* so netplan_netdef_get_output_filename() uses the SHA-256 hash path.
* netplan_get_id_from_nm_filepath() must fall back to the hash when the
* escaped suffix is not found in the filename. */
const char ssid[] =
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;";
/* Get the hashed filename from the public API */
NetplanNetDefinition netdef = { .backend = NETPLAN_BACKEND_NM, .id = "wlan0" };
char hashed_path[256] = { 0 };
netplan_netdef_get_output_filename(&netdef, ssid, hashed_path, sizeof(hashed_path) - 1);

char id[16] = { 0 };
ssize_t bytes_copied = netplan_get_id_from_nm_filepath(hashed_path, ssid, id, sizeof(id));

assert_string_equal(id, "wlan0");
assert_int_equal(bytes_copied, 6); /* strlen("wlan0") + 1 */
}

void
test_netplan_netdef_write_yaml(__unused void** state)
{
Expand Down Expand Up @@ -575,6 +642,8 @@ main()
cmocka_unit_test(test_netplan_netdef_get_output_filename_networkd),
cmocka_unit_test(test_netplan_netdef_get_output_filename_buffer_is_too_small),
cmocka_unit_test(test_netplan_netdef_get_output_filename_invalid_backend),
cmocka_unit_test(test_netplan_netdef_get_output_filename_nm_with_long_ssid),
cmocka_unit_test(test_netplan_get_id_from_nm_filepath_with_hashed_ssid),
cmocka_unit_test(test_netplan_netdef_write_yaml),
cmocka_unit_test(test_netplan_netdef_write_yaml_90NM),
cmocka_unit_test(test_util_is_route_present),
Expand Down
73 changes: 73 additions & 0 deletions tests/ctests/test_netplan_nm.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <string.h>
#include <sys/stat.h>
#include <linux/limits.h>

#include <cmocka.h>

#include "netplan.h"
#include "util-internal.h"
#include "nm.h"

#include "test_utils.h"

Expand All @@ -28,6 +32,74 @@ test_write_empty_state(__unused void** state)
netplan_state_clear(&np_state);
}

/* A WiFi SSID whose percent-encoded form would make the .nmconnection
* basename exceed NAME_MAX (255) must fall back to a SHA-256 digest.
*
* NM stores non-ASCII SSIDs as semicolon-delimited decimal bytes, e.g.
* the emoji U+1F600 (😀, UTF-8: F0 9F 98 80) becomes "240;159;152;128;".
* g_uri_escape_string() encodes each ';' as '%3B' (3 chars), so 20 such
* emojis → 480-char encoded SSID → 507-byte candidate basename > NAME_MAX.
*/
void
test_write_wifi_long_ssid_uses_hash(__unused void** state)
{
/* 20× U+1F600 (😀) in NM decimal-byte format */
const char ssid[] =
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;"
"240;159;152;128;240;159;152;128;240;159;152;128;240;159;152;128;";

g_autofree char* yaml = g_strdup_printf(
"network:\n"
" version: 2\n"
" renderer: NetworkManager\n"
" wifis:\n"
" wlan0:\n"
" dhcp4: true\n"
" access-points:\n"
" \"%s\":\n"
" password: \"s0s3kr1t\"\n",
ssid);

NetplanState* np_state = load_string_to_netplan_state(yaml);
assert_non_null(np_state);
assert_true(netplan_state_get_netdefs_size(np_state) > 0);

NetplanNetDefinition* netdef = netplan_state_get_netdef(np_state, "wlan0");
assert_non_null(netdef);

char template[] = "/tmp/netplan_nm_test.XXXXXX";
char* rootdir = mkdtemp(template);
assert_non_null(rootdir);

gboolean has_been_written = FALSE;
GError* error = NULL;
assert_true(_netplan_netdef_write_nm(np_state, netdef, rootdir, &has_been_written, &error));
assert_null(error);
assert_true(has_been_written);

/* The output filename must use the SHA-256 digest of the raw SSID */
g_autofree char* hash = g_compute_checksum_for_string(G_CHECKSUM_SHA256, ssid, -1);
g_autofree char* expected = g_strdup_printf(
"%s/run/NetworkManager/system-connections/netplan-wlan0-%s.nmconnection",
rootdir, hash);

assert_true(g_file_test(expected, G_FILE_TEST_EXISTS));

/* Basename must be within NAME_MAX */
const char* basename = strrchr(expected, '/');
assert_true(strlen(basename + 1) <= NAME_MAX);

/* Cleanup */
const gchar *rm_argv[] = { "/bin/rm", "-rf", rootdir, NULL };
g_spawn_sync(NULL, (gchar**)rm_argv, NULL, G_SPAWN_DEFAULT,
NULL, NULL, NULL, NULL, NULL, NULL);

netplan_state_clear(&np_state);
}


int
setup(__unused void** state)
Expand All @@ -47,6 +119,7 @@ main()

const struct CMUnitTest tests[] = {
cmocka_unit_test(test_write_empty_state),
cmocka_unit_test(test_write_wifi_long_ssid_uses_hash),
};

return cmocka_run_group_tests(tests, setup, tear_down);
Expand Down
Loading