Symlink-race-safe variant of do_link() for receiver-side use. See the comment on do_chmod_at() for the threat model. link() resolves parent components of *both* old_path and new_path, so a parent- symlink swap on either side can plant the new hard link outside the module, or hard-link an outside file into the module (read disclosure). Defence: open each parent under secure_relative_op
| 313 | pre-AT_FDCWD systems fall through to do_link(). |
| 314 | */ |
| 315 | int do_link_at(const char *old_path, const char *new_path) |
| 316 | { |
| 317 | #if defined AT_FDCWD && defined HAVE_LINKAT |
| 318 | extern int am_daemon, am_chrooted; |
| 319 | char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; |
| 320 | const char *old_bname, *new_bname; |
| 321 | const char *old_slash, *new_slash; |
| 322 | int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; |
| 323 | BOOL old_owns = False, new_owns = False; |
| 324 | int ret, e; |
| 325 | size_t old_dlen = 0, new_dlen = 0; |
| 326 | |
| 327 | if (dry_run) return 0; |
| 328 | RETURN_ERROR_IF_RO_OR_LO; |
| 329 | |
| 330 | if (!am_daemon || am_chrooted) |
| 331 | return do_link(old_path, new_path); |
| 332 | |
| 333 | if (!old_path || !*old_path || *old_path == '/' |
| 334 | || !new_path || !*new_path || *new_path == '/') |
| 335 | return do_link(old_path, new_path); |
| 336 | |
| 337 | old_slash = strrchr(old_path, '/'); |
| 338 | new_slash = strrchr(new_path, '/'); |
| 339 | |
| 340 | /* Resolve each path's parent dir independently. A path without a |
| 341 | * slash lives in CWD (AT_FDCWD), no parent open required. A path |
| 342 | * with a slash needs secure_relative_open to confine its parent |
| 343 | * resolution -- otherwise a parent symlink (e.g. --link-dest=cd |
| 344 | * where cd -> /outside) lets the kernel-level linkat(AT_FDCWD, |
| 345 | * "cd/target.txt", ...) escape the module. */ |
| 346 | if (old_slash) { |
| 347 | old_dlen = old_slash - old_path; |
| 348 | if (old_dlen >= sizeof old_dirpath) { errno = ENAMETOOLONG; return -1; } |
| 349 | memcpy(old_dirpath, old_path, old_dlen); |
| 350 | old_dirpath[old_dlen] = '\0'; |
| 351 | old_bname = old_slash + 1; |
| 352 | old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); |
| 353 | if (old_dfd < 0) |
| 354 | return -1; |
| 355 | old_owns = True; |
| 356 | } else { |
| 357 | old_bname = old_path; |
| 358 | } |
| 359 | |
| 360 | if (new_slash) { |
| 361 | new_dlen = new_slash - new_path; |
| 362 | if (new_dlen >= sizeof new_dirpath) { |
| 363 | e = ENAMETOOLONG; |
| 364 | if (old_owns) close(old_dfd); |
| 365 | errno = e; |
| 366 | return -1; |
| 367 | } |
| 368 | memcpy(new_dirpath, new_path, new_dlen); |
| 369 | new_dirpath[new_dlen] = '\0'; |
| 370 | new_bname = new_slash + 1; |
| 371 | if (old_owns && old_dlen == new_dlen |
| 372 | && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { |
no test coverage detected