diff --git a/src/libexpr/meson.build b/src/libexpr/meson.build index 18c4c7fa32c..a98319e4af1 100644 --- a/src/libexpr/meson.build +++ b/src/libexpr/meson.build @@ -28,7 +28,7 @@ deps_public_maybe_subproject = [ subdir('nix-meson-build-support/subprojects') subdir('nix-meson-build-support/big-objs') -# Check for each of these functions, and create a define like `#define HAVE_LCHOWN 1`. +# Check for each of these functions, and create a define like `#define HAVE_SYSCONF 1`. check_funcs = [ 'sysconf', ] diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index a0ac1906d1e..57d561085f3 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -16,10 +16,13 @@ #include "nix/util/json-utils.hh" #include "nix/util/archive.hh" #include "nix/util/mounted-source-accessor.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/canon-path.hh" #include #include #include +#include #ifndef _WIN32 # include @@ -846,8 +849,25 @@ struct GitInputScheme : InputScheme } try { - if (!input.getRev()) - setWriteTime(localRefFile, now, now); + if (!input.getRev()) { + auto parent = localRefFile.parent_path(); + auto name = localRefFile.filename(); + AutoCloseFD dirFd +#ifndef _WIN32 + = toDescriptor(open(parent.string().c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC)); +#else + = CreateFileW( + parent.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); +#endif + if (dirFd) + setWriteTime(dirFd.get(), CanonPath(name.string()), now, now); + } } catch (Error & e) { warn("could not update mtime for file %s: %s", PathFmt(localRefFile), e.info().msg); } diff --git a/src/libstore/meson.build b/src/libstore/meson.build index a01aa903ae2..3452ed215d3 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -68,10 +68,8 @@ endif summary('can hardlink to symlink', can_link_symlink, bool_yn : true) configdata_priv.set('CAN_LINK_SYMLINK', can_link_symlink.to_int()) -# Check for each of these functions, and create a define like `#define HAVE_LCHOWN 1`. +# Check for each of these functions, and create a define like `#define HAVE_POSIX_FALLOCATE 1`. check_funcs = [ - # Optionally used for canonicalising files from the build - 'lchown', 'posix_fallocate', 'statvfs', ] diff --git a/src/libstore/posix-fs-canonicalise.cc b/src/libstore/posix-fs-canonicalise.cc index fe1d0031622..e13190ffa2c 100644 --- a/src/libstore/posix-fs-canonicalise.cc +++ b/src/libstore/posix-fs-canonicalise.cc @@ -5,8 +5,16 @@ #include "nix/util/util.hh" #include "nix/store/store-api.hh" #include "nix/store/globals.hh" +#include "nix/util/file-descriptor.hh" +#include "nix/util/canon-path.hh" #include "store-config-private.hh" +#ifndef _WIN32 +# include +# include +# include +#endif + #if NIX_SUPPORT_ACL # include #endif @@ -15,56 +23,87 @@ namespace nix { const time_t mtimeStore = 1; /* 1 second into the epoch */ -static void canonicaliseTimestampAndPermissions(const Path & path, const PosixStat & st) +static void canonicaliseTimestampAndPermissions(Descriptor dirFd, const CanonPath & path, const struct stat & st) { if (!S_ISLNK(st.st_mode)) { - /* Mask out all type related bits. */ mode_t mode = st.st_mode & ~S_IFMT; bool isDir = S_ISDIR(st.st_mode); if ((mode != 0444 || isDir) && mode != 0555) { - mode = (st.st_mode & S_IFMT) | 0444 | (st.st_mode & S_IXUSR || isDir ? 0111 : 0); - chmod(path, mode); + mode = 0444 | (st.st_mode & S_IXUSR || isDir ? 0111 : 0); +#ifndef _WIN32 + unix::fchmodatTryNoFollow(dirFd, path, mode); +#else + // TODO: implement fchmodatTryNoFollow for Windows +#endif } } -#ifndef _WIN32 // TODO implement if (st.st_mtime != mtimeStore) { - PosixStat st2 = st; - st2.st_mtime = mtimeStore, setWriteTime(path, st2); + setWriteTime(dirFd, path, st.st_atime, mtimeStore); } -#endif } void canonicaliseTimestampAndPermissions(const Path & path) { - canonicaliseTimestampAndPermissions(path, lstat(path)); + auto parent = std::filesystem::path(path).parent_path(); + auto name = std::filesystem::path(path).filename(); + + if (parent.empty()) + parent = "."; + + AutoCloseFD dirFd = toDescriptor(open(parent.string().c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + if (!dirFd) + throw SysError("opening parent directory of '%s'", path); + + struct stat st; + if (fstatat(dirFd.get(), name.string().c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) + throw SysError("statting '%s'", path); + + canonicaliseTimestampAndPermissions(dirFd.get(), CanonPath(name.string()), st); } -static void -canonicalisePathMetaData_(const Path & path, CanonicalizePathMetadataOptions options, InodesSeen & inodesSeen) +static void canonicalisePathMetaData_( + Descriptor dirFd, const CanonPath & path, CanonicalizePathMetadataOptions options, InodesSeen & inodesSeen) { checkInterrupt(); + struct stat st; + if (fstatat(dirFd, path.rel_c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) + throw SysError("statting '%s'", path); + #ifdef __APPLE__ /* Remove flags, in particular UF_IMMUTABLE which would prevent the file from being garbage-collected. FIXME: Use setattrlist() to remove other attributes as well. */ - if (lchflags(path.c_str(), 0)) { + AutoCloseFD fd = openat(dirFd, path.rel_c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (!fd) + throw SysError("opening '%s' to clear flags", path); + if (fchflags(fd.get(), 0)) { if (errno != ENOTSUP) throw SysError("clearing flags of path '%1%'", path); } #endif - auto st = lstat(path); - /* Really make sure that the path is of a supported type. */ if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode))) throw Error("file '%1%' has an unsupported type", path); #if NIX_SUPPORT_ACL /* Remove extended attributes / ACLs. */ - ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0); + /* We need a file descriptor for xattr operations on Linux. */ + AutoCloseFD fd = openat( + dirFd, + path.rel_c_str(), + O_RDONLY | O_NOFOLLOW | O_CLOEXEC +# ifdef O_PATH + | O_PATH +# endif + ); + if (!fd) + throw SysError("opening '%s' to remove extended attributes", path); + + ssize_t eaSize = flistxattr(fd.get(), nullptr, 0); if (eaSize < 0) { if (errno != ENOTSUP && errno != ENODATA) @@ -72,19 +111,18 @@ canonicalisePathMetaData_(const Path & path, CanonicalizePathMetadataOptions opt } else if (eaSize > 0) { std::vector eaBuf(eaSize); - if ((eaSize = llistxattr(path.c_str(), eaBuf.data(), eaBuf.size())) < 0) + if ((eaSize = flistxattr(fd.get(), eaBuf.data(), eaBuf.size())) < 0) throw SysError("querying extended attributes of '%s'", path); for (auto & eaName : tokenizeString(std::string(eaBuf.data(), eaSize), std::string("\000", 1))) { if (options.ignoredAcls.count(eaName)) continue; - if (lremovexattr(path.c_str(), eaName.c_str()) == -1) + if (fremovexattr(fd.get(), eaName.c_str()) == -1) throw SysError("removing extended attribute '%s' from '%s'", eaName, path); } } #endif -#ifndef _WIN32 /* Fail if the file is not owned by the build user. This prevents us from messing up the ownership/permissions of files hard-linked into the output (e.g. "ln /etc/shadow $out/foo"). @@ -100,58 +138,59 @@ canonicalisePathMetaData_(const Path & path, CanonicalizePathMetadataOptions opt || (st.st_uid == geteuid() && (mode == 0444 || mode == 0555) && st.st_mtime == mtimeStore)); return; } -#endif inodesSeen.insert(Inode(st.st_dev, st.st_ino)); - canonicaliseTimestampAndPermissions(path, st); + canonicaliseTimestampAndPermissions(dirFd, path, st); -#ifndef _WIN32 - /* Change ownership to the current uid. If it's a symlink, use - lchown if available, otherwise don't bother. Wrong ownership - of a symlink doesn't matter, since the owning user can't change - the symlink and can't delete it because the directory is not - writable. The only exception is top-level paths in the Nix - store (since that directory is group-writable for the Nix build - users group); we check for this case below. */ + /* Change ownership to the current uid. */ if (st.st_uid != geteuid()) { -# if HAVE_LCHOWN - if (lchown(path.c_str(), geteuid(), getegid()) == -1) -# else - if (!S_ISLNK(st.st_mode) && chown(path.c_str(), geteuid(), getegid()) == -1) -# endif + if (fchownat(dirFd, path.rel_c_str(), geteuid(), getegid(), AT_SYMLINK_NOFOLLOW) == -1) throw SysError("changing owner of '%1%' to %2%", path, geteuid()); } -#endif if (S_ISDIR(st.st_mode)) { - for (auto & i : DirectoryIterator{path}) { + AutoCloseFD childDirFd = + openFileEnsureBeneathNoSymlinks(dirFd, path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (!childDirFd) + throw SysError("opening directory '%s'", path); + + AutoCloseDir dir(fdopendir(dup(childDirFd.get()))); + if (!dir) + throw SysError("opening directory '%s' for iteration", path); + + struct dirent * dirent; + while (errno = 0, dirent = readdir(dir.get())) { checkInterrupt(); - canonicalisePathMetaData_(i.path().string(), options, inodesSeen); + std::string childName = dirent->d_name; + if (childName == "." || childName == "..") + continue; + canonicalisePathMetaData_(childDirFd.get(), CanonPath(childName), options, inodesSeen); } + if (errno) + throw SysError("reading directory '%s'", path); } } void canonicalisePathMetaData(const Path & path, CanonicalizePathMetadataOptions options, InodesSeen & inodesSeen) { - canonicalisePathMetaData_(path, options, inodesSeen); + auto parent = std::filesystem::path(path).parent_path(); + auto name = std::filesystem::path(path).filename(); -#ifndef _WIN32 - /* On platforms that don't have lchown(), the top-level path can't - be a symlink, since we can't change its ownership. */ - auto st = lstat(path); + if (parent.empty()) + parent = "."; - if (st.st_uid != geteuid()) { - assert(S_ISLNK(st.st_mode)); - throw Error("wrong ownership of top-level store path '%1%'", path); - } -#endif + AutoCloseFD dirFd = toDescriptor(open(parent.string().c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + if (!dirFd) + throw SysError("opening parent directory of '%s'", path); + + canonicalisePathMetaData_(dirFd.get(), CanonPath(name.string()), options, inodesSeen); } void canonicalisePathMetaData(const Path & path, CanonicalizePathMetadataOptions options) { InodesSeen inodesSeen; - canonicalisePathMetaData_(path, options, inodesSeen); + canonicalisePathMetaData(path, options, inodesSeen); } } // namespace nix diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 1611ed8622a..a7b4053beb9 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -676,11 +676,6 @@ void replaceSymlink(const std::filesystem::path & target, const std::filesystem: } } -void setWriteTime(const std::filesystem::path & path, const PosixStat & st) -{ - setWriteTime(path, st.st_atime, st.st_mtime, S_ISLNK(st.st_mode)); -} - void copyFile(const std::filesystem::path & from, const std::filesystem::path & to, bool andDelete) { auto fromStatus = std::filesystem::symlink_status(from); @@ -705,7 +700,7 @@ void copyFile(const std::filesystem::path & from, const std::filesystem::path & throw Error("file %s has an unsupported type", PathFmt(from)); } - setWriteTime(to, lstat(from.string().c_str())); + // Note: we don't preserve timestamps anymore if (andDelete) { if (!std::filesystem::is_symlink(fromStatus)) std::filesystem::permissions( diff --git a/src/libutil/include/nix/util/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh index 68be0b35d91..b9867f26f56 100644 --- a/src/libutil/include/nix/util/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -342,6 +342,14 @@ Descriptor openFileEnsureBeneathNoSymlinks( #endif ); +/** + * Set the access and modification time of a file relative to a directory file descriptor. + * + * @pre path.isRoot() is false + * @throws SysError if any operation fails + */ +void setWriteTime(Descriptor dirFd, const CanonPath & path, time_t accessedTime, time_t modificationTime); + #ifndef _WIN32 namespace unix { diff --git a/src/libutil/include/nix/util/file-system.hh b/src/libutil/include/nix/util/file-system.hh index 0ac61728d56..f65b17863f5 100644 --- a/src/libutil/include/nix/util/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -299,30 +299,6 @@ void createDirs(const std::filesystem::path & path); */ void createDir(const Path & path, mode_t mode = 0755); -/** - * Set the access and modification times of the given path, not - * following symlinks. - * - * @param accessedTime Specified in seconds. - * - * @param modificationTime Specified in seconds. - * - * @param isSymlink Whether the file in question is a symlink. Used for - * fallback code where we don't have `lutimes` or similar. if - * `std::optional` is passed, the information will be recomputed if it - * is needed. Race conditions are possible so be careful! - */ -void setWriteTime( - const std::filesystem::path & path, - time_t accessedTime, - time_t modificationTime, - std::optional isSymlink = std::nullopt); - -/** - * Convenience wrapper that takes all arguments from the `PosixStat`. - */ -void setWriteTime(const std::filesystem::path & path, const PosixStat & st); - /** * Create a symlink. * diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc index 756fa0159c6..84f8496c45a 100644 --- a/src/libutil/unix/file-descriptor.cc +++ b/src/libutil/unix/file-descriptor.cc @@ -410,4 +410,31 @@ OsString readLinkAt(Descriptor dirFd, const CanonPath & path) } } +void setWriteTime(Descriptor dirFd, const CanonPath & path, time_t accessedTime, time_t modificationTime) +{ + struct timespec times[2] = { + { + .tv_sec = accessedTime, + .tv_nsec = 0, + }, + { + .tv_sec = modificationTime, + .tv_nsec = 0, + }, + }; + +#if HAVE_UTIMENSAT && HAVE_DECL_AT_SYMLINK_NOFOLLOW + if (utimensat(dirFd, path.rel_c_str(), times, AT_SYMLINK_NOFOLLOW) == -1) + throw SysError("changing modification time of '%s' (using `utimensat`)", path); +#else + // Fallback: open the file and use futimens + AutoCloseFD fd = openat(dirFd, path.rel_c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (!fd) + throw SysError("opening '%s' to change modification time", path); + + if (futimens(fd.get(), times) == -1) + throw SysError("changing modification time of '%s' (using `futimens`)", path); +#endif +} + } // namespace nix diff --git a/src/libutil/unix/file-system.cc b/src/libutil/unix/file-system.cc index ab9fed6ea41..294a6519c5f 100644 --- a/src/libutil/unix/file-system.cc +++ b/src/libutil/unix/file-system.cc @@ -50,53 +50,6 @@ std::filesystem::path defaultTempDir() return getEnvNonEmpty("TMPDIR").value_or("/tmp"); } -void setWriteTime( - const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) -{ - // Would be nice to use std::filesystem unconditionally, but - // doesn't support access time just modification time. - // - // System clock vs File clock issues also make that annoying. -#if HAVE_UTIMENSAT && HAVE_DECL_AT_SYMLINK_NOFOLLOW - struct timespec times[2] = { - { - .tv_sec = accessedTime, - .tv_nsec = 0, - }, - { - .tv_sec = modificationTime, - .tv_nsec = 0, - }, - }; - if (utimensat(AT_FDCWD, path.c_str(), times, AT_SYMLINK_NOFOLLOW) == -1) - throw SysError("changing modification time of %s (using `utimensat`)", PathFmt(path)); -#else - struct timeval times[2] = { - { - .tv_sec = accessedTime, - .tv_usec = 0, - }, - { - .tv_sec = modificationTime, - .tv_usec = 0, - }, - }; -# if HAVE_LUTIMES - if (lutimes(path.c_str(), times) == -1) - throw SysError("changing modification time of %s", PathFmt{path}); -# else - bool isSymlink = optIsSymlink ? *optIsSymlink : std::filesystem::is_symlink(path); - - if (!isSymlink) { - if (utimes(path.c_str(), times) == -1) - throw SysError("changing modification time of %s (not a symlink)", PathFmt{path}); - } else { - throw Error("Cannot change modification time of symlink %s", PathFmt{path}); - } -# endif -#endif -} - #ifdef __FreeBSD__ # define MOUNTEDPATHS_PARAM , std::set & mountedPaths # define MOUNTEDPATHS_ARG , mountedPaths diff --git a/src/libutil/windows/file-descriptor.cc b/src/libutil/windows/file-descriptor.cc index adea665ba34..804eec5502b 100644 --- a/src/libutil/windows/file-descriptor.cc +++ b/src/libutil/windows/file-descriptor.cc @@ -4,6 +4,7 @@ #include "nix/util/serialise.hh" #include "nix/util/file-path.hh" #include "nix/util/source-accessor.hh" +#include "nix/util/canon-path.hh" #include @@ -444,4 +445,59 @@ OsString readLinkAt(Descriptor dirFd, const CanonPath & path) return windows::readSymlinkTarget(linkHandle.get()); } +namespace windows { + +namespace { + +/** + * Convert Unix time_t to Windows FILETIME + */ +FILETIME timeToFileTime(time_t t) +{ + // Windows FILETIME is 100-nanosecond intervals since January 1, 1601 (UTC) + // Unix time_t is seconds since January 1, 1970 (UTC) + // Difference between 1601 and 1970 in 100-nanosecond intervals + const uint64_t EPOCH_DIFFERENCE = 116444736000000000ULL; + + uint64_t intervals = (static_cast(t) * 10000000ULL) + EPOCH_DIFFERENCE; + + FILETIME ft; + ft.dwLowDateTime = static_cast(intervals); + ft.dwHighDateTime = static_cast(intervals >> 32); + return ft; +} + +void setWriteTime(Descriptor fileHandle, time_t accessedTime, time_t modificationTime) +{ + // Convert times to FILETIME + FILETIME accessTime = timeToFileTime(accessedTime); + FILETIME modifyTime = timeToFileTime(modificationTime); + + // Set the file times + // Note: We pass NULL for creation time to leave it unchanged + if (!SetFileTime(fileHandle, NULL, &accessTime, &modifyTime)) + throw WinError("setting file times for '%s'", path.rel()); +} + +} // anonymous namespace + +} // namespace windows + +void setWriteTime(Descriptor dirHandle, const CanonPath & path, time_t accessedTime, time_t modificationTime) +{ + assert(!path.isRoot()); + + // Open the file relative to the directory handle using ntOpenAt + // This avoids TOCTOU issues by not converting handle to path and back + std::wstring wpath = string_to_os_string(path.rel()); + AutoCloseFD fileHandle = ntOpenAt( + dirHandle, + wpath, + FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, + FILE_OPEN_REPARSE_POINT // Don't follow symlinks + ); + + setWriteTime(fileHandle.get(), accessedTime, modificationTime); +} + } // namespace nix diff --git a/src/libutil/windows/file-system.cc b/src/libutil/windows/file-system.cc index e3b74fe392f..1f8895e0088 100644 --- a/src/libutil/windows/file-system.cc +++ b/src/libutil/windows/file-system.cc @@ -11,18 +11,6 @@ namespace nix { using namespace nix::windows; -void setWriteTime( - const std::filesystem::path & path, time_t accessedTime, time_t modificationTime, std::optional optIsSymlink) -{ - // FIXME use `std::filesystem::last_write_time`. - // - // Would be nice to use std::filesystem unconditionally, but - // doesn't support access time just modification time. - // - // System clock vs File clock issues also make that annoying. - warn("Changing file times is not yet implemented on Windows, path is %s", PathFmt(path)); -} - Descriptor openDirectory(const std::filesystem::path & path) { return CreateFileW(