|
| 1 | +#include "nix/util/canon-path.hh" |
1 | 2 | #include "nix/util/file-system.hh" |
2 | 3 | #include "nix/util/signals.hh" |
3 | 4 | #include "nix/util/finally.hh" |
|
7 | 8 | #include <unistd.h> |
8 | 9 | #include <poll.h> |
9 | 10 |
|
| 11 | +#if defined(__linux__) && defined(__NR_openat2) |
| 12 | +# define HAVE_OPENAT2 1 |
| 13 | +# include <sys/syscall.h> |
| 14 | +# include <linux/openat2.h> |
| 15 | +#else |
| 16 | +# define HAVE_OPENAT2 0 |
| 17 | +#endif |
| 18 | + |
10 | 19 | #include "util-config-private.hh" |
11 | 20 | #include "util-unix-config-private.hh" |
12 | 21 |
|
@@ -223,4 +232,104 @@ void unix::closeOnExec(int fd) |
223 | 232 | throw SysError("setting close-on-exec flag"); |
224 | 233 | } |
225 | 234 |
|
| 235 | +#ifdef __linux__ |
| 236 | + |
| 237 | +namespace linux { |
| 238 | + |
| 239 | +std::optional<Descriptor> openat2(Descriptor dirFd, const char * path, uint64_t flags, uint64_t mode, uint64_t resolve) |
| 240 | +{ |
| 241 | +# if HAVE_OPENAT2 |
| 242 | + /* Cache the result of whether openat2 is not supported. */ |
| 243 | + static std::atomic_flag unsupported{}; |
| 244 | + |
| 245 | + if (!unsupported.test()) { |
| 246 | + /* No glibc wrapper yet, but there's a patch: |
| 247 | + * https://patchwork.sourceware.org/project/glibc/patch/20251029200519.3203914-1-adhemerval.zanella@linaro.org/ |
| 248 | + */ |
| 249 | + auto how = ::open_how{.flags = flags, .mode = mode, .resolve = resolve}; |
| 250 | + auto res = ::syscall(__NR_openat2, dirFd, path, &how, sizeof(how)); |
| 251 | + /* Cache that the syscall is not supported. */ |
| 252 | + if (res < 0 && errno == ENOSYS) { |
| 253 | + unsupported.test_and_set(); |
| 254 | + return std::nullopt; |
| 255 | + } |
| 256 | + |
| 257 | + return res; |
| 258 | + } |
| 259 | +# endif |
| 260 | + return std::nullopt; |
| 261 | +} |
| 262 | + |
| 263 | +} // namespace linux |
| 264 | + |
| 265 | +#endif |
| 266 | + |
| 267 | +static Descriptor |
| 268 | +openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode) |
| 269 | +{ |
| 270 | + AutoCloseFD parentFd; |
| 271 | + auto nrComponents = std::ranges::distance(path); |
| 272 | + auto components = std::views::take(path, nrComponents - 1); /* Everything but last component */ |
| 273 | + auto getParentFd = [&]() { return parentFd ? parentFd.get() : dirFd; }; |
| 274 | + |
| 275 | + /* This rather convoluted loop is necessary to avoid TOCTOU when validating that |
| 276 | + no inner path component is a symlink. */ |
| 277 | + for (auto it = components.begin(); it != components.end(); ++it) { |
| 278 | + auto component = std::string(*it); /* Copy into a string to make NUL terminated. */ |
| 279 | + assert(component != ".." && !component.starts_with('/')); /* In case invariant is broken somehow.. */ |
| 280 | + |
| 281 | + AutoCloseFD parentFd2 = ::openat( |
| 282 | + getParentFd(), /* First iteration uses dirFd. */ |
| 283 | + component.c_str(), |
| 284 | + O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC |
| 285 | +#ifdef __linux__ |
| 286 | + | O_PATH /* Linux-specific optimization. Files are open only for path resolution purposes. */ |
| 287 | +#endif |
| 288 | +#ifdef __FreeBSD__ |
| 289 | + | O_RESOLVE_BENEATH /* Further guard against any possible SNAFUs. */ |
| 290 | +#endif |
| 291 | + ); |
| 292 | + |
| 293 | + if (!parentFd2) { |
| 294 | + /* Construct the CanonPath for error message. */ |
| 295 | + auto path2 = std::ranges::fold_left(components.begin(), ++it, CanonPath::root, [](auto lhs, auto rhs) { |
| 296 | + lhs.push(rhs); |
| 297 | + return lhs; |
| 298 | + }); |
| 299 | + |
| 300 | + if (errno == ENOTDIR) /* Path component might be a symlink. */ { |
| 301 | + struct ::stat st; |
| 302 | + if (::fstatat(getParentFd(), component.c_str(), &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(st.st_mode)) |
| 303 | + throw unix::SymlinkNotAllowed(path2); |
| 304 | + errno = ENOTDIR; /* Restore the errno. */ |
| 305 | + } else if (errno == ELOOP) { |
| 306 | + throw unix::SymlinkNotAllowed(path2); |
| 307 | + } |
| 308 | + |
| 309 | + return INVALID_DESCRIPTOR; |
| 310 | + } |
| 311 | + |
| 312 | + parentFd = std::move(parentFd2); |
| 313 | + } |
| 314 | + |
| 315 | + auto res = ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags | O_NOFOLLOW, mode); |
| 316 | + if (res < 0 && errno == ELOOP) |
| 317 | + throw unix::SymlinkNotAllowed(path); |
| 318 | + return res; |
| 319 | +} |
| 320 | + |
| 321 | +Descriptor unix::openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode) |
| 322 | +{ |
| 323 | +#ifdef __linux__ |
| 324 | + auto maybeFd = linux::openat2( |
| 325 | + dirFd, path.rel_c_str(), flags, static_cast<uint64_t>(mode), RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS); |
| 326 | + if (maybeFd) { |
| 327 | + if (*maybeFd < 0 && errno == ELOOP) |
| 328 | + throw unix::SymlinkNotAllowed(path); |
| 329 | + return *maybeFd; |
| 330 | + } |
| 331 | +#endif |
| 332 | + return openFileEnsureBeneathNoSymlinksIterative(dirFd, path, flags, mode); |
| 333 | +} |
| 334 | + |
226 | 335 | } // namespace nix |
0 commit comments