Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sudo-project
GitHub Repository: sudo-project/sudo
Path: blob/main/src/edit_open.c
3875 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_int, 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_bool(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_bool(true);
138
}
139
}
140
141
errno = EACCES;
142
debug_return_bool(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
#if defined(HAVE_DECL_O_NOFOLLOW) && HAVE_DECL_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) {
304
if (errno != ENOENT)
305
goto done;
306
} else if (S_ISLNK(sb.st_mode)) {
307
errno = ELOOP;
308
goto done;
309
}
310
311
fd = open(path, oflags, mode);
312
if (fd == -1)
313
goto done;
314
315
/*
316
* Post-open symlink check. This will leave a zero-length file if
317
* O_CREAT was specified but it is too dangerous to try and remove it.
318
*/
319
if (sudo_edit_is_symlink(fd, path)) {
320
close(fd);
321
fd = -1;
322
errno = ELOOP;
323
}
324
325
done:
326
/* Restore cwd */
327
if (odfd != -1) {
328
if (fchdir(odfd) == -1)
329
sudo_fatal("%s", U_("unable to restore current working directory"));
330
close(odfd);
331
}
332
333
debug_return_int(fd);
334
}
335
#endif /* HAVE_DECL_O_NOFOLLOW */
336
337
static int
338
sudo_edit_open_nonwritable(char *path, int oflags, mode_t mode,
339
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
340
{
341
const int dflags = DIR_OPEN_FLAGS;
342
int dfd, fd, writable;
343
debug_decl(sudo_edit_open_nonwritable, SUDO_DEBUG_EDIT);
344
345
if (path[0] == '/') {
346
dfd = open("/", dflags);
347
path++;
348
} else {
349
dfd = open(".", dflags);
350
if (path[0] == '.' && path[1] == '/')
351
path += 2;
352
}
353
if (dfd == -1)
354
debug_return_int(-1);
355
356
for (;;) {
357
char *slash;
358
int subdfd;
359
360
/*
361
* Look up one component at a time, avoiding symbolic links in
362
* writable directories.
363
*/
364
writable = dir_is_writable(dfd, user_cred, cur_cred);
365
if (writable == -1) {
366
close(dfd);
367
debug_return_int(-1);
368
}
369
370
path += strspn(path, "/");
371
slash = strchr(path, '/');
372
if (slash == NULL)
373
break;
374
*slash = '\0';
375
if (writable)
376
subdfd = sudo_edit_openat_nofollow(dfd, path, dflags, 0);
377
else
378
subdfd = openat(dfd, path, dflags, 0);
379
*slash = '/'; /* restore path */
380
close(dfd);
381
if (subdfd == -1)
382
debug_return_int(-1);
383
path = slash + 1;
384
dfd = subdfd;
385
}
386
387
if (writable) {
388
close(dfd);
389
errno = EISDIR;
390
debug_return_int(-1);
391
}
392
393
/*
394
* For "sudoedit /" we will receive ENOENT from openat() and sudoedit
395
* will try to create a file with an empty name. We treat an empty
396
* path as the cwd so sudoedit can give a sensible error message.
397
*/
398
fd = openat(dfd, *path ? path : ".", oflags, mode);
399
close(dfd);
400
debug_return_int(fd);
401
}
402
403
#if defined(HAVE_DECL_O_NOFOLLOW) && HAVE_DECL_O_NOFOLLOW
404
int
405
sudo_edit_open(char *path, int oflags, mode_t mode, unsigned int sflags,
406
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
407
{
408
int fd;
409
debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT);
410
411
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW))
412
oflags |= O_NOFOLLOW;
413
if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_cred->uid != ROOT_UID) {
414
fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode,
415
user_cred, cur_cred);
416
} else {
417
fd = open(path, oflags|O_NONBLOCK, mode);
418
}
419
if (fd == -1 && ISSET(oflags, O_NOFOLLOW)) {
420
/* Handle non-standard O_NOFOLLOW errno values. */
421
if (errno == EMLINK)
422
errno = ELOOP; /* FreeBSD */
423
#ifdef EFTYPE
424
else if (errno == EFTYPE)
425
errno = ELOOP; /* NetBSD */
426
#endif
427
}
428
if (fd != -1 && !ISSET(oflags, O_NONBLOCK))
429
(void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
430
debug_return_int(fd);
431
}
432
#else
433
int
434
sudo_edit_open(char *path, int oflags, mode_t mode, unsigned int sflags,
435
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
436
{
437
struct stat sb;
438
int fd;
439
debug_decl(sudo_edit_open, SUDO_DEBUG_EDIT);
440
441
/*
442
* Check if path is a symlink. This is racey but we detect whether
443
* we lost the race in sudo_edit_is_symlink() after the file is opened.
444
*/
445
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW)) {
446
if (lstat(path, &sb) == -1 && errno != ENOENT)
447
debug_return_int(-1);
448
if (S_ISLNK(sb.st_mode)) {
449
errno = ELOOP;
450
debug_return_int(-1);
451
}
452
}
453
454
if (ISSET(sflags, CD_SUDOEDIT_CHECKDIR) && user_cred->uid != ROOT_UID) {
455
fd = sudo_edit_open_nonwritable(path, oflags|O_NONBLOCK, mode,
456
user_cred, cur_cred);
457
} else {
458
fd = open(path, oflags|O_NONBLOCK, mode);
459
}
460
if (fd == -1)
461
debug_return_int(-1);
462
if (!ISSET(oflags, O_NONBLOCK))
463
(void) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
464
465
/*
466
* Post-open symlink check. This will leave a zero-length file if
467
* O_CREAT was specified but it is too dangerous to try and remove it.
468
*/
469
if (!ISSET(sflags, CD_SUDOEDIT_FOLLOW) && sudo_edit_is_symlink(fd, path)) {
470
close(fd);
471
fd = -1;
472
errno = ELOOP;
473
}
474
475
debug_return_int(fd);
476
}
477
#endif /* HAVE_DECL_O_NOFOLLOW */
478
479
/*
480
* Verify that the parent dir of a new file exists and is not writable
481
* by the user. This fails early so the user knows ahead of time if the
482
* edit won't succeed. Additional checks are performed when copying the
483
* temporary file back to the origin so there are no TOCTOU issues.
484
* Does not modify the value of errno.
485
*/
486
bool
487
sudo_edit_parent_valid(char *path, unsigned int sflags,
488
const struct sudo_cred *user_cred, const struct sudo_cred *cur_cred)
489
{
490
const int serrno = errno;
491
struct stat sb;
492
bool ret = false;
493
char *slash;
494
char pathbuf[2];
495
int dfd;
496
debug_decl(sudo_edit_parent_valid, SUDO_DEBUG_EDIT);
497
498
/* Get dirname of path (the slash is restored later). */
499
slash = strrchr(path, '/');
500
if (slash == NULL) {
501
/* cwd */
502
pathbuf[0] = '.';
503
pathbuf[1] = '\0';
504
path = pathbuf;
505
} else if (slash == path) {
506
pathbuf[0] = '/';
507
pathbuf[1] = '\0';
508
path = pathbuf;
509
slash = NULL;
510
} else {
511
*slash = '\0';
512
}
513
514
/*
515
* The parent directory is allowed to be a symbolic link unless
516
* *its* parent is writable and CD_SUDOEDIT_CHECK is set.
517
*/
518
dfd = sudo_edit_open(path, DIR_OPEN_FLAGS, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH,
519
sflags|CD_SUDOEDIT_FOLLOW, user_cred, cur_cred);
520
if (dfd != -1) {
521
if (fstat(dfd, &sb) == 0 && S_ISDIR(sb.st_mode))
522
ret = true;
523
close(dfd);
524
}
525
if (slash != NULL)
526
*slash = '/';
527
528
/* Restore errno. */
529
errno = serrno;
530
531
debug_return_bool(ret);
532
}
533
534
#endif /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */
535
536