Skip to content

Commit 70b9fbd

Browse files
authored
Merge pull request #14597 from NixOS/restore-sink-openat
libutil: Make RestoreSink use *at system calls on UNIX
2 parents f7de5b3 + 40b2515 commit 70b9fbd

File tree

7 files changed

+167
-50
lines changed

7 files changed

+167
-50
lines changed

src/libutil/archive.cc

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -200,54 +200,54 @@ static void parse(FileSystemObjectSink & sink, Source & source, const CanonPath
200200
}
201201

202202
else if (type == "directory") {
203-
sink.createDirectory(path);
203+
sink.createDirectory(path, [&](FileSystemObjectSink & dirSink, const CanonPath & relDirPath) {
204+
std::map<Path, int, CaseInsensitiveCompare> names;
204205

205-
std::map<Path, int, CaseInsensitiveCompare> names;
206+
std::string prevName;
206207

207-
std::string prevName;
208+
while (1) {
209+
auto tag = getString();
208210

209-
while (1) {
210-
auto tag = getString();
211+
if (tag == ")")
212+
break;
211213

212-
if (tag == ")")
213-
break;
214-
215-
if (tag != "entry")
216-
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
217-
218-
expectTag("(");
219-
220-
expectTag("name");
221-
222-
auto name = getString();
223-
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos
224-
|| name.find((char) 0) != std::string::npos)
225-
throw badArchive("NAR contains invalid file name '%1%'", name);
226-
if (name <= prevName)
227-
throw badArchive("NAR directory is not sorted");
228-
prevName = name;
229-
if (archiveSettings.useCaseHack) {
230-
auto i = names.find(name);
231-
if (i != names.end()) {
232-
debug("case collision between '%1%' and '%2%'", i->first, name);
233-
name += caseHackSuffix;
234-
name += std::to_string(++i->second);
235-
auto j = names.find(name);
236-
if (j != names.end())
237-
throw badArchive(
238-
"NAR contains file name '%s' that collides with case-hacked file name '%s'",
239-
prevName,
240-
j->first);
241-
} else
242-
names[name] = 0;
243-
}
214+
if (tag != "entry")
215+
throw badArchive("expected tag 'entry' or ')', got '%s'", tag);
244216

245-
expectTag("node");
217+
expectTag("(");
246218

247-
parse(sink, source, path / name);
219+
expectTag("name");
248220

249-
expectTag(")");
250-
}
221+
auto name = getString();
222+
if (name.empty() || name == "." || name == ".." || name.find('/') != std::string::npos
223+
|| name.find((char) 0) != std::string::npos)
224+
throw badArchive("NAR contains invalid file name '%1%'", name);
225+
if (name <= prevName)
226+
throw badArchive("NAR directory is not sorted");
227+
prevName = name;
228+
if (archiveSettings.useCaseHack) {
229+
auto i = names.find(name);
230+
if (i != names.end()) {
231+
debug("case collision between '%1%' and '%2%'", i->first, name);
232+
name += caseHackSuffix;
233+
name += std::to_string(++i->second);
234+
auto j = names.find(name);
235+
if (j != names.end())
236+
throw badArchive(
237+
"NAR contains file name '%s' that collides with case-hacked file name '%s'",
238+
prevName,
239+
j->first);
240+
} else
241+
names[name] = 0;
242+
}
243+
244+
expectTag("node");
245+
246+
parse(dirSink, source, relDirPath / name);
247+
248+
expectTag(")");
249+
}
250+
});
251251
}
252252

253253
else if (type == "symlink") {

src/libutil/fs-sink.cc

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ void copyRecursive(SourceAccessor & accessor, const CanonPath & from, FileSystem
3434
}
3535

3636
case SourceAccessor::tDirectory: {
37-
sink.createDirectory(to);
38-
for (auto & [name, _] : accessor.readDirectory(from)) {
39-
copyRecursive(accessor, from / name, sink, to / name);
40-
break;
41-
}
37+
sink.createDirectory(to, [&](FileSystemObjectSink & dirSink, const CanonPath & relDirPath) {
38+
for (auto & [name, _] : accessor.readDirectory(from)) {
39+
copyRecursive(accessor, from / name, dirSink, relDirPath / name);
40+
break;
41+
}
42+
});
4243
break;
4344
}
4445

@@ -70,11 +71,60 @@ static std::filesystem::path append(const std::filesystem::path & src, const Can
7071
return dst;
7172
}
7273

74+
#ifndef _WIN32
75+
void RestoreSink::createDirectory(const CanonPath & path, DirectoryCreatedCallback callback)
76+
{
77+
if (path.isRoot()) {
78+
createDirectory(path);
79+
callback(*this, path);
80+
return;
81+
}
82+
83+
createDirectory(path);
84+
assert(dirFd); // If that's not true the above call must have thrown an exception.
85+
86+
RestoreSink dirSink{startFsync};
87+
dirSink.dstPath = append(dstPath, path);
88+
dirSink.dirFd = ::openat(dirFd.get(), path.rel_c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
89+
90+
if (!dirSink.dirFd)
91+
throw SysError("opening directory '%s'", dirSink.dstPath.string());
92+
93+
callback(dirSink, CanonPath::root);
94+
}
95+
#endif
96+
7397
void RestoreSink::createDirectory(const CanonPath & path)
7498
{
7599
auto p = append(dstPath, path);
100+
101+
#ifndef _WIN32
102+
if (dirFd) {
103+
if (path.isRoot())
104+
/* Trying to create a directory that we already have a file descriptor for. */
105+
throw Error("path '%s' already exists", p.string());
106+
107+
if (::mkdirat(dirFd.get(), path.rel_c_str(), 0777) == -1)
108+
throw SysError("creating directory '%s'", p.string());
109+
110+
return;
111+
}
112+
#endif
113+
76114
if (!std::filesystem::create_directory(p))
77115
throw Error("path '%s' already exists", p.string());
116+
117+
#ifndef _WIN32
118+
if (path.isRoot()) {
119+
assert(!dirFd); // Handled above
120+
121+
/* Open directory for further *at operations relative to the sink root
122+
directory. */
123+
dirFd = open(p.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
124+
if (!dirFd)
125+
throw SysError("creating directory '%1%'", p.string());
126+
}
127+
#endif
78128
};
79129

80130
struct RestoreRegularFile : CreateRegularFileSink
@@ -114,7 +164,14 @@ void RestoreSink::createRegularFile(const CanonPath & path, std::function<void(C
114164
FILE_ATTRIBUTE_NORMAL,
115165
NULL)
116166
#else
117-
open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)
167+
[&]() {
168+
/* O_EXCL together with O_CREAT ensures symbolic links in the last
169+
component are not followed. */
170+
constexpr int flags = O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC;
171+
if (!dirFd)
172+
return ::open(p.c_str(), flags, 0666);
173+
return ::openat(dirFd.get(), path.rel_c_str(), flags, 0666);
174+
}();
118175
#endif
119176
;
120177
if (!crf.fd)
@@ -161,6 +218,13 @@ void RestoreRegularFile::operator()(std::string_view data)
161218
void RestoreSink::createSymlink(const CanonPath & path, const std::string & target)
162219
{
163220
auto p = append(dstPath, path);
221+
#ifndef _WIN32
222+
if (dirFd) {
223+
if (::symlinkat(requireCString(target), dirFd.get(), path.rel_c_str()) == -1)
224+
throw SysError("creating symlink from '%1%' -> '%2%'", p.string(), target);
225+
return;
226+
}
227+
#endif
164228
nix::createSymlink(target, p.string());
165229
}
166230

src/libutil/include/nix/util/fs-sink.hh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ struct FileSystemObjectSink
3636

3737
virtual void createDirectory(const CanonPath & path) = 0;
3838

39+
using DirectoryCreatedCallback = std::function<void(FileSystemObjectSink & dirSink, const CanonPath & dirRelPath)>;
40+
41+
/**
42+
* Create a directory and invoke a callback with a pair of sink + CanonPath
43+
* of the created subdirectory relative to dirSink.
44+
*
45+
* @note This allows for UNIX RestoreSink implementations to implement
46+
* *at-style accessors that always keep an open file descriptor for the
47+
* freshly created directory. Use this when it's important to disallow any
48+
* intermediate path components from being symlinks.
49+
*/
50+
virtual void createDirectory(const CanonPath & path, DirectoryCreatedCallback callback)
51+
{
52+
createDirectory(path);
53+
callback(*this, path);
54+
}
55+
3956
/**
4057
* This function in general is no re-entrant. Only one file can be
4158
* written at a time.
@@ -82,6 +99,18 @@ struct NullFileSystemObjectSink : FileSystemObjectSink
8299
struct RestoreSink : FileSystemObjectSink
83100
{
84101
std::filesystem::path dstPath;
102+
#ifndef _WIN32
103+
/**
104+
* File descriptor for the directory located at dstPath. Used for *at
105+
* operations relative to this file descriptor. This sink must *never*
106+
* follow intermediate symlinks (starting from dstPath) in case a file
107+
* collision is encountered for various reasons like case-insensitivity or
108+
* other types on normalization. using appropriate *at system calls and traversing
109+
* only one path component at a time ensures that writing is race-free and is
110+
* is not susceptible to symlink replacement.
111+
*/
112+
AutoCloseFD dirFd;
113+
#endif
85114
bool startFsync = false;
86115

87116
explicit RestoreSink(bool startFsync)
@@ -91,6 +120,10 @@ struct RestoreSink : FileSystemObjectSink
91120

92121
void createDirectory(const CanonPath & path) override;
93122

123+
#ifndef _WIN32
124+
void createDirectory(const CanonPath & path, DirectoryCreatedCallback callback) override;
125+
#endif
126+
94127
void createRegularFile(const CanonPath & path, std::function<void(CreateRegularFileSink &)>) override;
95128

96129
void createSymlink(const CanonPath & path, const std::string & target) override;

src/libutil/include/nix/util/strings.hh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,10 @@ public:
166166
}
167167
};
168168

169+
/**
170+
* Check that the string does not contain any NUL bytes and return c_str().
171+
* @throws Error if str contains '\0' bytes.
172+
*/
173+
const char * requireCString(const std::string & str);
174+
169175
} // namespace nix

src/libutil/strings.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "nix/util/strings-inline.hh"
66
#include "nix/util/os-string.hh"
77
#include "nix/util/error.hh"
8+
#include "nix/util/util.hh"
89

910
namespace nix {
1011

@@ -152,4 +153,14 @@ std::string optionalBracket(std::string_view prefix, std::string_view content, s
152153
return result;
153154
}
154155

156+
const char * requireCString(const std::string & s)
157+
{
158+
if (std::memchr(s.data(), '\0', s.size())) [[unlikely]] {
159+
using namespace std::string_view_literals;
160+
auto str = replaceStrings(s, "\0"sv, ""sv);
161+
throw Error("string '%s' with null (\\0) bytes used where it's not allowed", str);
162+
}
163+
return s.c_str();
164+
}
165+
155166
} // namespace nix

src/libutil/url.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,11 @@ Path renderUrlPathEnsureLegal(const std::vector<std::string> & urlPath)
327327
/* This is only really valid for UNIX. Windows has more restrictions. */
328328
if (comp.contains('/'))
329329
throw BadURL("URL path component '%s' contains '/', which is not allowed in file names", comp);
330-
if (comp.contains(char(0)))
331-
throw BadURL("URL path component '%s' contains NUL byte which is not allowed", comp);
330+
if (comp.contains(char(0))) {
331+
using namespace std::string_view_literals;
332+
auto str = replaceStrings(comp, "\0"sv, ""sv);
333+
throw BadURL("URL path component '%s' contains NUL byte which is not allowed", str);
334+
}
332335
}
333336

334337
return concatStringsSep("/", urlPath);

tests/functional/nars.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ if (( unicodeTestCode == 1 )); then
114114
# If the command failed (MacOS or ZFS + normalization), checks that it failed
115115
# with the expected "already exists" error, and that this is the same
116116
# behavior as `touch`
117-
echo "$unicodeTestOut" | grepQuiet "path '.*/out/â' already exists"
117+
echo "$unicodeTestOut" | grepQuiet "creating directory '.*/out/â': File exists"
118118

119119
(( touchFilesCount == 1 ))
120120
elif (( unicodeTestCode == 0 )); then

0 commit comments

Comments
 (0)