Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sudo-project
GitHub Repository: sudo-project/sudo
Path: blob/main/src/edit_open.c
1532 views
1
/*
2
* SPDX-License-Identifier: ISC
3
*
4
* Copyright (c) 2015-2021 Todd C. Miller <[email protected]>
5
*
6
* Permission to use, copy, modify, and distribute this software for any
7
* purpose with or without fee is hereby granted, provided that the above
8
* copyright notice and this permission notice appear in all copies.
9
*
10
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
*/
18
19
#include <config.h>
20
21
#include <sys/types.h>
22
#include <sys/stat.h>
23
#include <stdio.h>
24
#include <stdlib.h>
25
#include <string.h>
26
#include <unistd.h>
27
#include <errno.h>
28
#include <grp.h>
29
#include <pwd.h>
30
#include <fcntl.h>
31
32
#include <sudo.h>
33
#include <sudo_edit.h>
34
35
#if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID)
36
37
static int
38
switch_user_int(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups,
39
bool nonfatal)
40
{
41
int serrno = errno;
42
int ret = -1;
43
debug_decl(switch_user, SUDO_DEBUG_EDIT);
44
45
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
46
"set uid:gid to %u:%u(%u)", (unsigned int)euid, (unsigned int)egid,
47
ngroups > 0 ? (unsigned int)groups[0] : (unsigned int)egid);
48
49
/* When restoring root, change euid first; otherwise change it last. */
50
if (euid == ROOT_UID) {
51
if (seteuid(ROOT_UID) != 0) {
52
if (nonfatal)
53
goto done;
54
sudo_fatal("seteuid(ROOT_UID)");
55
}
56
}
57
if (setegid(egid) != 0) {
58
if (nonfatal)
59
goto done;
60
sudo_fatal("setegid(%d)", (int)egid);
61
}
62
if (ngroups != -1) {
63
if (sudo_setgroups(ngroups, groups) != 0) {
64
if (nonfatal)
65
goto done;
66
sudo_fatal("setgroups");
67
}
68
}
69
if (euid != ROOT_UID) {
70
if (seteuid(euid) != 0) {
71
if (nonfatal)
72
goto done;
73
sudo_fatal("seteuid(%u)", (unsigned int)euid);
74
}
75
}
76
ret = 0;
77
78
done:
79
errno = serrno;
80
debug_return_int(ret);
81
}
82
83
#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS)
84
static int
85
switch_user_nonfatal(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups)
86
{
87
return switch_user_int(euid, egid, ngroups, groups, true);
88
}
89
#endif
90
91
void
92
switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups)
93
{
94
(void)switch_user_int(euid, egid, ngroups, groups, false);
95
}
96
97
static bool
98
group_matches(gid_t target, const struct sudo_cred *cred)
99
{
100
int i;
101
debug_decl(group_matches, SUDO_DEBUG_EDIT);
102
103
if (target == cred->gid) {
104
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
105
"user gid %u matches directory gid %u", (unsigned int)cred->gid,
106
(unsigned int)target);
107
debug_return_bool(true);
108
}
109
for (i = 0; i < cred->ngroups; i++) {
110
if (target == cred->groups[i]) {
111
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
112
"user gid %u matches directory gid %u",
113
(unsigned int)cred->groups[i], (unsigned int)target);
114
debug_return_bool(true);
115
}
116
}
117
debug_return_bool(false);
118
}
119
120
static bool
121
is_writable(const struct sudo_cred *user_cred, struct stat *sb)
122
{
123
debug_decl(is_writable, SUDO_DEBUG_EDIT);
124
125
/* Other writable? */
126
if (ISSET(sb->st_mode, S_IWOTH)) {
127
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
128
"directory is writable by other");
129
debug_return_int(true);
130
}
131
132
/* Group writable? */
133
if (ISSET(sb->st_mode, S_IWGRP)) {
134
if (group_matches(sb->st_gid, user_cred)) {
135
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
136
"directory is writable by one of the user's groups");
137
debug_return_int(true);
138
}
139
}
140
141
errno = EACCES;
142
debug_return_int(false);
143
}
144
145
#if defined(HAVE_FACCESSAT) && defined(AT_EACCESS)
146
/*
147
* Checks whether the open directory dfd is owned or writable by the user.
148
* Returns true if writable, false if not, or -1 on error.
149
*/
150
int
151
dir_is_writable(int dfd, const struct sudo_cred *user_cred,
152
const struct sudo_cred *cur_cred)
153
{
154
struct stat sb;
155
int rc;
156
debug_decl(dir_is_writable, SUDO_DEBUG_EDIT);
157
158
if (fstat(dfd, &sb) == -1)
159
debug_return_int(-1);
160
161
/* If the user owns the dir we always consider it writable. */
162
if (sb.st_uid == user_cred->uid) {
163
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
164
"user uid %u matches directory uid %u",
165
(unsigned int)user_cred->uid, (unsigned int)sb.st_uid);
166
debug_return_int(true);
167
}
168
169
/* Change uid/gid/groups to invoking user, usually needs root perms. */
170
if (cur_cred->euid != ROOT_UID) {
171
if (seteuid(ROOT_UID) != 0) {
172
sudo_debug_printf(
173
SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
174
"seteuid(ROOT_UID)");
175
goto fallback;
176
}
177
}
178
if (switch_user_nonfatal(user_cred->uid, user_cred->gid, user_cred->ngroups,
179
user_cred->groups) == -1) {
180
sudo_debug_printf(
181
SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
182
"unable to switch to user_cred");
183
goto fallback;
184
}
185
186
/* Access checks are done using the euid/egid and group vector. */
187
rc = faccessat(dfd, ".", W_OK, AT_EACCESS);
188
189
/* Restore uid/gid/groups, may need root perms. */
190
if (user_cred->uid != ROOT_UID) {
191
if (seteuid(ROOT_UID) != 0)
192
sudo_fatal("seteuid(ROOT_UID)");
193
}
194
switch_user(cur_cred->euid, cur_cred->egid, cur_cred->ngroups,
195
cur_cred->groups);
196
197
if (rc == 0)
198
debug_return_int(true);
199
if (errno == EACCES || errno == EPERM || errno == EROFS)
200
debug_return_int(false);
201
debug_return_int(-1);
202
203
fallback:
204
debug_return_int(is_writable(user_cred, &sb));
205
}
206
#endif /* HAVE_FACCESSAT && AT_EACCESS */
207
208
#if !defined(HAVE_FACCESSAT) || !defined(AT_EACCESS)
209
/*
210
* Checks whether the open directory dfd is owned or writable by the user.
211
* Returns true if writable, false if not, or -1 on error.
212
*/
213
int
214
dir_is_writable(int dfd, const struct sudo_cred *user_cred,
215
const struct sudo_cred *cur_cred)
216
{
217
struct stat sb;
218
debug_decl(dir_is_writable, SUDO_DEBUG_EDIT);
219
220
if (fstat(dfd, &sb) == -1)
221
debug_return_int(-1);
222
223
/* If the user owns the dir we always consider it writable. */
224
if (sb.st_uid == user_cred->uid) {
225
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
226
"user uid %u matches directory uid %u",
227
(unsigned int)user_cred->uid, (unsigned int)sb.st_uid);
228
debug_return_int(true);
229
}
230
231
debug_return_int(is_writable(user_cred, &sb));
232
}
233
#endif /* HAVE_FACCESSAT && AT_EACCESS */
234
235
#ifdef O_NOFOLLOW
236
static int
237
sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode)
238
{
239
int fd;
240
debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT);
241
242
fd = openat(dfd, path, oflags|O_NOFOLLOW, mode);
243
if (fd == -1) {
244
/* Handle non-standard O_NOFOLLOW errno values. */
245
if (errno == EMLINK)
246
errno = ELOOP; /* FreeBSD */
247
#ifdef EFTYPE
248
else if (errno == EFTYPE)
249
errno = ELOOP; /* NetBSD */
250
#endif
251
}
252
253
debug_return_int(fd);
254
}
255
#else
256
/*
257
* Returns true if fd and path don't match or path is a symlink.
258
* Used on older systems without O_NOFOLLOW.
259
*/
260
static bool
261
sudo_edit_is_symlink(int fd, char *path)
262
{
263
struct stat sb1, sb2;
264
debug_decl(sudo_edit_is_symlink, SUDO_DEBUG_EDIT);
265
266
/*
267
* Treat [fl]stat() failure like there was a symlink.
268
*/
269
if (fstat(fd, &sb1) == -1 || lstat(path, &sb2) == -1)
270
debug_return_bool(true);
271
272
/*
273
* Make sure we did not open a link and that what we opened
274
* matches what is currently on the file system.
275
*/
276
if (S_ISLNK(sb2.st_mode) ||
277
sb1.st_dev != sb2.st_dev || sb1.st_ino != sb2.st_ino) {
278
debug_return_bool(true);
279
}
280
281
debug_return_bool(false);
282
}
283
284
static int
285
sudo_edit_openat_nofollow(int dfd, char *path, int oflags, mode_t mode)
286
{
287
int fd = -1, odfd = -1;
288
struct stat sb;
289
debug_decl(sudo_edit_openat_nofollow, SUDO_DEBUG_EDIT);
290
291
/* Save cwd and chdir to dfd (cannot use O_PATH on older Linux kernels). */
292
if ((odfd = open(".", O_RDONLY|DIRECTORY)) == -1)
293
debug_return_int(-1);
294
if (fchdir(dfd) == -1) {
295
close(odfd);
296
debug_return_int(-1);
297
}
298
299
/*
300
* Check if path is a symlink. This is racey but we detect whether
301
* we lost the race in sudo_edit_is_symlink() after the open.
302
*/
303
if (lstat(path, &sb) == -1 && errno != ENOENT)
304
goto done;
305
if (S_ISLNK(sb.st_mode)) {
306
errno = ELOOP;
307
goto done;
308
}
309
310
fd = open(path, oflags, mode);
311
if (fd == -1)
312
goto done;
313
314
/*
315
* Post-open symlink check. This will leave a zero-length file if
316
* O_CREAT was specified but it is too dangerous to try and remove it.
317
*/
318
if (sudo_edit_is_symlink(fd, path)) {
319
close(fd);
320
fd = -1;
321
errno = ELOOP;
322
}
323
324
done:
325
/* Restore cwd */
326
if (odfd != -1) {
327
if (fchdir(odfd) == -1)
328
sudo_fatal("%s", U_("unable to restore current working directory"));
329
close(odfd);
330
}
331
332
debug_return_int(fd);
333
}
334
#endif /* O_NOFOLLOW */
335
336
static int
337
sudo_edit_open_nonwritable(char *path, int oflags, mode_t mode,
338
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
339
{
340
const int dflags = DIR_OPEN_FLAGS;
341
int dfd, fd, writable;
342
debug_decl(sudo_edit_open_nonwritable, SUDO_DEBUG_EDIT);
343
344
if (path[0] == '/') {
345
dfd = open("/", dflags);
346
path++;
347
} else {
348
dfd = open(".", dflags);
349
if (path[0] == '.' && path[1] == '/')
350
path += 2;
351
}
352
if (dfd == -1)
353
debug_return_int(-1);
354
355
for (;;) {
356
char *slash;
357
int subdfd;
358
359
/*
360
* Look up one component at a time, avoiding symbolic links in
361
* writable directories.
362
*/
363
writable = dir_is_writable(dfd, user_cred, cur_cred);
364
if (writable == -1) {
365
close(dfd);
366
debug_return_int(-1);
367
}
368
369
path += strspn(path, "/");
370
slash = strchr(path, '/');
371
if (slash == NULL)
372
break;
373
*slash = '\0';
374
if (writable)
375
subdfd = sudo_edit_openat_nofollow(dfd, path, dflags, 0);
376
else
377
subdfd = openat(dfd, path, dflags, 0);
378
*slash = '/'; /* restore path */
379
close(dfd);
380
if (subdfd == -1)
381
debug_return_int(-1);
382
path = slash + 1;
383
dfd = subdfd;
384
}
385
386
if (writable) {
387
close(dfd);
388
errno = EISDIR;
389
debug_return_int(-1);
390
}
391
392
/*
393
* For "sudoedit /" we will receive ENOENT from openat() and sudoedit
394
* will try to create a file with an empty name. We treat an empty
395
* path as the cwd so sudoedit can give a sensible error message.
396
*/
397
fd = openat(dfd, *path ? path : ".", oflags, mode);
398
close(dfd);
399
debug_return_int(fd);
400
}
401
402
#ifdef O_NOFOLLOW
403
int
404
sudo_edit_open(char *path, int oflags, mode_t mode, unsigned int sflags,
405
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
406
{
407
int fd;
408
debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT);
409
410
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW))
411
oflags |= O_NOFOLLOW;
412
if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_cred->uid != ROOT_UID) {
413
fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode,
414
user_cred, cur_cred);
415
} else {
416
fd = open(path, oflags|O_NONBLOCK, mode);
417
}
418
if (fd == -1 && ISSET(oflags, O_NOFOLLOW)) {
419
/* Handle non-standard O_NOFOLLOW errno values. */
420
if (errno == EMLINK)
421
errno = ELOOP; /* FreeBSD */
422
#ifdef EFTYPE
423
else if (errno == EFTYPE)
424
errno = ELOOP; /* NetBSD */
425
#endif
426
}
427
if (fd != -1 && !ISSET(oflags, O_NONBLOCK))
428
(void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
429
debug_return_int(fd);
430
}
431
#else
432
int
433
sudo_edit_open(char *path, int oflags, mode_t mode, unsigned int sflags,
434
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
435
{
436
struct stat sb;
437
int fd;
438
debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT);
439
440
/*
441
* Check if path is a symlink. This is racey but we detect whether
442
* we lost the race in sudo_edit_is_symlink() after the file is opened.
443
*/
444
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) {
445
if (lstat(path, &sb) == -1 && errno != ENOENT)
446
debug_return_int(-1);
447
if (S_ISLNK(sb.st_mode)) {
448
errno = ELOOP;
449
debug_return_int(-1);
450
}
451
}
452
453
if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_cred->uid != ROOT_UID) {
454
fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode,
455
user_cred, cur_cred);
456
} else {
457
fd = open(path, oflags|O_NONBLOCK, mode);
458
}
459
if (fd == -1)
460
debug_return_int(-1);
461
if (!ISSET(oflags, O_NONBLOCK))
462
(void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
463
464
/*
465
* Post-open symlink check. This will leave a zero-length file if
466
* O_CREAT was specified but it is too dangerous to try and remove it.
467
*/
468
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW) && sudo_edit_is_symlink(fd, path)) {
469
close(fd);
470
fd = -1;
471
errno = ELOOP;
472
}
473
474
debug_return_int(fd);
475
}
476
#endif /* O_NOFOLLOW */
477
478
/*
479
* Verify that the parent dir of a new file exists and is not writable
480
* by the user. This fails early so the user knows ahead of time if the
481
* edit won't succeed. Additional checks are performed when copying the
482
* temporary file back to the origin so there are no TOCTOU issues.
483
* Does not modify the value of errno.
484
*/
485
bool
486
sudo_edit_parent_valid(char *path, unsigned int sflags,
487
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
488
{
489
const int serrno = errno;
490
struct stat sb;
491
bool ret = false;
492
char *slash;
493
char pathbuf[2];
494
int dfd;
495
debug_decl(sudo_edit_parent_valid, SUDO_DEBUG_EDIT);
496
497
/* Get dirname of path (the slash is restored later). */
498
slash = strrchr(path, '/');
499
if (slash == NULL) {
500
/* cwd */
501
pathbuf[0] = '.';
502
pathbuf[1] = '\0';
503
path = pathbuf;
504
} else if (slash == path) {
505
pathbuf[0] = '/';
506
pathbuf[1] = '\0';
507
path = pathbuf;
508
slash = NULL;
509
} else {
510
*slash = '\0';
511
}
512
513
/*
514
* The parent directory is allowed to be a symbolic link unless
515
* *its* parent is writable and CD_SUDOEDIT_CHECK is set.
516
*/
517
dfd = sudo_edit_open(path, DIR_OPEN_FLAGS, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH,
518
sflags|CD_SUDOEDIT_FOLLOW, user_cred, cur_cred);
519
if (dfd != -1) {
520
if (fstat(dfd, &sb) == 0 && S_ISDIR(sb.st_mode))
521
ret = true;
522
close(dfd);
523
}
524
if (slash != NULL)
525
*slash = '/';
526
527
/* Restore errno. */
528
errno = serrno;
529
530
debug_return_bool(ret);
531
}
532
533
#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */
534
535