1 /* Tests for lchmod and fchmodat with AT_SYMLINK_NOFOLLOW.
2    Copyright (C) 2020-2021 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 
45 static int
fchmodat_with_lchmod(int fd,const char * path,mode_t mode,int flags)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 *
select_path(bool do_relative_path,const char * full_path,const char * relative_path)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 
69 static void
test_1(bool do_relative_path,int (* chmod_func)(int fd,const char *,mode_t,int))70 test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t, int))
71 {
72   char *tempdir = support_create_temp_directory ("tst-lchmod-");
73 
74   char *path_dangling = xasprintf ("%s/dangling", tempdir);
75   char *path_file = xasprintf ("%s/file", tempdir);
76   char *path_loop = xasprintf ("%s/loop", tempdir);
77   char *path_missing = xasprintf ("%s/missing", tempdir);
78   char *path_to_file = xasprintf ("%s/to-file", tempdir);
79 
80   int fd;
81   if (do_relative_path)
82     fd = xopen (tempdir, O_DIRECTORY | O_RDONLY, 0);
83   else
84     fd = AT_FDCWD;
85 
86   add_temp_file (path_dangling);
87   add_temp_file (path_loop);
88   add_temp_file (path_file);
89   add_temp_file (path_to_file);
90 
91   support_write_file_string (path_file, "");
92   xsymlink ("file", path_to_file);
93   xsymlink ("loop", path_loop);
94   xsymlink ("target-does-not-exist", path_dangling);
95 
96   /* Check that the modes do not collide with what we will use in the
97      test.  */
98   struct stat64 st;
99   xstat (path_file, &st);
100   TEST_VERIFY ((st.st_mode & 0777) != 1);
101   xlstat (path_to_file, &st);
102   TEST_VERIFY ((st.st_mode & 0777) != 2);
103   mode_t original_symlink_mode = st.st_mode;
104 
105   /* We should be able to change the mode of a file, including through
106      the symbolic link to-file.  */
107   const char *arg = select_path (do_relative_path, path_file, "file");
108   TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0);
109   xstat (path_file, &st);
110   TEST_COMPARE (st.st_mode & 0777, 1);
111   arg = select_path (do_relative_path, path_to_file, "to-file");
112   TEST_COMPARE (chmod_func (fd, arg, 2, 0), 0);
113   xstat (path_file, &st);
114   TEST_COMPARE (st.st_mode & 0777, 2);
115   xlstat (path_to_file, &st);
116   TEST_COMPARE (original_symlink_mode, st.st_mode);
117   arg = select_path (do_relative_path, path_file, "file");
118   TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0);
119   xstat (path_file, &st);
120   TEST_COMPARE (st.st_mode & 0777, 1);
121   xlstat (path_to_file, &st);
122   TEST_COMPARE (original_symlink_mode, st.st_mode);
123 
124   /* Changing the mode of a symbolic link should fail.  */
125   arg = select_path (do_relative_path, path_to_file, "to-file");
126   int ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW);
127   TEST_COMPARE (ret, -1);
128   TEST_COMPARE (errno, EOPNOTSUPP);
129 
130   /* The modes should remain unchanged.  */
131   xstat (path_file, &st);
132   TEST_COMPARE (st.st_mode & 0777, 1);
133   xlstat (path_to_file, &st);
134   TEST_COMPARE (original_symlink_mode, st.st_mode);
135 
136   /* Likewise, changing dangling and looping symbolic links must
137      fail.  */
138   const char *paths[] = { path_dangling, path_loop };
139   for (size_t i = 0; i < array_length (paths); ++i)
140     {
141       const char *path = paths[i];
142       const char *filename = strrchr (path, '/');
143       TEST_VERIFY_EXIT (filename != NULL);
144       ++filename;
145       mode_t new_mode = 010 + i;
146 
147       xlstat (path, &st);
148       TEST_VERIFY ((st.st_mode & 0777) != new_mode);
149       original_symlink_mode = st.st_mode;
150       arg = select_path (do_relative_path, path, filename);
151       ret = chmod_func (fd, arg, new_mode, AT_SYMLINK_NOFOLLOW);
152       TEST_COMPARE (ret, -1);
153       TEST_COMPARE (errno, EOPNOTSUPP);
154       xlstat (path, &st);
155       TEST_COMPARE (st.st_mode, original_symlink_mode);
156     }
157 
158    /* A missing file should always result in ENOENT.  The presence of
159       /proc does not matter.  */
160    arg = select_path (do_relative_path, path_missing, "missing");
161    TEST_COMPARE (chmod_func (fd, arg, 020, 0), -1);
162    TEST_COMPARE (errno, ENOENT);
163    TEST_COMPARE (chmod_func (fd, arg, 020, AT_SYMLINK_NOFOLLOW), -1);
164    TEST_COMPARE (errno, ENOENT);
165 
166    /* Test without available file descriptors.  */
167    {
168      struct fd_list fd_list;
169      fd_list_init (&fd_list);
170      while (true)
171        {
172          int ret = dup (STDOUT_FILENO);
173          if (ret == -1)
174            {
175              if (errno == ENFILE || errno == EMFILE)
176                break;
177              FAIL_EXIT1 ("dup: %m");
178            }
179          fd_list_add (&fd_list, ret);
180          TEST_VERIFY_EXIT (!fd_list_has_failed (&fd_list));
181        }
182      /* Without AT_SYMLINK_NOFOLLOW, changing the permissions should
183         work as before.  */
184      arg = select_path (do_relative_path, path_file, "file");
185      TEST_COMPARE (chmod_func (fd, arg, 3, 0), 0);
186      xstat (path_file, &st);
187      TEST_COMPARE (st.st_mode & 0777, 3);
188      /* But with AT_SYMLINK_NOFOLLOW, even if we originally had
189         support, we may have lost it.  */
190      ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW);
191      if (ret == 0)
192        {
193          xstat (path_file, &st);
194          TEST_COMPARE (st.st_mode & 0777, 2);
195        }
196      else
197        {
198          TEST_COMPARE (ret, -1);
199          /* The error code from the openat fallback leaks out.  */
200          if (errno != ENFILE && errno != EMFILE)
201            TEST_COMPARE (errno, EOPNOTSUPP);
202        }
203      xstat (path_file, &st);
204      TEST_COMPARE (st.st_mode & 0777, 3);
205 
206      /* Close the descriptors.  */
207      for (int *pfd = fd_list_begin (&fd_list); pfd < fd_list_end (&fd_list);
208           ++pfd)
209        xclose (*pfd);
210      fd_list_free (&fd_list);
211    }
212 
213    if (do_relative_path)
214     xclose (fd);
215 
216    free (path_dangling);
217    free (path_file);
218    free (path_loop);
219    free (path_missing);
220    free (path_to_file);
221 
222    free (tempdir);
223 }
224 
225 static void
test_3(void)226 test_3 (void)
227 {
228   puts ("info: testing lchmod");
229   test_1 (false, fchmodat_with_lchmod);
230   puts ("info: testing fchmodat with AT_FDCWD");
231   test_1 (false, fchmodat);
232   puts ("info: testing fchmodat with relative path");
233   test_1 (true, fchmodat);
234 }
235 
236 static int
do_test(void)237 do_test (void)
238 {
239   struct support_descriptors *descriptors = support_descriptors_list ();
240 
241   /* Run the three tests in the default environment.  */
242   test_3 ();
243 
244   /* Try to set up a /proc-less environment and re-test.  */
245 #if __has_include (<sys/mount.h>)
246   if (!support_become_root ())
247     puts ("warning: could not obtain root-like privileges");
248   if (!support_enter_mount_namespace ())
249     puts ("warning: could enter a mount namespace");
250   else
251     {
252       /* Attempt to mount an empty directory over /proc.  */
253       char *tempdir = support_create_temp_directory ("tst-lchmod-");
254       bool proc_emptied
255         = mount (tempdir, "/proc", "none", MS_BIND, NULL) == 0;
256       if (!proc_emptied)
257         printf ("warning: bind-mounting /proc failed: %m");
258       free (tempdir);
259 
260       puts ("info: re-running tests (after trying to empty /proc)");
261       test_3 ();
262 
263       if (proc_emptied)
264         /* Reveal the original /proc, which is needed by the
265            descriptors check below.  */
266         TEST_COMPARE (umount ("/proc"), 0);
267     }
268 #endif /* <sys/mount.h>.  */
269 
270   support_descriptors_check (descriptors);
271   support_descriptors_free (descriptors);
272 
273   return 0;
274 }
275 
276 #include <support/test-driver.c>
277