-
Notifications
You must be signed in to change notification settings - Fork 482
Expand file tree
/
Copy patht_chmod_secure.c
More file actions
183 lines (166 loc) · 5.92 KB
/
t_chmod_secure.c
File metadata and controls
183 lines (166 loc) · 5.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
* Test harness for do_chmod_at(). Confirms the symlink-TOCTOU
* primitive used by CVE-2026-29518 (and its incomplete-fix follow-up
* for chmod) is closed by do_chmod_at(): a parent directory component
* being a symlink that escapes the receiver's confinement must be
* rejected, while a parent symlink that resolves *within* the tree
* must still work (so legitimate dir-symlinks are not regressed).
*
* Not linked into rsync itself.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include "rsync.h"
#include <sys/stat.h>
#ifdef __linux__
#include <sys/syscall.h>
#include <linux/openat2.h>
#endif
int dry_run = 0;
int am_root = 0;
int am_sender = 0;
int read_only = 0;
int list_only = 0;
int copy_links = 0;
int copy_unsafe_links = 0;
extern int am_daemon, am_chrooted;
short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
static int errs = 0;
/* Probe the running kernel for the RESOLVE_BENEATH-equivalent confinement
* that secure_relative_open() prefers over the per-component O_NOFOLLOW
* walk. Returns 1 if either openat2(RESOLVE_BENEATH) on Linux 5.6+ or
* openat(O_RESOLVE_BENEATH) on FreeBSD 13+ / macOS 15+ is honoured by
* the running kernel, 0 otherwise. The probe opens "." (a directory
* the helper has just chdir'd into) so it can't fail for any reason
* other than the kernel rejecting the requested confinement flag. */
static int kernel_resolve_beneath_supported(void)
{
int fd;
#ifdef __linux__
{
struct open_how how;
memset(&how, 0, sizeof how);
how.flags = O_RDONLY | O_DIRECTORY;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
if (fd >= 0) {
close(fd);
return 1;
}
/* ENOSYS = kernel < 5.6. Fall through to the O_RESOLVE_BENEATH
* probe in case we're a Linux build running on a kernel that
* gained O_RESOLVE_BENEATH via some out-of-tree backport. */
}
#endif
#ifdef O_RESOLVE_BENEATH
fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY | O_RESOLVE_BENEATH);
if (fd >= 0) {
close(fd);
return 1;
}
#endif
return 0;
}
static void check(const char *label, int actual_rc, int expect_ok,
const char *path, mode_t expected_mode)
{
struct stat st;
int got_ok = (actual_rc == 0);
if (got_ok != expect_ok) {
fprintf(stderr, "FAIL [%s]: rc=%d errno=%d (%s), expected %s\n",
label, actual_rc, errno, strerror(errno),
expect_ok ? "success" : "rejection");
errs++;
return;
}
if (path && stat(path, &st) < 0) {
fprintf(stderr, "FAIL [%s]: stat(%s) failed: %s\n",
label, path, strerror(errno));
errs++;
return;
}
if (path && (st.st_mode & 07777) != expected_mode) {
fprintf(stderr,
"FAIL [%s]: %s mode is 0%o, expected 0%o\n",
label, path, st.st_mode & 07777, expected_mode);
errs++;
return;
}
fprintf(stderr, "OK [%s]\n", label);
}
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "usage: %s <module-dir>\n", argv[0]);
return 2;
}
if (chdir(argv[1]) < 0) {
perror("chdir");
return 2;
}
/* Simulate the daemon-without-chroot deployment that do_chmod_at()
* defends. With am_daemon=0 or am_chrooted=1 the wrapper falls
* through to plain do_chmod() and the symlink-race test would be
* meaningless. */
am_daemon = 1;
am_chrooted = 0;
/* Test layout (all inside the directory we just chdir'd to):
*
* ./realdir/sentinel -- regular target file
* ./inside_link -> realdir -- legitimate dir-symlink within the tree
* ./escape_link -> ../trap -- attacker swap, target outside tree
* ../trap/sentinel -- the file the attacker wants to alter
*
* The shell wrapper that calls this helper has set both sentinel
* files to mode 0600 so we have a clean baseline to compare.
*/
/* Scenario A: legitimate parent dir-symlink.
*
* On platforms whose kernel offers RESOLVE_BENEATH-equivalent
* confinement (Linux 5.6+ openat2, FreeBSD 13+ / macOS 15+
* O_RESOLVE_BENEATH), the within-tree symlink is followed and
* the chmod must succeed.
*
* On platforms that fall back to the per-component O_NOFOLLOW
* walk (OpenBSD, NetBSD, Solaris, older Cygwin, HPE NonStop,
* and pre-5.6 Linux), every symlink is rejected -- including
* this legitimate one. That's a real platform limitation (the
* same one that causes the #715 regression there) and the
* expected outcome is rejection.
*
* Detect at runtime and expect accordingly. The other three
* scenarios behave identically on both code paths and need no
* adjustment. */
int kernel_has_rb = kernel_resolve_beneath_supported();
fprintf(stderr, "INFO: kernel RESOLVE_BENEATH-equivalent confinement: %s\n",
kernel_has_rb ? "available" : "not available (per-component fallback)");
int rc = do_chmod_at("inside_link/sentinel", 0640);
if (kernel_has_rb) {
check("A: legit dir-symlink within tree (kernel confined)",
rc, 1, "realdir/sentinel", 0640);
} else {
check("A: legit dir-symlink within tree (per-component fallback rejects)",
rc, 0, "realdir/sentinel", 0600);
}
/* Scenario B: parent symlink escapes the tree -- chmod must be
* rejected and the outside file's mode must be unchanged. */
rc = do_chmod_at("escape_link/sentinel", 0666);
check("B: parent symlink escapes tree (the attack)",
rc, 0, "../trap/sentinel", 0600);
/* Scenario C: plain relative path with no symlink components,
* regression check that the safe wrapper doesn't break the
* normal case. */
rc = do_chmod_at("realdir/sentinel", 0644);
check("C: plain relative path (regression check)",
rc, 1, "realdir/sentinel", 0644);
/* Scenario D: top-level file, no parent directory component.
* Falls back to do_chmod(); should succeed. */
rc = do_chmod_at("topfile", 0640);
check("D: top-level file, no parent component",
rc, 1, "topfile", 0640);
if (errs)
fprintf(stderr, "%d failure(s)\n", errs);
return errs ? 1 : 0;
}