]>
Commit | Line | Data |
---|---|---|
47136d6c | 1 | /* Tests for lchmod and fchmodat with AT_SYMLINK_NOFOLLOW. |
dff8da6b | 2 | Copyright (C) 2020-2024 Free Software Foundation, Inc. |
47136d6c FW |
3 | This file is part of the GNU C Library. |
4 | ||
5 | The GNU C Library is free software; you can redistribute it and/or | |
6 | modify it under the terms of the GNU Lesser General Public | |
7 | License as published by the Free Software Foundation; either | |
8 | version 2.1 of the License, or (at your option) any later version. | |
9 | ||
10 | The GNU C Library is distributed in the hope that it will be useful, | |
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 | Lesser General Public License for more details. | |
14 | ||
15 | You should have received a copy of the GNU Lesser General Public | |
16 | License along with the GNU C Library; if not, see | |
17 | <https://www.gnu.org/licenses/>. */ | |
18 | ||
19 | #include <array_length.h> | |
20 | #include <errno.h> | |
21 | #include <fcntl.h> | |
22 | #include <stdbool.h> | |
23 | #include <stdio.h> | |
24 | #include <stdlib.h> | |
25 | #include <string.h> | |
26 | #include <support/check.h> | |
27 | #include <support/descriptors.h> | |
28 | #include <support/namespace.h> | |
29 | #include <support/support.h> | |
30 | #include <support/temp_file.h> | |
31 | #include <support/xunistd.h> | |
32 | #include <unistd.h> | |
33 | ||
34 | #if __has_include (<sys/mount.h>) | |
35 | # include <sys/mount.h> | |
36 | #endif | |
37 | ||
38 | /* Array of file descriptors. */ | |
39 | #define DYNARRAY_STRUCT fd_list | |
40 | #define DYNARRAY_ELEMENT int | |
41 | #define DYNARRAY_INITIAL_SIZE 0 | |
42 | #define DYNARRAY_PREFIX fd_list_ | |
43 | #include <malloc/dynarray-skeleton.c> | |
44 | ||
45 | static int | |
46 | fchmodat_with_lchmod (int fd, const char *path, mode_t mode, int flags) | |
47 | { | |
48 | TEST_COMPARE (fd, AT_FDCWD); | |
49 | if (flags == 0) | |
50 | return chmod (path, mode); | |
51 | else | |
52 | { | |
53 | TEST_COMPARE (flags, AT_SYMLINK_NOFOLLOW); | |
54 | return lchmod (path, mode); | |
55 | } | |
56 | } | |
57 | ||
58 | /* Chose the appropriate path to pass as the path argument to the *at | |
59 | functions. */ | |
60 | static const char * | |
61 | select_path (bool do_relative_path, const char *full_path, const char *relative_path) | |
62 | { | |
63 | if (do_relative_path) | |
64 | return relative_path; | |
65 | else | |
66 | return full_path; | |
67 | } | |
68 | ||
118a2aee AZ |
69 | static void |
70 | update_file_time_to_y2038 (const char *fname, int flags) | |
71 | { | |
72 | #ifdef CHECK_TIME64 | |
73 | /* Y2038 threshold plus 1 second. */ | |
74 | const struct timespec ts[] = { { 0x80000001LL, 0}, { 0x80000001LL } }; | |
75 | TEST_VERIFY_EXIT (utimensat (AT_FDCWD, fname, ts, flags) == 0); | |
76 | #endif | |
77 | } | |
78 | ||
47136d6c FW |
79 | static void |
80 | test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t, int)) | |
81 | { | |
82 | char *tempdir = support_create_temp_directory ("tst-lchmod-"); | |
118a2aee AZ |
83 | #ifdef CHECK_TIME64 |
84 | if (!support_path_support_time64 (tempdir)) | |
85 | { | |
86 | puts ("info: test skipped, filesystem does not support 64 bit time_t"); | |
87 | return; | |
88 | } | |
89 | #endif | |
47136d6c FW |
90 | |
91 | char *path_dangling = xasprintf ("%s/dangling", tempdir); | |
92 | char *path_file = xasprintf ("%s/file", tempdir); | |
93 | char *path_loop = xasprintf ("%s/loop", tempdir); | |
94 | char *path_missing = xasprintf ("%s/missing", tempdir); | |
95 | char *path_to_file = xasprintf ("%s/to-file", tempdir); | |
96 | ||
97 | int fd; | |
98 | if (do_relative_path) | |
99 | fd = xopen (tempdir, O_DIRECTORY | O_RDONLY, 0); | |
100 | else | |
101 | fd = AT_FDCWD; | |
102 | ||
103 | add_temp_file (path_dangling); | |
104 | add_temp_file (path_loop); | |
105 | add_temp_file (path_file); | |
106 | add_temp_file (path_to_file); | |
107 | ||
108 | support_write_file_string (path_file, ""); | |
109 | xsymlink ("file", path_to_file); | |
110 | xsymlink ("loop", path_loop); | |
111 | xsymlink ("target-does-not-exist", path_dangling); | |
112 | ||
118a2aee AZ |
113 | update_file_time_to_y2038 (path_file, 0); |
114 | update_file_time_to_y2038 (path_to_file, AT_SYMLINK_NOFOLLOW); | |
115 | ||
47136d6c FW |
116 | /* Check that the modes do not collide with what we will use in the |
117 | test. */ | |
118a2aee | 118 | struct stat st; |
47136d6c FW |
119 | xstat (path_file, &st); |
120 | TEST_VERIFY ((st.st_mode & 0777) != 1); | |
121 | xlstat (path_to_file, &st); | |
122 | TEST_VERIFY ((st.st_mode & 0777) != 2); | |
123 | mode_t original_symlink_mode = st.st_mode; | |
124 | ||
47136d6c FW |
125 | /* We should be able to change the mode of a file, including through |
126 | the symbolic link to-file. */ | |
127 | const char *arg = select_path (do_relative_path, path_file, "file"); | |
128 | TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0); | |
129 | xstat (path_file, &st); | |
130 | TEST_COMPARE (st.st_mode & 0777, 1); | |
a492b1e5 FW |
131 | arg = select_path (do_relative_path, path_to_file, "to-file"); |
132 | TEST_COMPARE (chmod_func (fd, arg, 2, 0), 0); | |
47136d6c FW |
133 | xstat (path_file, &st); |
134 | TEST_COMPARE (st.st_mode & 0777, 2); | |
a492b1e5 FW |
135 | xlstat (path_to_file, &st); |
136 | TEST_COMPARE (original_symlink_mode, st.st_mode); | |
137 | arg = select_path (do_relative_path, path_file, "file"); | |
138 | TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0); | |
47136d6c FW |
139 | xstat (path_file, &st); |
140 | TEST_COMPARE (st.st_mode & 0777, 1); | |
141 | xlstat (path_to_file, &st); | |
142 | TEST_COMPARE (original_symlink_mode, st.st_mode); | |
143 | ||
a492b1e5 | 144 | /* Changing the mode of a symbolic link should fail. */ |
47136d6c | 145 | arg = select_path (do_relative_path, path_to_file, "to-file"); |
a492b1e5 FW |
146 | int ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW); |
147 | TEST_COMPARE (ret, -1); | |
148 | TEST_COMPARE (errno, EOPNOTSUPP); | |
47136d6c | 149 | |
a492b1e5 FW |
150 | /* The modes should remain unchanged. */ |
151 | xstat (path_file, &st); | |
152 | TEST_COMPARE (st.st_mode & 0777, 1); | |
153 | xlstat (path_to_file, &st); | |
154 | TEST_COMPARE (original_symlink_mode, st.st_mode); | |
47136d6c | 155 | |
a492b1e5 FW |
156 | /* Likewise, changing dangling and looping symbolic links must |
157 | fail. */ | |
47136d6c FW |
158 | const char *paths[] = { path_dangling, path_loop }; |
159 | for (size_t i = 0; i < array_length (paths); ++i) | |
160 | { | |
161 | const char *path = paths[i]; | |
162 | const char *filename = strrchr (path, '/'); | |
163 | TEST_VERIFY_EXIT (filename != NULL); | |
164 | ++filename; | |
165 | mode_t new_mode = 010 + i; | |
166 | ||
167 | xlstat (path, &st); | |
168 | TEST_VERIFY ((st.st_mode & 0777) != new_mode); | |
169 | original_symlink_mode = st.st_mode; | |
170 | arg = select_path (do_relative_path, path, filename); | |
171 | ret = chmod_func (fd, arg, new_mode, AT_SYMLINK_NOFOLLOW); | |
a492b1e5 FW |
172 | TEST_COMPARE (ret, -1); |
173 | TEST_COMPARE (errno, EOPNOTSUPP); | |
174 | xlstat (path, &st); | |
175 | TEST_COMPARE (st.st_mode, original_symlink_mode); | |
47136d6c FW |
176 | } |
177 | ||
178 | /* A missing file should always result in ENOENT. The presence of | |
179 | /proc does not matter. */ | |
180 | arg = select_path (do_relative_path, path_missing, "missing"); | |
181 | TEST_COMPARE (chmod_func (fd, arg, 020, 0), -1); | |
182 | TEST_COMPARE (errno, ENOENT); | |
183 | TEST_COMPARE (chmod_func (fd, arg, 020, AT_SYMLINK_NOFOLLOW), -1); | |
184 | TEST_COMPARE (errno, ENOENT); | |
185 | ||
186 | /* Test without available file descriptors. */ | |
187 | { | |
188 | struct fd_list fd_list; | |
189 | fd_list_init (&fd_list); | |
190 | while (true) | |
191 | { | |
192 | int ret = dup (STDOUT_FILENO); | |
193 | if (ret == -1) | |
194 | { | |
195 | if (errno == ENFILE || errno == EMFILE) | |
196 | break; | |
197 | FAIL_EXIT1 ("dup: %m"); | |
198 | } | |
199 | fd_list_add (&fd_list, ret); | |
200 | TEST_VERIFY_EXIT (!fd_list_has_failed (&fd_list)); | |
201 | } | |
202 | /* Without AT_SYMLINK_NOFOLLOW, changing the permissions should | |
203 | work as before. */ | |
204 | arg = select_path (do_relative_path, path_file, "file"); | |
205 | TEST_COMPARE (chmod_func (fd, arg, 3, 0), 0); | |
206 | xstat (path_file, &st); | |
207 | TEST_COMPARE (st.st_mode & 0777, 3); | |
208 | /* But with AT_SYMLINK_NOFOLLOW, even if we originally had | |
209 | support, we may have lost it. */ | |
210 | ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW); | |
211 | if (ret == 0) | |
212 | { | |
213 | xstat (path_file, &st); | |
214 | TEST_COMPARE (st.st_mode & 0777, 2); | |
215 | } | |
216 | else | |
217 | { | |
218 | TEST_COMPARE (ret, -1); | |
219 | /* The error code from the openat fallback leaks out. */ | |
220 | if (errno != ENFILE && errno != EMFILE) | |
221 | TEST_COMPARE (errno, EOPNOTSUPP); | |
65341f7b AZ |
222 | xstat (path_file, &st); |
223 | TEST_COMPARE (st.st_mode & 0777, 3); | |
47136d6c | 224 | } |
47136d6c FW |
225 | |
226 | /* Close the descriptors. */ | |
227 | for (int *pfd = fd_list_begin (&fd_list); pfd < fd_list_end (&fd_list); | |
228 | ++pfd) | |
229 | xclose (*pfd); | |
230 | fd_list_free (&fd_list); | |
231 | } | |
232 | ||
233 | if (do_relative_path) | |
234 | xclose (fd); | |
235 | ||
236 | free (path_dangling); | |
237 | free (path_file); | |
238 | free (path_loop); | |
239 | free (path_missing); | |
240 | free (path_to_file); | |
241 | ||
242 | free (tempdir); | |
243 | } | |
244 | ||
245 | static void | |
246 | test_3 (void) | |
247 | { | |
248 | puts ("info: testing lchmod"); | |
249 | test_1 (false, fchmodat_with_lchmod); | |
250 | puts ("info: testing fchmodat with AT_FDCWD"); | |
251 | test_1 (false, fchmodat); | |
252 | puts ("info: testing fchmodat with relative path"); | |
253 | test_1 (true, fchmodat); | |
254 | } | |
255 | ||
256 | static int | |
257 | do_test (void) | |
258 | { | |
259 | struct support_descriptors *descriptors = support_descriptors_list (); | |
260 | ||
261 | /* Run the three tests in the default environment. */ | |
262 | test_3 (); | |
263 | ||
264 | /* Try to set up a /proc-less environment and re-test. */ | |
265 | #if __has_include (<sys/mount.h>) | |
266 | if (!support_become_root ()) | |
267 | puts ("warning: could not obtain root-like privileges"); | |
268 | if (!support_enter_mount_namespace ()) | |
269 | puts ("warning: could enter a mount namespace"); | |
270 | else | |
271 | { | |
272 | /* Attempt to mount an empty directory over /proc. */ | |
273 | char *tempdir = support_create_temp_directory ("tst-lchmod-"); | |
274 | bool proc_emptied | |
275 | = mount (tempdir, "/proc", "none", MS_BIND, NULL) == 0; | |
276 | if (!proc_emptied) | |
277 | printf ("warning: bind-mounting /proc failed: %m"); | |
278 | free (tempdir); | |
279 | ||
280 | puts ("info: re-running tests (after trying to empty /proc)"); | |
281 | test_3 (); | |
282 | ||
283 | if (proc_emptied) | |
284 | /* Reveal the original /proc, which is needed by the | |
285 | descriptors check below. */ | |
286 | TEST_COMPARE (umount ("/proc"), 0); | |
287 | } | |
288 | #endif /* <sys/mount.h>. */ | |
289 | ||
290 | support_descriptors_check (descriptors); | |
291 | support_descriptors_free (descriptors); | |
292 | ||
293 | return 0; | |
294 | } | |
295 | ||
296 | #include <support/test-driver.c> |