• 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

72.25
/src/login/user-runtime-dir.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <linux/magic.h>
4
#include <sys/mount.h>
5

6
#include "sd-bus.h"
7

8
#include "bus-error.h"
9
#include "bus-locator.h"
10
#include "devnum-util.h"
11
#include "errno-util.h"
12
#include "fd-util.h"
13
#include "format-util.h"
14
#include "fs-util.h"
15
#include "label-util.h"
16
#include "limits-util.h"
17
#include "log.h"
18
#include "main-func.h"
19
#include "mkdir-label.h"
20
#include "mount-util.h"
21
#include "mountpoint-util.h"
22
#include "path-util.h"
23
#include "quota-util.h"
24
#include "rm-rf.h"
25
#include "set.h"
26
#include "smack-util.h"
27
#include "stat-util.h"
28
#include "stdio-util.h"
29
#include "string-util.h"
30
#include "strv.h"
31
#include "user-util.h"
32
#include "userdb.h"
33

34
static int acquire_runtime_dir_properties(uint64_t *ret_size, uint64_t *ret_inodes) {
183✔
35
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
×
36
        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
183✔
37
        uint64_t size, inodes;
183✔
38
        int r;
183✔
39

40
        assert(ret_size);
183✔
41
        assert(ret_inodes);
183✔
42

43
        r = sd_bus_default_system(&bus);
183✔
44
        if (r < 0)
183✔
45
                return log_error_errno(r, "Failed to connect to system bus: %m");
×
46

47
        r = bus_get_property_trivial(bus, bus_login_mgr, "RuntimeDirectorySize", &error, 't', &size);
183✔
48
        if (r < 0) {
183✔
49
                log_warning_errno(r, "Failed to acquire runtime directory size, ignoring: %s", bus_error_message(&error, r));
×
50
                sd_bus_error_free(&error);
×
51

52
                size = physical_memory_scale(10U, 100U); /* 10% */
×
53
        }
54

55
        r = bus_get_property_trivial(bus, bus_login_mgr, "RuntimeDirectoryInodesMax", &error, 't', &inodes);
183✔
56
        if (r < 0) {
183✔
57
                log_warning_errno(r, "Failed to acquire number of inodes for runtime directory, ignoring: %s", bus_error_message(&error, r));
×
58
                sd_bus_error_free(&error);
×
59

60
                inodes = DIV_ROUND_UP(size, 4096);
×
61
        }
62

63
        *ret_size = size;
183✔
64
        *ret_inodes = inodes;
183✔
65

66
        return 0;
183✔
67
}
68

69
static int user_mkdir_runtime_path(
183✔
70
                const char *runtime_path,
71
                uid_t uid,
72
                gid_t gid,
73
                uint64_t runtime_dir_size,
74
                uint64_t runtime_dir_inodes) {
75

76
        int r;
183✔
77

78
        assert(runtime_path);
183✔
79
        assert(path_is_absolute(runtime_path));
183✔
80
        assert(uid_is_valid(uid));
183✔
81
        assert(gid_is_valid(gid));
183✔
82

83
        r = mkdir_safe_label("/run/user", 0755, 0, 0, MKDIR_WARN_MODE);
183✔
84
        if (r < 0)
183✔
85
                return log_error_errno(r, "Failed to create /run/user: %m");
×
86

87
        if (path_is_mount_point(runtime_path) > 0)
183✔
UNCOV
88
                log_debug("%s is already a mount point", runtime_path);
×
89
        else {
90
                char options[STRLEN("mode=0700,uid=,gid=,size=,nr_inodes=,smackfsroot=*")
183✔
91
                             + DECIMAL_STR_MAX(uid_t)
92
                             + DECIMAL_STR_MAX(gid_t)
93
                             + DECIMAL_STR_MAX(uint64_t)
94
                             + DECIMAL_STR_MAX(uint64_t)];
95

96
                xsprintf(options,
366✔
97
                         "mode=0700,uid=" UID_FMT ",gid=" GID_FMT ",size=%" PRIu64 ",nr_inodes=%" PRIu64 "%s",
98
                         uid, gid, runtime_dir_size, runtime_dir_inodes,
99
                         mac_smack_use() ? ",smackfsroot=*" : "");
100

101
                _cleanup_free_ char *d = strdup(runtime_path);
183✔
102
                if (!d)
183✔
103
                        return log_oom();
×
104

105
                r = mkdir_label(runtime_path, 0700);
183✔
106
                if (r < 0 && r != -EEXIST)
183✔
107
                        return log_error_errno(r, "Failed to create %s: %m", runtime_path);
×
108

109
                _cleanup_(rmdir_and_freep) char *destroy = TAKE_PTR(d); /* auto-destroy */
×
110

111
                r = mount_nofollow_verbose(LOG_DEBUG, "tmpfs", runtime_path, "tmpfs", MS_NODEV|MS_NOSUID, options);
183✔
112
                if (r < 0) {
183✔
113
                        if (!ERRNO_IS_PRIVILEGE(r))
×
114
                                return log_error_errno(r, "Failed to mount per-user tmpfs directory %s: %m", runtime_path);
×
115

116
                        log_debug_errno(r,
×
117
                                        "Failed to mount per-user tmpfs directory %s.\n"
118
                                        "Assuming containerized execution, ignoring: %m", runtime_path);
119

120
                        r = chmod_and_chown(runtime_path, 0700, uid, gid);
×
121
                        if (r < 0)
×
122
                                return log_error_errno(r, "Failed to change ownership and mode of \"%s\": %m", runtime_path);
×
123
                }
124

125
                destroy = mfree(destroy); /* deactivate auto-destroy */
183✔
126

127
                r = label_fix(runtime_path, 0);
183✔
128
                if (r < 0)
183✔
129
                        log_warning_errno(r, "Failed to fix label of \"%s\", ignoring: %m", runtime_path);
183✔
130
        }
131

132
        return 0;
133
}
134

135
static int do_mount(UserRecord *ur) {
183✔
136
        int r;
183✔
137

138
        assert(ur);
183✔
139

140
        if (!uid_is_valid(ur->uid) || !gid_is_valid(user_record_gid(ur)))
183✔
141
                return log_error_errno(SYNTHETIC_ERRNO(ENOMSG), "User '%s' lacks UID or GID, refusing.", ur->user_name);
×
142

143
        uint64_t runtime_dir_size, runtime_dir_inodes;
183✔
144
        r = acquire_runtime_dir_properties(&runtime_dir_size, &runtime_dir_inodes);
183✔
145
        if (r < 0)
183✔
146
                return r;
147

148
        char runtime_path[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)];
183✔
149
        xsprintf(runtime_path, "/run/user/" UID_FMT, ur->uid);
183✔
150

151
        log_debug("Will mount %s owned by "UID_FMT":"GID_FMT, runtime_path, ur->uid, user_record_gid(ur));
183✔
152
        return user_mkdir_runtime_path(runtime_path, ur->uid, user_record_gid(ur), runtime_dir_size, runtime_dir_inodes);
183✔
153
}
154

155
static int user_remove_runtime_path(const char *runtime_path) {
183✔
156
        int r;
183✔
157

158
        assert(runtime_path);
183✔
159
        assert(path_is_absolute(runtime_path));
183✔
160

161
        r = rm_rf(runtime_path, 0);
183✔
162
        if (r < 0)
183✔
163
                log_debug_errno(r, "Failed to remove runtime directory %s (before unmounting), ignoring: %m", runtime_path);
×
164

165
        /* Ignore cases where the directory isn't mounted, as that's quite possible, if we lacked the permissions to
166
         * mount something */
167
        r = RET_NERRNO(umount2(runtime_path, MNT_DETACH));
183✔
168
        if (r < 0 && !IN_SET(r, -EINVAL, -ENOENT))
×
169
                log_debug_errno(r, "Failed to unmount user runtime directory %s, ignoring: %m", runtime_path);
×
170

171
        r = rm_rf(runtime_path, REMOVE_ROOT);
183✔
172
        if (r < 0 && r != -ENOENT)
183✔
173
                return log_error_errno(r, "Failed to remove runtime directory %s (after unmounting): %m", runtime_path);
×
174

175
        return 0;
176
}
177

178
static int do_umount(const char *user) {
183✔
179
        char runtime_path[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)];
183✔
180
        uid_t uid;
183✔
181
        int r;
183✔
182

183
        /* The user may be already removed. So, first try to parse the string by parse_uid(),
184
         * and if it fails, fall back to get_user_creds(). */
185
        if (parse_uid(user, &uid) < 0) {
183✔
186
                r = get_user_creds(&user, &uid, NULL, NULL, NULL, 0);
×
187
                if (r < 0)
×
188
                        return log_error_errno(r,
×
189
                                               r == -ESRCH ? "No such user \"%s\"" :
190
                                               r == -ENOMSG ? "UID \"%s\" is invalid or has an invalid main group"
191
                                                            : "Failed to look up user \"%s\": %m",
192
                                               user);
193
        }
194

195
        xsprintf(runtime_path, "/run/user/" UID_FMT, uid);
183✔
196

197
        log_debug("Will remove %s", runtime_path);
183✔
198
        return user_remove_runtime_path(runtime_path);
183✔
199
}
200

201
static int apply_tmpfs_quota(
236✔
202
                char **paths,
203
                uid_t uid,
204
                uint64_t limit,
205
                uint32_t scale) {
206

207
        _cleanup_set_free_ Set *processed = NULL;
236✔
208
        int r;
236✔
209

210
        assert(uid_is_valid(uid));
236✔
211

212
        STRV_FOREACH(p, paths) {
590✔
213
                if (limit == UINT64_MAX && scale == UINT32_MAX) {
354✔
214
                        log_debug("No disk quota on '%s' is requested.", *p);
3✔
215
                        continue;
3✔
216
                }
217

218
                _cleanup_close_ int fd = open(*p, O_DIRECTORY|O_CLOEXEC);
705✔
219
                if (fd < 0) {
351✔
220
                        log_warning_errno(errno, "Failed to open '%s' in order to set quota, ignoring: %m", *p);
×
221
                        continue;
×
222
                }
223

224
                struct stat st;
351✔
225
                if (fstat(fd, &st) < 0) {
351✔
226
                        log_warning_errno(errno, "Failed to stat '%s' in order to set quota, ignoring: %m", *p);
×
227
                        continue;
×
228
                }
229

230
                /* Cover for bind mounted or symlinked /var/tmp/ + /tmp/ */
231
                if (set_contains(processed, DEVNUM_TO_PTR(st.st_dev))) {
351✔
232
                        log_debug("Not setting quota on '%s', since already processed.", *p);
×
233
                        continue;
×
234
                }
235

236
                /* Remember we already dealt with this fs, even if the subsequent operation fails, since
237
                 * there's no point in appyling quota twice, regardless if it succeeds or not. */
238
                if (set_ensure_put(&processed, /* hash_ops= */ NULL, DEVNUM_TO_PTR(st.st_dev)) < 0)
351✔
239
                        return log_oom();
×
240

241
                struct statfs sfs;
351✔
242
                if (fstatfs(fd, &sfs) < 0) {
351✔
243
                        log_warning_errno(errno, "Failed to statfs '%s' in order to set quota, ignoring: %m", *p);
×
244
                        continue;
×
245
                }
246

247
                if (!is_fs_type(&sfs, TMPFS_MAGIC)) {
351✔
248
                        log_debug("Not setting quota on '%s', since not tmpfs.", *p);
117✔
249
                        continue;
117✔
250
                }
251

252
                struct dqblk req;
234✔
253
                r = RET_NERRNO(quotactl_fd(fd, QCMD_FIXED(Q_GETQUOTA, USRQUOTA), uid, &req));
234✔
254
                if (r == -ESRCH)
128✔
255
                        zero(req);
×
256
                else if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
234✔
257
                        log_debug_errno(r, "No UID quota support on %s, not setting quota: %m", *p);
×
258
                        continue;
×
259
                } else if (ERRNO_IS_NEG_PRIVILEGE(r)) {
234✔
260
                        log_debug_errno(r, "Lacking privileges to query UID quota on %s, not setting quota: %m", *p);
128✔
261
                        continue;
128✔
262
                } else if (r < 0) {
106✔
263
                        log_warning_errno(r, "Failed to query disk quota on %s for UID " UID_FMT ", ignoring: %m", *p, uid);
×
264
                        continue;
×
265
                }
266

267
                uint64_t v =
212✔
268
                        (scale == 0) ? 0 :
106✔
269
                        (scale == UINT32_MAX) ? UINT64_MAX :
106✔
270
                        (uint64_t) ((double) (sfs.f_blocks * sfs.f_frsize) * scale / UINT32_MAX);
106✔
271

272
                v = MIN(v, limit);
106✔
273
                v /= QIF_DQBLKSIZE;
106✔
274

275
                if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && v == req.dqb_bhardlimit) {
106✔
276
                        /* Shortcut things if everything is set up properly already */
277
                        log_debug("Configured quota on '%s' already matches the intended setting, not updating quota.", *p);
6✔
278
                        continue;
6✔
279
                }
280

281
                req.dqb_valid = QIF_BLIMITS;
100✔
282
                req.dqb_bsoftlimit = req.dqb_bhardlimit = v;
100✔
283

284
                r = RET_NERRNO(quotactl_fd(fd, QCMD_FIXED(Q_SETQUOTA, USRQUOTA), uid, &req));
100✔
285
                if (r == -ESRCH) {
×
286
                        log_debug_errno(r, "Not setting UID quota on %s since UID quota is not supported: %m", *p);
×
287
                        continue;
×
288
                } else if (ERRNO_IS_NEG_PRIVILEGE(r)) {
100✔
289
                        log_debug_errno(r, "Lacking privileges to set UID quota on %s, skipping: %m", *p);
×
290
                        continue;
×
291
                } else if (r < 0) {
100✔
292
                        log_warning_errno(r, "Failed to set disk quota limit to %s on %s for UID " UID_FMT ", ignoring: %m",
×
293
                                          FORMAT_BYTES(v * QIF_DQBLKSIZE), *p, uid);
294
                        continue;
×
295
                }
296

297
                log_info("Successfully configured disk quota for UID " UID_FMT " on %s to %s", uid, *p, FORMAT_BYTES(v * QIF_DQBLKSIZE));
100✔
298
        }
299

300
        return 0;
301
}
302

303
static int do_tmpfs_quota(UserRecord *ur) {
183✔
304
        int r;
183✔
305

306
        assert(ur);
183✔
307

308
        if (user_record_is_root(ur)) {
183✔
309
                log_debug("Not applying tmpfs quota to root user.");
65✔
310
                return 0;
65✔
311
        }
312

313
        if (!uid_is_valid(ur->uid))
118✔
314
                return log_error_errno(SYNTHETIC_ERRNO(ENOMSG), "User '%s' lacks UID, refusing.", ur->user_name);
×
315

316
        r = apply_tmpfs_quota(STRV_MAKE("/tmp", "/var/tmp"), ur->uid, ur->tmp_limit.limit, user_record_tmp_limit_scale(ur));
118✔
317
        if (r < 0)
118✔
318
                return r;
319

320
        r = apply_tmpfs_quota(STRV_MAKE("/dev/shm"), ur->uid, ur->dev_shm_limit.limit, user_record_dev_shm_limit_scale(ur));
118✔
321
        if (r < 0)
118✔
322
                return r;
×
323

324
        return 0;
325
}
326

327
static int run(int argc, char *argv[]) {
366✔
328
        int r;
366✔
329

330
        log_setup();
366✔
331

332
        if (argc != 3)
366✔
333
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
366✔
334
                                       "This program takes two arguments.");
335

336
        const char *verb = argv[1], *user = argv[2];
366✔
337

338
        if (!STR_IN_SET(verb, "start", "stop"))
366✔
339
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
340
                                       "First argument must be either \"start\" or \"stop\".");
341

342
        umask(0022);
366✔
343

344
        r = mac_init();
366✔
345
        if (r < 0)
366✔
346
                return r;
347

348
        if (streq(verb, "start")) {
366✔
349
                _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
183✔
350
                r = userdb_by_name(user, /* match= */ NULL, USERDB_PARSE_NUMERIC|USERDB_SUPPRESS_SHADOW, &ur);
183✔
351
                if (r < 0)
183✔
352
                        return log_error_errno(r, "Failed to resolve user '%s': %s", user, STRERROR_USER(r));
×
353

354
                /* We do two things here: mount the per-user XDG_RUNTIME_DIR, and set up tmpfs quota on /tmp/
355
                 * and /dev/shm/. */
356

357
                r = 0;
183✔
358
                RET_GATHER(r, do_mount(ur));
183✔
359
                RET_GATHER(r, do_tmpfs_quota(ur));
183✔
360
                return r;
183✔
361
        }
362

363
        if (streq(verb, "stop"))
183✔
364
                return do_umount(user);
183✔
365

366
        assert_not_reached();
×
367
}
368

369
DEFINE_MAIN_FUNCTION(run);
366✔
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