1/* Tests for lchmod and fchmodat with AT_SYMLINK_NOFOLLOW.
2 Copyright (C) 2020-2022 Free Software Foundation, Inc.
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
45static int
46fchmodat_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 (file: path, mode: mode);
51 else
52 {
53 TEST_COMPARE (flags, AT_SYMLINK_NOFOLLOW);
54 return lchmod (file: path, mode: mode);
55 }
56}
57
58/* Chose the appropriate path to pass as the path argument to the *at
59 functions. */
60static const char *
61select_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
69static void
70update_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
79static void
80test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t, int))
81{
82 char *tempdir = support_create_temp_directory (base: "tst-lchmod-");
83#ifdef CHECK_TIME64
84 if (!support_path_support_time64 (path: tempdir))
85 {
86 puts (s: "info: test skipped, filesystem does not support 64 bit time_t");
87 return;
88 }
89#endif
90
91 char *path_dangling = xasprintf (format: "%s/dangling", tempdir);
92 char *path_file = xasprintf (format: "%s/file", tempdir);
93 char *path_loop = xasprintf (format: "%s/loop", tempdir);
94 char *path_missing = xasprintf (format: "%s/missing", tempdir);
95 char *path_to_file = xasprintf (format: "%s/to-file", tempdir);
96
97 int fd;
98 if (do_relative_path)
99 fd = xopen (path: tempdir, O_DIRECTORY | O_RDONLY, 0);
100 else
101 fd = AT_FDCWD;
102
103 add_temp_file (name: path_dangling);
104 add_temp_file (name: path_loop);
105 add_temp_file (name: path_file);
106 add_temp_file (name: path_to_file);
107
108 support_write_file_string (path: path_file, contents: "");
109 xsymlink (target: "file", linkpath: path_to_file);
110 xsymlink (target: "loop", linkpath: path_loop);
111 xsymlink (target: "target-does-not-exist", linkpath: path_dangling);
112
113 update_file_time_to_y2038 (fname: path_file, flags: 0);
114 update_file_time_to_y2038 (fname: path_to_file, AT_SYMLINK_NOFOLLOW);
115
116 /* Check that the modes do not collide with what we will use in the
117 test. */
118 struct stat st;
119 xstat (path: path_file, &st);
120 TEST_VERIFY ((st.st_mode & 0777) != 1);
121 xlstat (path: path_to_file, &st);
122 TEST_VERIFY ((st.st_mode & 0777) != 2);
123 mode_t original_symlink_mode = st.st_mode;
124
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, full_path: path_file, relative_path: "file");
128 TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0);
129 xstat (path: path_file, &st);
130 TEST_COMPARE (st.st_mode & 0777, 1);
131 arg = select_path (do_relative_path, full_path: path_to_file, relative_path: "to-file");
132 TEST_COMPARE (chmod_func (fd, arg, 2, 0), 0);
133 xstat (path: path_file, &st);
134 TEST_COMPARE (st.st_mode & 0777, 2);
135 xlstat (path: path_to_file, &st);
136 TEST_COMPARE (original_symlink_mode, st.st_mode);
137 arg = select_path (do_relative_path, full_path: path_file, relative_path: "file");
138 TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0);
139 xstat (path: path_file, &st);
140 TEST_COMPARE (st.st_mode & 0777, 1);
141 xlstat (path: path_to_file, &st);
142 TEST_COMPARE (original_symlink_mode, st.st_mode);
143
144 /* Changing the mode of a symbolic link should fail. */
145 arg = select_path (do_relative_path, full_path: path_to_file, relative_path: "to-file");
146 int ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW);
147 TEST_COMPARE (ret, -1);
148 TEST_COMPARE (errno, EOPNOTSUPP);
149
150 /* The modes should remain unchanged. */
151 xstat (path: path_file, &st);
152 TEST_COMPARE (st.st_mode & 0777, 1);
153 xlstat (path: path_to_file, &st);
154 TEST_COMPARE (original_symlink_mode, st.st_mode);
155
156 /* Likewise, changing dangling and looping symbolic links must
157 fail. */
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, full_path: path, relative_path: filename);
171 ret = chmod_func (fd, arg, new_mode, AT_SYMLINK_NOFOLLOW);
172 TEST_COMPARE (ret, -1);
173 TEST_COMPARE (errno, EOPNOTSUPP);
174 xlstat (path, &st);
175 TEST_COMPARE (st.st_mode, original_symlink_mode);
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, full_path: path_missing, relative_path: "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 (list: &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 (list: &fd_list, item: 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, full_path: path_file, relative_path: "file");
205 TEST_COMPARE (chmod_func (fd, arg, 3, 0), 0);
206 xstat (path: 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: 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);
222 }
223 xstat (path: path_file, &st);
224 TEST_COMPARE (st.st_mode & 0777, 3);
225
226 /* Close the descriptors. */
227 for (int *pfd = fd_list_begin (list: &fd_list); pfd < fd_list_end (list: &fd_list);
228 ++pfd)
229 xclose (*pfd);
230 fd_list_free (list: &fd_list);
231 }
232
233 if (do_relative_path)
234 xclose (fd);
235
236 free (ptr: path_dangling);
237 free (ptr: path_file);
238 free (ptr: path_loop);
239 free (ptr: path_missing);
240 free (ptr: path_to_file);
241
242 free (ptr: tempdir);
243}
244
245static void
246test_3 (void)
247{
248 puts (s: "info: testing lchmod");
249 test_1 (false, chmod_func: fchmodat_with_lchmod);
250 puts (s: "info: testing fchmodat with AT_FDCWD");
251 test_1 (false, chmod_func: fchmodat);
252 puts (s: "info: testing fchmodat with relative path");
253 test_1 (true, chmod_func: fchmodat);
254}
255
256static int
257do_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 (s: "warning: could not obtain root-like privileges");
268 if (!support_enter_mount_namespace ())
269 puts (s: "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 (base: "tst-lchmod-");
274 bool proc_emptied
275 = mount (special_file: tempdir, dir: "/proc", fstype: "none", MS_BIND, NULL) == 0;
276 if (!proc_emptied)
277 printf (format: "warning: bind-mounting /proc failed: %m");
278 free (ptr: tempdir);
279
280 puts (s: "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>
297

source code of glibc/io/tst-lchmod.c