• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 19520565317

19 Nov 2025 11:19PM UTC coverage: 72.548% (+0.1%) from 72.449%
19520565317

push

github

web-flow
core: Verify inherited FDs are writable for stdout/stderr (#39674)

When inheriting file descriptors for stdout/stderr (either from stdin or
when making stderr inherit from stdout), we previously just assumed they
would be writable and dup'd them. This could lead to broken setups if
the inherited FD was actually opened read-only.

Before dup'ing any inherited FDs to stdout/stderr, verify they are
actually writable using the new fd_is_writable() helper. If not, fall
back to /dev/null (or reopen the terminal in the TTY case) with a
warning, rather than silently creating a broken setup where output
operations would fail.

31 of 44 new or added lines in 3 files covered. (70.45%)

813 existing lines in 43 files now uncovered.

308541 of 425291 relevant lines covered (72.55%)

1188151.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.08
/src/shared/tests.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <sched.h>
4
#include <stdlib.h>
5
#include <sys/mman.h>
6
#include <unistd.h>
7

8
#include "sd-bus.h"
9
#include "sd-daemon.h"
10

11
#include "alloc-util.h"
12
#include "argv-util.h"
13
#include "bus-error.h"
14
#include "bus-locator.h"
15
#include "bus-util.h"
16
#include "bus-wait-for-jobs.h"
17
#include "cgroup-setup.h"
18
#include "cgroup-util.h"
19
#include "coredump-util.h"
20
#include "env-file.h"
21
#include "env-util.h"
22
#include "errno-util.h"
23
#include "extract-word.h"
24
#include "fd-util.h"
25
#include "fs-util.h"
26
#include "hexdecoct.h"
27
#include "log.h"
28
#include "namespace-util.h"
29
#include "path-util.h"
30
#include "pidref.h"
31
#include "process-util.h"
32
#include "random-util.h"
33
#include "rlimit-util.h"
34
#include "strv.h"
35
#include "tests.h"
36
#include "tmpfile-util.h"
37
#include "uid-range.h"
38

39
char* setup_fake_runtime_dir(void) {
9✔
40
        char *t;
9✔
41

42
        ASSERT_OK(mkdtemp_malloc("/tmp/fake-xdg-runtime-XXXXXX", &t));
9✔
43
        ASSERT_OK(setenv("XDG_RUNTIME_DIR", t, /* overwrite = */ true));
9✔
44
        return t;
9✔
45
}
46

47
static void load_testdata_env(void) {
30✔
48
        static bool called = false;
30✔
49
        _cleanup_free_ char *s = NULL, *d = NULL, *envpath = NULL;
30✔
50
        _cleanup_strv_free_ char **pairs = NULL;
30✔
51
        int r;
30✔
52

53
        if (called)
30✔
54
                return;
55
        called = true;
15✔
56

57
        ASSERT_OK(readlink_and_make_absolute("/proc/self/exe", &s));
15✔
58
        ASSERT_OK(path_extract_directory(s, &d));
15✔
59
        ASSERT_NOT_NULL(envpath = path_join(d, "systemd-runtest.env"));
15✔
60

61
        r = load_env_file_pairs(NULL, envpath, &pairs);
15✔
62
        if (r < 0) {
15✔
63
                log_debug_errno(r, "Reading %s failed, ignoring: %m", envpath);
15✔
64
                return;
15✔
65
        }
66

67
        STRV_FOREACH_PAIR(k, v, pairs)
×
68
                ASSERT_OK(setenv(*k, *v, /* overwrite = */ false));
×
69
}
70

71
int get_testdata_dir(const char *suffix, char **ret) {
29✔
72
        const char *dir;
29✔
73
        char *p;
29✔
74

75
        load_testdata_env();
29✔
76

77
        /* if the env var is set, use that */
78
        dir = getenv("SYSTEMD_TEST_DATA");
29✔
79
        if (!dir)
29✔
80
                dir = SYSTEMD_TEST_DATA;
29✔
81
        if (access(dir, F_OK) < 0)
29✔
82
                return log_error_errno(errno, "ERROR: $SYSTEMD_TEST_DATA directory [%s] not accessible: %m", dir);
×
83

84
        p = path_join(dir, suffix);
29✔
85
        if (!p)
29✔
86
                return log_oom();
×
87

88
        *ret = p;
29✔
89
        return 0;
29✔
90
}
91

92
const char* get_catalog_dir(void) {
1✔
93
        const char *env;
1✔
94

95
        load_testdata_env();
1✔
96

97
        /* if the env var is set, use that */
98
        env = getenv("SYSTEMD_CATALOG_DIR");
1✔
99
        if (!env)
1✔
100
                env = SYSTEMD_CATALOG_DIR;
1✔
101
        if (access(env, F_OK) < 0) {
1✔
102
                fprintf(stderr, "ERROR: $SYSTEMD_CATALOG_DIR directory [%s] does not exist\n", env);
×
103
                exit(EXIT_FAILURE);
×
104
        }
105
        return env;
1✔
106
}
107

108
bool slow_tests_enabled(void) {
17✔
109
        int r;
17✔
110

111
        r = getenv_bool("SYSTEMD_SLOW_TESTS");
17✔
112
        if (r >= 0)
17✔
113
                return r;
×
114

115
        if (r != -ENXIO)
17✔
116
                log_warning_errno(r, "Cannot parse $SYSTEMD_SLOW_TESTS, ignoring.");
×
117
        return SYSTEMD_SLOW_TESTS_DEFAULT;
118
}
119

120
void test_setup_logging(int level) {
408✔
121
        log_set_assert_return_is_critical(true);
408✔
122
        log_set_max_level(level);
408✔
123
        log_setup();
408✔
124
}
408✔
125

126
int write_tmpfile(char *pattern, const char *contents) {
17✔
127
        _cleanup_close_ int fd = -EBADF;
17✔
128

129
        assert(pattern);
17✔
130
        assert(contents);
17✔
131

132
        fd = mkostemp_safe(pattern);
17✔
133
        if (fd < 0)
17✔
134
                return fd;
135

136
        ssize_t l = strlen(contents);
17✔
137
        errno = 0;
17✔
138
        if (write(fd, contents, l) != l)
17✔
139
                return errno_or_else(EIO);
×
140
        return 0;
141
}
142

143
bool have_namespaces(void) {
4✔
144
        _cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
4✔
145
        int r;
4✔
146

147
        /* Checks whether namespaces are available. In some cases they aren't. We do this by calling unshare(), and we
148
         * do so in a child process in order not to affect our own process. */
149

150
        ASSERT_OK(r = pidref_safe_fork("(have_namespace)", /* flags = */ 0, &pid));
4✔
151
        if (r == 0) {
7✔
152
                /* child */
153
                if (detach_mount_namespace() < 0)
3✔
154
                        _exit(EXIT_FAILURE);
×
155

156
                _exit(EXIT_SUCCESS);
3✔
157
        }
158

159
        ASSERT_OK(r = pidref_wait_for_terminate_and_check("(have_namespace)", &pid, /* flags = */ 0));
4✔
160
        if (r == EXIT_SUCCESS)
4✔
161
                return true;
162

163
        if (r == EXIT_FAILURE)
×
164
                return false;
165

166
        assert_not_reached();
×
167
}
168

169
bool userns_has_single_user(void) {
9✔
170
        _cleanup_(uid_range_freep) UIDRange *uidrange = NULL, *gidrange = NULL;
9✔
171

172
        /* Check if we're in a user namespace with only a single user mapped in. We special case this
173
         * scenario in a few tests because it's the only kind of namespace that can be created unprivileged
174
         * and as such happens more often than not, so we make sure to deal with it so that all tests pass
175
         * in such environments. */
176

177
        if (uid_range_load_userns(NULL, UID_RANGE_USERNS_INSIDE, &uidrange) < 0)
9✔
178
                return false;
179

180
        if (uid_range_load_userns(NULL, GID_RANGE_USERNS_INSIDE, &gidrange) < 0)
9✔
181
                return false;
182

183
        return uidrange->n_entries == 1 && uidrange->entries[0].nr == 1 &&
9✔
184
                gidrange->n_entries == 1 && gidrange->entries[0].nr == 1;
18✔
185
}
186

187
bool can_memlock(void) {
2✔
188
        /* Let's see if we can mlock() a larger blob of memory. BPF programs are charged against
189
         * RLIMIT_MEMLOCK, hence let's first make sure we can lock memory at all, and skip the test if we
190
         * cannot. Why not check RLIMIT_MEMLOCK explicitly? Because in container environments the
191
         * RLIMIT_MEMLOCK value we see might not match the RLIMIT_MEMLOCK value actually in effect. */
192

193
        void *p = mmap(NULL, CAN_MEMLOCK_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);
2✔
194
        if (p == MAP_FAILED)
2✔
195
                return false;
196

197
        bool b = mlock(p, CAN_MEMLOCK_SIZE) >= 0;
2✔
198
        if (b)
2✔
199
                ASSERT_OK(munlock(p, CAN_MEMLOCK_SIZE));
×
200

201
        ASSERT_OK(munmap(p, CAN_MEMLOCK_SIZE));
2✔
202
        return b;
203
}
204

205
static int allocate_scope(void) {
17✔
206
        _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
23✔
207
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
17✔
208
        _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
×
209
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
17✔
210
        _cleanup_free_ char *scope = NULL, *cgroup_root = NULL;
17✔
211
        const char *object;
17✔
212
        int r;
17✔
213

214
        /* Let's try to run this test in a scope of its own, with delegation turned on, so that PID 1 doesn't
215
         * interfere with our cgroup management. */
216
        if (cg_pid_get_path(0, &cgroup_root) >= 0 && cg_is_delegated(cgroup_root) && stderr_is_journal()) {
17✔
217
                log_debug("Already running as a unit with delegated cgroup, not allocating a cgroup subroot.");
11✔
218
                return 0;
11✔
219
        }
220

221
        if (geteuid() == 0)
6✔
222
                r = sd_bus_default_system(&bus);
6✔
223
        else
224
                r = sd_bus_default_user(&bus);
×
225
        if (r < 0)
6✔
226
                return log_error_errno(r, "Failed to connect to system bus: %m");
×
227

228
        r = bus_wait_for_jobs_new(bus, &w);
6✔
229
        if (r < 0)
6✔
230
                return log_error_errno(r, "Could not watch jobs: %m");
×
231

232
        if (asprintf(&scope, "%s-%" PRIx64 ".scope", program_invocation_short_name, random_u64()) < 0)
6✔
233
                return log_oom();
×
234

235
        r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "StartTransientUnit");
6✔
236
        if (r < 0)
6✔
237
                return bus_log_create_error(r);
×
238

239
        /* Name and Mode */
240
        r = sd_bus_message_append(m, "ss", scope, "fail");
6✔
241
        if (r < 0)
6✔
242
                return bus_log_create_error(r);
×
243

244
        /* Properties */
245
        r = sd_bus_message_open_container(m, 'a', "(sv)");
6✔
246
        if (r < 0)
6✔
247
                return bus_log_create_error(r);
×
248

249
        r = sd_bus_message_append(m, "(sv)", "PIDs", "au", 1, (uint32_t) getpid_cached());
6✔
250
        if (r < 0)
6✔
251
                return bus_log_create_error(r);
×
252

253
        r = sd_bus_message_append(m, "(sv)", "Delegate", "b", 1);
6✔
254
        if (r < 0)
6✔
255
                return bus_log_create_error(r);
×
256

257
        r = sd_bus_message_append(m, "(sv)", "CollectMode", "s", "inactive-or-failed");
6✔
258
        if (r < 0)
6✔
259
                return bus_log_create_error(r);
×
260

261
        r = sd_bus_message_close_container(m);
6✔
262
        if (r < 0)
6✔
263
                return bus_log_create_error(r);
×
264

265
        /* Auxiliary units */
266
        r = sd_bus_message_append(m, "a(sa(sv))", 0);
6✔
267
        if (r < 0)
6✔
268
                return bus_log_create_error(r);
×
269

270
        r = sd_bus_call(bus, m, 0, &error, &reply);
6✔
271
        if (r < 0)
6✔
272
                return log_error_errno(r, "Failed to start transient scope unit: %s", bus_error_message(&error, r));
×
273

274
        r = sd_bus_message_read(reply, "o", &object);
6✔
275
        if (r < 0)
6✔
276
                return bus_log_parse_error(r);
×
277

278
        r = bus_wait_for_jobs_one(w, object, BUS_WAIT_JOBS_LOG_ERROR, NULL);
6✔
279
        if (r < 0)
6✔
280
                return r;
×
281

282
        return 0;
283
}
284

285
static int enter_cgroup(char **ret_cgroup, bool enter_subroot) {
17✔
286
        _cleanup_free_ char *cgroup_root = NULL, *cgroup_subroot = NULL;
17✔
287
        CGroupMask supported;
17✔
288
        int r;
17✔
289

290
        r = cg_is_available();
17✔
291
        if (r < 0)
17✔
292
                return r;
293
        if (r == 0)
17✔
294
                return log_warning_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "cgroupfs v2 is not mounted.");
×
295

296
        r = allocate_scope();
17✔
297
        if (r < 0)
17✔
298
                log_warning_errno(r, "Couldn't allocate a scope unit for this test, proceeding without.");
×
299

300
        r = cg_pid_get_path(0, &cgroup_root);
17✔
301
        if (IN_SET(r, -ENOMEDIUM, -ENOENT))
17✔
302
                return log_warning_errno(r, "cg_pid_get_path(0, ...) failed: %m");
×
303
        ASSERT_OK(r);
17✔
304

305
        if (enter_subroot)
17✔
306
                ASSERT_OK(asprintf(&cgroup_subroot, "%s/%" PRIx64, cgroup_root, random_u64()));
16✔
307
        else
308
                ASSERT_NOT_NULL(cgroup_subroot = strdup(cgroup_root));
1✔
309

310
        ASSERT_OK(cg_mask_supported(&supported));
17✔
311

312
        /* If this fails, then we don't mind as the later cgroup operations will fail too, and it's fine if
313
         * we handle any errors at that point. */
314

315
        r = cg_create_and_attach(cgroup_subroot, 0);
17✔
316
        if (r < 0)
17✔
317
                return r;
318

319
        if (ret_cgroup)
17✔
320
                *ret_cgroup = TAKE_PTR(cgroup_subroot);
1✔
321

322
        return 0;
323
}
324

325
int enter_cgroup_subroot(char **ret_cgroup) {
16✔
326
        return enter_cgroup(ret_cgroup, true);
16✔
327
}
328

329
int enter_cgroup_root(char **ret_cgroup) {
1✔
330
        return enter_cgroup(ret_cgroup, false);
1✔
331
}
332

333
int define_hex_ptr_internal(const char *hex, void **name, size_t *name_len) {
180✔
334
        return unhexmem_full(hex, strlen_ptr(hex), false, name, name_len);
352✔
335
}
336

337
const char* ci_environment(void) {
1✔
338
        /* We return a string because we might want to provide multiple bits of information later on: not
339
         * just the general CI environment type, but also whether we're sanitizing or not, etc. The caller is
340
         * expected to use strstr on the returned value. */
341
        static const char *ans = POINTER_MAX;
1✔
342
        int r;
1✔
343

344
        if (ans != POINTER_MAX)
1✔
345
                return ans;
346

347
        /* We allow specifying the environment with $CITYPE. Nobody uses this so far, but we are ready. */
348
        const char *citype = getenv("CITYPE");
1✔
349
        if (!isempty(citype))
1✔
350
                return (ans = citype);
×
351

352
        if (getenv_bool("TRAVIS") > 0)
1✔
353
                return (ans = "travis");
×
354
        if (getenv_bool("SEMAPHORE") > 0)
1✔
355
                return (ans = "semaphore");
×
356
        if (getenv_bool("GITHUB_ACTIONS") > 0)
1✔
357
                return (ans = "github-actions");
×
358
        if (getenv("AUTOPKGTEST_ARTIFACTS") || getenv("AUTOPKGTEST_TMP"))
1✔
359
                return (ans = "autopkgtest");
×
360
        if (getenv("SALSA_CI_IMAGES"))
1✔
361
                return (ans = "salsa-ci");
×
362

363
        FOREACH_STRING(var, "CI", "CONTINOUS_INTEGRATION") {
3✔
364
                /* Those vars are booleans according to Semaphore and Travis docs:
365
                 * https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
366
                 * https://docs.semaphoreci.com/ci-cd-environment/environment-variables/#ci
367
                 */
368
                r = getenv_bool(var);
2✔
369
                if (r > 0)
2✔
370
                        return (ans = "unknown"); /* Some other unknown thing */
×
371
                if (r == 0)
2✔
372
                        return (ans = NULL);
×
373
        }
374

375
        return (ans = NULL);
1✔
376
}
377

378
int run_test_table(const TestFunc *start, const TestFunc *end) {
264✔
379
        _cleanup_strv_free_ char **tests = NULL;
247✔
380
        int r = EXIT_SUCCESS;
264✔
381
        bool ran = false;
264✔
382
        const char *e;
264✔
383

384
        if (!start)
264✔
385
                return r;
386

387
        e = getenv("TESTFUNCS");
263✔
388
        if (e) {
263✔
389
                r = strv_split_full(&tests, e, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
×
390
                if (r < 0)
×
391
                        return log_error_errno(r, "Failed to parse $TESTFUNCS: %m");
×
392
        }
393

394
        for (const TestFunc *t = ALIGN_PTR(start); t + 1 <= end; t = ALIGN_PTR(t + 1)) {
2,256✔
395

396
                if (tests && !strv_contains(tests, t->name))
2,010✔
397
                        continue;
×
398

399
                if (t->sd_booted && sd_booted() <= 0) {
2,010✔
400
                        log_info("/* systemd not booted, skipping %s */", t->name);
×
401
                        if (t->has_ret && r == EXIT_SUCCESS)
×
402
                                r = EXIT_TEST_SKIP;
×
403
                } else {
404
                        log_info("/* %s */", t->name);
2,010✔
405

406
                        if (t->has_ret) {
2,010✔
407
                                int r2 = t->f.int_func();
17✔
408
                                if (r == EXIT_SUCCESS)
17✔
409
                                        r = r2;
14✔
410
                        } else
411
                                t->f.void_func();
1,993✔
412
                }
413

414
                ran = true;
415
        }
416

417
        if (!ran)
246✔
418
                return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No matching tests found.");
×
419

420
        return r;
421
}
422

423
void test_prepare(int argc, char *argv[], int log_level) {
269✔
424
        save_argc_argv(argc, argv);
269✔
425
        test_setup_logging(log_level);
269✔
426
}
269✔
427

428
/* Returns:
429
 * ASSERT_SIGNAL_FORK_CHILD  = We are in the child process
430
 * ASSERT_SIGNAL_FORK_PARENT = We are in the parent process (signal/status stored in *ret_signal)
431
 * <0                        = Error (negative errno)
432
 */
433
int assert_signal_internal(int *ret_signal) {
72✔
434
        siginfo_t siginfo = {};
72✔
435
        int r;
72✔
436

437
        assert(ret_signal);
72✔
438

439
        r = fork();
72✔
440
        if (r < 0)
78✔
441
                return -errno;
×
442

443
        if (r == 0) {
78✔
444
                /* Speed things up by never even attempting to generate a coredump */
445
                (void) set_dumpable(SUID_DUMP_DISABLE);
6✔
446

447
                /* But still set an rlimit just in case */
448
                (void) setrlimit(RLIMIT_CORE, &RLIMIT_MAKE_CONST(0));
6✔
449
                return ASSERT_SIGNAL_FORK_CHILD;
6✔
450
        }
451

452
        r = wait_for_terminate(r, &siginfo);
72✔
453
        if (r < 0)
72✔
454
                return r;
455

456
        /* si_status means different things depending on si_code:
457
         * - CLD_EXITED: si_status is the exit code
458
         * - CLD_KILLED/CLD_DUMPED: si_status is the signal number that killed the process
459
         * We need to return the signal number only if the child was killed by a signal. */
460
        if (IN_SET(siginfo.si_code, CLD_KILLED, CLD_DUMPED))
72✔
461
                *ret_signal = siginfo.si_status;
72✔
462
        else
UNCOV
463
                *ret_signal = 0;
×
464

465
        return ASSERT_SIGNAL_FORK_PARENT;
466
}
467

468

UNCOV
469
void log_test_failed_internal(const char *file, int line, const char *func, const char *format, ...) {
×
UNCOV
470
        va_list ap;
×
471

UNCOV
472
        va_start(ap, format);
×
UNCOV
473
        DISABLE_WARNING_FORMAT_NONLITERAL;
×
UNCOV
474
        log_internalv(LOG_ERR, 0, file, line, func, format, ap);
×
UNCOV
475
        REENABLE_WARNING;
×
UNCOV
476
        va_end(ap);
×
477

UNCOV
478
        abort();
×
479
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc