@samitouri / QOSamiQemu / commits / 05221c600a

tests/unit: add test-envlist covering setenv/unsetenv name matching

util/envlist had no test coverage. Add tests/unit/test-envlist exercising the public envlist API and pinning down the prefix-match hazard fixed in the previous commit: - envlist_unsetenv("FOO") must not remove an entry named "FOOBAR"; - envlist_setenv("FOO=...") must not replace an existing "FOOBAR=..." entry placed earlier in the list (envlist_setenv() inserts at the head, so the first prefix match wins under the old strncmp rule). Also cover the rest of the contract: head-insertion order observed through envlist_to_environ(), replacement of an existing variable, the count argument of envlist_to_environ(), and the documented EINVAL paths (NULL inputs, setenv without '=', unsetenv with '='). Signed-off-by: Denis V. Lunev <den@openvz.org> Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com> Message-id: 20260520212628.479772-3-den@openvz.org Cc: Stefan Hajnoczi <stefanha@redhat.com> Cc: Markus Armbruster <armbru@redhat.com> Cc: Paolo Bonzini <pbonzini@redhat.com> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>

Denis V. Lunev committed May 20, 2026 at 23:26 UTC 05221c600a5f3ef657d71aeaea632c5f1bab3a2d
2 files changed +197
tests/unit/meson.build
+1
@@ -49,6 +49,7 @@ tests = {
49 'test-qapi-util': [],
50 'test-interval-tree': [],
51 'test-fifo': [],
52 + 'test-envlist': [],
53 }
54
55 if have_system or have_tools
tests/unit/test-envlist.c new
+196
@@ -0,0 +1,196 @@
1 +/* SPDX-License-Identifier: GPL-2.0-or-later */
2 +/*
3 + * envlist tests
4 + *
5 + * Copyright 2026 Virtuozzo International GmbH
6 + *
7 + * Authors:
8 + * Denis V. Lunev <den@openvz.org>
9 + */
10 +
11 +#include "qemu/osdep.h"
12 +#include "qemu/envlist.h"
13 +
14 +static void free_environ(char **env)
15 +{
16 + char **p;
17 +
18 + for (p = env; *p != NULL; p++) {
19 + g_free(*p);
20 + }
21 + g_free(env);
22 +}
23 +
24 +static const char *find_env(char **env, const char *name)
25 +{
26 + size_t name_len = strlen(name);
27 + char **p;
28 +
29 + for (p = env; *p != NULL; p++) {
30 + if (strncmp(*p, name, name_len) == 0 && (*p)[name_len] == '=') {
31 + return *p + name_len + 1;
32 + }
33 + }
34 + return NULL;
35 +}
36 +
37 +static void test_envlist_basic(void)
38 +{
39 + envlist_t *el = envlist_create();
40 + char **env;
41 + size_t count;
42 +
43 + /* empty list */
44 + env = envlist_to_environ(el, &count);
45 + g_assert_cmpuint(count, ==, 0);
46 + g_assert_null(env[0]);
47 + free_environ(env);
48 +
49 + /* add */
50 + g_assert_cmpint(envlist_setenv(el, "A=1"), ==, 0);
51 + g_assert_cmpint(envlist_setenv(el, "B=2"), ==, 0);
52 +
53 + env = envlist_to_environ(el, &count);
54 + g_assert_cmpuint(count, ==, 2);
55 + g_assert_cmpstr(find_env(env, "A"), ==, "1");
56 + g_assert_cmpstr(find_env(env, "B"), ==, "2");
57 + free_environ(env);
58 +
59 + /* replace */
60 + g_assert_cmpint(envlist_setenv(el, "A=42"), ==, 0);
61 + env = envlist_to_environ(el, &count);
62 + g_assert_cmpuint(count, ==, 2);
63 + g_assert_cmpstr(find_env(env, "A"), ==, "42");
64 + g_assert_cmpstr(find_env(env, "B"), ==, "2");
65 + free_environ(env);
66 +
67 + /* unset existing */
68 + g_assert_cmpint(envlist_unsetenv(el, "A"), ==, 0);
69 + env = envlist_to_environ(el, &count);
70 + g_assert_cmpuint(count, ==, 1);
71 + g_assert_null(find_env(env, "A"));
72 + g_assert_cmpstr(find_env(env, "B"), ==, "2");
73 + free_environ(env);
74 +
75 + /* unset non-existing is a no-op success */
76 + g_assert_cmpint(envlist_unsetenv(el, "NOPE"), ==, 0);
77 + env = envlist_to_environ(el, &count);
78 + g_assert_cmpuint(count, ==, 1);
79 + free_environ(env);
80 +
81 + envlist_free(el);
82 +}
83 +
84 +/*
85 + * envlist_setenv() inserts at the head; envlist_to_environ() walks
86 + * head-to-tail, so the last setenv comes out first.
87 + */
88 +static void test_envlist_head_insertion_order(void)
89 +{
90 + envlist_t *el = envlist_create();
91 + char **env;
92 + size_t count;
93 +
94 + g_assert_cmpint(envlist_setenv(el, "A=1"), ==, 0);
95 + g_assert_cmpint(envlist_setenv(el, "B=2"), ==, 0);
96 + g_assert_cmpint(envlist_setenv(el, "C=3"), ==, 0);
97 +
98 + env = envlist_to_environ(el, &count);
99 + g_assert_cmpuint(count, ==, 3);
100 + g_assert_cmpstr(env[0], ==, "C=3");
101 + g_assert_cmpstr(env[1], ==, "B=2");
102 + g_assert_cmpstr(env[2], ==, "A=1");
103 + g_assert_null(env[3]);
104 +
105 + free_environ(env);
106 + envlist_free(el);
107 +}
108 +
109 +static void test_envlist_einval(void)
110 +{
111 + envlist_t *el = envlist_create();
112 +
113 + /* NULL list */
114 + g_assert_cmpint(envlist_setenv(NULL, "A=1"), ==, EINVAL);
115 + g_assert_cmpint(envlist_unsetenv(NULL, "A"), ==, EINVAL);
116 +
117 + /* NULL string */
118 + g_assert_cmpint(envlist_setenv(el, NULL), ==, EINVAL);
119 + g_assert_cmpint(envlist_unsetenv(el, NULL), ==, EINVAL);
120 +
121 + /* setenv: missing '=' */
122 + g_assert_cmpint(envlist_setenv(el, "NOEQ"), ==, EINVAL);
123 +
124 + /* unsetenv: name must not contain '=' */
125 + g_assert_cmpint(envlist_unsetenv(el, "A=B"), ==, EINVAL);
126 +
127 + envlist_free(el);
128 +}
129 +
130 +/*
131 + * Regression: envlist_unsetenv("FOO") must not remove an entry named
132 + * "FOOBAR" -- the previous strncmp(entry, name, strlen(name)) lookup
133 + * prefix-matched. To trigger the bug, the longer-named entry has to
134 + * be ahead of the target in the list: envlist_setenv() inserts at
135 + * the head, so we add FOO first and FOOBAR last.
136 + */
137 +static void test_envlist_unsetenv_no_prefix_match(void)
138 +{
139 + envlist_t *el = envlist_create();
140 + char **env;
141 + size_t count;
142 +
143 + g_assert_cmpint(envlist_setenv(el, "FOO=y"), ==, 0);
144 + g_assert_cmpint(envlist_setenv(el, "FOOBAR=x"), ==, 0);
145 +
146 + g_assert_cmpint(envlist_unsetenv(el, "FOO"), ==, 0);
147 +
148 + env = envlist_to_environ(el, &count);
149 + g_assert_cmpuint(count, ==, 1);
150 + g_assert_cmpstr(find_env(env, "FOOBAR"), ==, "x");
151 + g_assert_null(find_env(env, "FOO"));
152 +
153 + free_environ(env);
154 + envlist_free(el);
155 +}
156 +
157 +/*
158 + * envlist_setenv() must not replace a prior FOOBAR=... entry when
159 + * setting FOO=... The pre-fix code happened to be safe here only
160 + * because it included the trailing '=' byte in its strncmp length;
161 + * this test pins down the post-fix contract that the name boundary
162 + * is a property of the entry, not of the encoded form.
163 + */
164 +static void test_envlist_setenv_no_prefix_match(void)
165 +{
166 + envlist_t *el = envlist_create();
167 + char **env;
168 + size_t count;
169 +
170 + g_assert_cmpint(envlist_setenv(el, "FOOBAR=x"), ==, 0);
171 + g_assert_cmpint(envlist_setenv(el, "FOO=y"), ==, 0);
172 +
173 + env = envlist_to_environ(el, &count);
174 + g_assert_cmpuint(count, ==, 2);
175 + g_assert_cmpstr(find_env(env, "FOOBAR"), ==, "x");
176 + g_assert_cmpstr(find_env(env, "FOO"), ==, "y");
177 +
178 + free_environ(env);
179 + envlist_free(el);
180 +}
181 +
182 +int main(int argc, char *argv[])
183 +{
184 + g_test_init(&argc, &argv, NULL);
185 +
186 + g_test_add_func("/envlist/basic", test_envlist_basic);
187 + g_test_add_func("/envlist/head_insertion_order",
188 + test_envlist_head_insertion_order);
189 + g_test_add_func("/envlist/einval", test_envlist_einval);
190 + g_test_add_func("/envlist/unsetenv_no_prefix_match",
191 + test_envlist_unsetenv_no_prefix_match);
192 + g_test_add_func("/envlist/setenv_no_prefix_match",
193 + test_envlist_setenv_no_prefix_match);
194 +
195 + return g_test_run();
196 +}