1 /* provide a chdir function that tries not to fail due to ENAMETOOLONG 2 Copyright (C) 2004, 2005 Free Software Foundation, Inc. 3 4 This program is free software; you can redistribute it and/or modify 5 it under the terms of the GNU General Public License as published by 6 the Free Software Foundation; either version 2, or (at your option) 7 any later version. 8 9 This program is distributed in the hope that it will be useful, 10 but WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 GNU General Public License for more details. 13 14 You should have received a copy of the GNU General Public License 15 along with this program; if not, write to the Free Software Foundation, 16 Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ 17 #include <sys/cdefs.h> 18 __RCSID("$NetBSD: chdir-long.c,v 1.2 2016/05/17 14:00:09 christos Exp $"); 19 20 21 /* written by Jim Meyering */ 22 23 #ifdef HAVE_CONFIG_H 24 # include <config.h> 25 #endif 26 27 #include "chdir-long.h" 28 29 #include <stdlib.h> 30 #include <stdbool.h> 31 #include <string.h> 32 #include <unistd.h> 33 #include <errno.h> 34 #include <stdio.h> 35 #include <assert.h> 36 #include <limits.h> 37 38 #include "memrchr.h" 39 #include "openat.h" 40 41 #ifndef O_DIRECTORY 42 # define O_DIRECTORY 0 43 #endif 44 45 #ifndef PATH_MAX 46 # error "compile this file only if your system defines PATH_MAX" 47 #endif 48 49 struct cd_buf 50 { 51 int fd; 52 }; 53 54 static inline void 55 cdb_init (struct cd_buf *cdb) 56 { 57 cdb->fd = AT_FDCWD; 58 } 59 60 static inline int 61 cdb_fchdir (struct cd_buf const *cdb) 62 { 63 return fchdir (cdb->fd); 64 } 65 66 static inline void 67 cdb_free (struct cd_buf const *cdb) 68 { 69 if (0 <= cdb->fd) 70 { 71 bool close_fail = close (cdb->fd); 72 assert (! close_fail); 73 } 74 } 75 76 /* Given a file descriptor of an open directory (or AT_FDCWD), CDB->fd, 77 try to open the CDB->fd-relative directory, DIR. If the open succeeds, 78 update CDB->fd with the resulting descriptor, close the incoming file 79 descriptor, and return zero. Upon failure, return -1 and set errno. */ 80 static int 81 cdb_advance_fd (struct cd_buf *cdb, char const *dir) 82 { 83 int new_fd = openat (cdb->fd, dir, O_RDONLY | O_DIRECTORY); 84 if (new_fd < 0) 85 { 86 new_fd = openat (cdb->fd, dir, O_WRONLY | O_DIRECTORY); 87 if (new_fd < 0) 88 return -1; 89 } 90 91 cdb_free (cdb); 92 cdb->fd = new_fd; 93 94 return 0; 95 } 96 97 /* Return a pointer to the first non-slash in S. */ 98 static inline char * 99 find_non_slash (char const *s) 100 { 101 size_t n_slash = strspn (s, "/"); 102 return (char *) s + n_slash; 103 } 104 105 /* This is a function much like chdir, but without the PATH_MAX limitation 106 on the length of the directory name. A significant difference is that 107 it must be able to modify (albeit only temporarily) the directory 108 name. It handles an arbitrarily long directory name by operating 109 on manageable portions of the name. On systems without the openat 110 syscall, this means changing the working directory to more and more 111 `distant' points along the long directory name and then restoring 112 the working directory. If any of those attempts to save or restore 113 the working directory fails, this function exits nonzero. 114 115 Note that this function may still fail with errno == ENAMETOOLONG, but 116 only if the specified directory name contains a component that is long 117 enough to provoke such a failure all by itself (e.g. if the component 118 has length PATH_MAX or greater on systems that define PATH_MAX). */ 119 120 int 121 chdir_long (char *dir) 122 { 123 int e = chdir (dir); 124 if (e == 0 || errno != ENAMETOOLONG) 125 return e; 126 127 { 128 size_t len = strlen (dir); 129 char *dir_end = dir + len; 130 struct cd_buf cdb; 131 size_t n_leading_slash; 132 133 cdb_init (&cdb); 134 135 /* If DIR is the empty string, then the chdir above 136 must have failed and set errno to ENOENT. */ 137 assert (0 < len); 138 assert (PATH_MAX <= len); 139 140 /* Count leading slashes. */ 141 n_leading_slash = strspn (dir, "/"); 142 143 /* Handle any leading slashes as well as any name that matches 144 the regular expression, m!^//hostname[/]*! . Handling this 145 prefix separately usually results in a single additional 146 cdb_advance_fd call, but it's worthwhile, since it makes the 147 code in the following loop cleaner. */ 148 if (n_leading_slash == 2) 149 { 150 int err; 151 /* Find next slash. 152 We already know that dir[2] is neither a slash nor '\0'. */ 153 char *slash = memchr (dir + 3, '/', dir_end - (dir + 3)); 154 if (slash == NULL) 155 { 156 errno = ENAMETOOLONG; 157 return -1; 158 } 159 *slash = '\0'; 160 err = cdb_advance_fd (&cdb, dir); 161 *slash = '/'; 162 if (err != 0) 163 goto Fail; 164 dir = find_non_slash (slash + 1); 165 } 166 else if (n_leading_slash) 167 { 168 if (cdb_advance_fd (&cdb, "/") != 0) 169 goto Fail; 170 dir += n_leading_slash; 171 } 172 173 assert (*dir != '/'); 174 assert (dir <= dir_end); 175 176 while (PATH_MAX <= dir_end - dir) 177 { 178 int err; 179 /* Find a slash that is PATH_MAX or fewer bytes away from dir. 180 I.e. see if there is a slash that will give us a name of 181 length PATH_MAX-1 or less. */ 182 char *slash = memrchr (dir, '/', PATH_MAX); 183 if (slash == NULL) 184 { 185 errno = ENAMETOOLONG; 186 return -1; 187 } 188 189 *slash = '\0'; 190 assert (slash - dir < PATH_MAX); 191 err = cdb_advance_fd (&cdb, dir); 192 *slash = '/'; 193 if (err != 0) 194 goto Fail; 195 196 dir = find_non_slash (slash + 1); 197 } 198 199 if (dir < dir_end) 200 { 201 if (cdb_advance_fd (&cdb, dir) != 0) 202 goto Fail; 203 } 204 205 if (cdb_fchdir (&cdb) != 0) 206 goto Fail; 207 208 cdb_free (&cdb); 209 return 0; 210 211 Fail: 212 { 213 int saved_errno = errno; 214 cdb_free (&cdb); 215 errno = saved_errno; 216 return -1; 217 } 218 } 219 } 220 221 #if TEST_CHDIR 222 223 # include <stdio.h> 224 # include "closeout.h" 225 # include "error.h" 226 227 char *program_name; 228 229 int 230 main (int argc, char *argv[]) 231 { 232 char *line = NULL; 233 size_t n = 0; 234 int len; 235 236 program_name = argv[0]; 237 atexit (close_stdout); 238 239 len = getline (&line, &n, stdin); 240 if (len < 0) 241 { 242 int saved_errno = errno; 243 if (feof (stdin)) 244 exit (0); 245 246 error (EXIT_FAILURE, saved_errno, 247 "reading standard input"); 248 } 249 else if (len == 0) 250 exit (0); 251 252 if (line[len-1] == '\n') 253 line[len-1] = '\0'; 254 255 if (chdir_long (line) != 0) 256 error (EXIT_FAILURE, errno, 257 "chdir_long failed: %s", line); 258 259 if (argc <= 1) 260 { 261 /* Using `pwd' here makes sense only if it is a robust implementation, 262 like the one in coreutils after the 2004-04-19 changes. */ 263 char const *cmd = "pwd"; 264 execlp (cmd, (char *) NULL); 265 error (EXIT_FAILURE, errno, "%s", cmd); 266 } 267 268 fclose (stdin); 269 fclose (stderr); 270 271 exit (EXIT_SUCCESS); 272 } 273 #endif 274 275 /* 276 Local Variables: 277 compile-command: "gcc -DTEST_CHDIR=1 -DHAVE_CONFIG_H -I.. -g -O -W -Wall chdir-long.c libcoreutils.a" 278 End: 279 */ 280