Skip to content

Commit b474e8d

Browse files
authored
Merge pull request #14935 from NixOS/delete-path-fchmod
libutil: Implement unix::fchmodatTryNoFollow, use in deletePath
2 parents afc6c24 + 9a63752 commit b474e8d

8 files changed

Lines changed: 298 additions & 67 deletions

File tree

src/libutil-tests/file-system.cc

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#include "nix/util/fs-sink.hh"
21
#include "nix/util/util.hh"
2+
#include "nix/util/serialise.hh"
33
#include "nix/util/types.hh"
44
#include "nix/util/file-system.hh"
55
#include "nix/util/processes.hh"
@@ -319,68 +319,6 @@ TEST(DirectoryIterator, nonexistent)
319319
ASSERT_THROW(DirectoryIterator("/schnitzel/darmstadt/pommes"), SysError);
320320
}
321321

322-
/* ----------------------------------------------------------------------------
323-
* openFileEnsureBeneathNoSymlinks
324-
* --------------------------------------------------------------------------*/
325-
326-
#ifndef _WIN32
327-
328-
TEST(openFileEnsureBeneathNoSymlinks, works)
329-
{
330-
std::filesystem::path tmpDir = nix::createTempDir();
331-
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);
332-
using namespace nix::unix;
333-
334-
{
335-
RestoreSink sink(/*startFsync=*/false);
336-
sink.dstPath = tmpDir;
337-
sink.dirFd = openDirectory(tmpDir);
338-
sink.createDirectory(CanonPath("a"));
339-
sink.createDirectory(CanonPath("c"));
340-
sink.createDirectory(CanonPath("c/d"));
341-
sink.createRegularFile(CanonPath("c/d/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); });
342-
sink.createSymlink(CanonPath("a/absolute_symlink"), tmpDir.string());
343-
sink.createSymlink(CanonPath("a/relative_symlink"), "../.");
344-
sink.createSymlink(CanonPath("a/broken_symlink"), "./nonexistent");
345-
sink.createDirectory(CanonPath("a/b"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {
346-
dirSink.createDirectory(CanonPath("d"));
347-
dirSink.createSymlink(CanonPath("c"), "./d");
348-
});
349-
sink.createDirectory(CanonPath("a/b/c/e")); // FIXME: This still follows symlinks
350-
ASSERT_THROW(
351-
sink.createDirectory(
352-
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
353-
SymlinkNotAllowed);
354-
ASSERT_THROW(
355-
sink.createRegularFile(
356-
CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }),
357-
SymlinkNotAllowed);
358-
}
359-
360-
AutoCloseFD dirFd = openDirectory(tmpDir);
361-
362-
auto open = [&](std::string_view path, int flags, mode_t mode = 0) {
363-
return openFileEnsureBeneathNoSymlinks(dirFd.get(), CanonPath(path), flags, mode);
364-
};
365-
366-
EXPECT_THROW(open("a/absolute_symlink", O_RDONLY), SymlinkNotAllowed);
367-
EXPECT_THROW(open("a/relative_symlink", O_RDONLY), SymlinkNotAllowed);
368-
EXPECT_THROW(open("a/absolute_symlink/a", O_RDONLY), SymlinkNotAllowed);
369-
EXPECT_THROW(open("a/absolute_symlink/c/d", O_RDONLY), SymlinkNotAllowed);
370-
EXPECT_THROW(open("a/relative_symlink/c", O_RDONLY), SymlinkNotAllowed);
371-
EXPECT_THROW(open("a/b/c/d", O_RDONLY), SymlinkNotAllowed);
372-
EXPECT_EQ(open("a/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), INVALID_DESCRIPTOR);
373-
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
374-
EXPECT_EQ(errno, EEXIST);
375-
EXPECT_THROW(open("a/absolute_symlink/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), SymlinkNotAllowed);
376-
EXPECT_EQ(open("c/d/regular/a", O_RDONLY), INVALID_DESCRIPTOR);
377-
EXPECT_EQ(open("c/d/regular", O_RDONLY | O_DIRECTORY), INVALID_DESCRIPTOR);
378-
EXPECT_TRUE(AutoCloseFD{open("c/d/regular", O_RDONLY)});
379-
EXPECT_TRUE(AutoCloseFD{open("a/regular", O_CREAT | O_WRONLY | O_EXCL, 0666)});
380-
}
381-
382-
#endif
383-
384322
/* ----------------------------------------------------------------------------
385323
* createAnonymousTempFile
386324
* --------------------------------------------------------------------------*/

src/libutil-tests/meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ sources = files(
8787
'xml-writer.cc',
8888
)
8989

90+
if host_machine.system() != 'windows'
91+
subdir('unix')
92+
endif
93+
9094
include_dirs = [ include_directories('.') ]
9195

9296

src/libutil-tests/package.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mkMesonExecutable (finalAttrs: {
3232
../../.version
3333
./.version
3434
./meson.build
35+
./unix/meson.build
3536
# ./meson.options
3637
(fileset.fileFilter (file: file.hasExt "cc") ./.)
3738
(fileset.fileFilter (file: file.hasExt "hh") ./.)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#include <gtest/gtest.h>
2+
3+
#include "nix/util/file-descriptor.hh"
4+
#include "nix/util/file-system.hh"
5+
#include "nix/util/fs-sink.hh"
6+
#include "nix/util/processes.hh"
7+
8+
#ifdef __linux__
9+
# include "nix/util/linux-namespaces.hh"
10+
11+
# include <sys/mount.h>
12+
#endif
13+
14+
#include <cstring>
15+
16+
namespace nix {
17+
18+
using namespace nix::unix;
19+
20+
/* ----------------------------------------------------------------------------
21+
* openFileEnsureBeneathNoSymlinks
22+
* --------------------------------------------------------------------------*/
23+
24+
TEST(openFileEnsureBeneathNoSymlinks, works)
25+
{
26+
std::filesystem::path tmpDir = nix::createTempDir();
27+
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);
28+
29+
{
30+
RestoreSink sink(/*startFsync=*/false);
31+
sink.dstPath = tmpDir;
32+
sink.dirFd = openDirectory(tmpDir);
33+
sink.createDirectory(CanonPath("a"));
34+
sink.createDirectory(CanonPath("c"));
35+
sink.createDirectory(CanonPath("c/d"));
36+
sink.createRegularFile(CanonPath("c/d/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); });
37+
sink.createSymlink(CanonPath("a/absolute_symlink"), tmpDir.string());
38+
sink.createSymlink(CanonPath("a/relative_symlink"), "../.");
39+
sink.createSymlink(CanonPath("a/broken_symlink"), "./nonexistent");
40+
sink.createDirectory(CanonPath("a/b"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {
41+
dirSink.createDirectory(CanonPath("d"));
42+
dirSink.createSymlink(CanonPath("c"), "./d");
43+
});
44+
sink.createDirectory(CanonPath("a/b/c/e")); // FIXME: This still follows symlinks
45+
ASSERT_THROW(
46+
sink.createDirectory(
47+
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
48+
SymlinkNotAllowed);
49+
ASSERT_THROW(
50+
sink.createRegularFile(
51+
CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }),
52+
SymlinkNotAllowed);
53+
}
54+
55+
AutoCloseFD dirFd = openDirectory(tmpDir);
56+
57+
auto open = [&](std::string_view path, int flags, mode_t mode = 0) {
58+
return openFileEnsureBeneathNoSymlinks(dirFd.get(), CanonPath(path), flags, mode);
59+
};
60+
61+
EXPECT_THROW(open("a/absolute_symlink", O_RDONLY), SymlinkNotAllowed);
62+
EXPECT_THROW(open("a/relative_symlink", O_RDONLY), SymlinkNotAllowed);
63+
EXPECT_THROW(open("a/absolute_symlink/a", O_RDONLY), SymlinkNotAllowed);
64+
EXPECT_THROW(open("a/absolute_symlink/c/d", O_RDONLY), SymlinkNotAllowed);
65+
EXPECT_THROW(open("a/relative_symlink/c", O_RDONLY), SymlinkNotAllowed);
66+
EXPECT_THROW(open("a/b/c/d", O_RDONLY), SymlinkNotAllowed);
67+
EXPECT_EQ(open("a/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), INVALID_DESCRIPTOR);
68+
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
69+
EXPECT_EQ(errno, EEXIST);
70+
EXPECT_THROW(open("a/absolute_symlink/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), SymlinkNotAllowed);
71+
EXPECT_EQ(open("c/d/regular/a", O_RDONLY), INVALID_DESCRIPTOR);
72+
EXPECT_EQ(open("c/d/regular", O_RDONLY | O_DIRECTORY), INVALID_DESCRIPTOR);
73+
EXPECT_TRUE(AutoCloseFD{open("c/d/regular", O_RDONLY)});
74+
EXPECT_TRUE(AutoCloseFD{open("a/regular", O_CREAT | O_WRONLY | O_EXCL, 0666)});
75+
}
76+
77+
/* ----------------------------------------------------------------------------
78+
* fchmodatTryNoFollow
79+
* --------------------------------------------------------------------------*/
80+
81+
TEST(fchmodatTryNoFollow, works)
82+
{
83+
std::filesystem::path tmpDir = nix::createTempDir();
84+
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);
85+
86+
{
87+
RestoreSink sink(/*startFsync=*/false);
88+
sink.dstPath = tmpDir;
89+
sink.dirFd = openDirectory(tmpDir);
90+
sink.createRegularFile(CanonPath("file"), [](CreateRegularFileSink & crf) {});
91+
sink.createDirectory(CanonPath("dir"));
92+
sink.createSymlink(CanonPath("filelink"), "file");
93+
sink.createSymlink(CanonPath("dirlink"), "dir");
94+
}
95+
96+
ASSERT_EQ(chmod((tmpDir / "file").c_str(), 0644), 0);
97+
ASSERT_EQ(chmod((tmpDir / "dir").c_str(), 0755), 0);
98+
99+
AutoCloseFD dirFd = openDirectory(tmpDir);
100+
ASSERT_TRUE(dirFd);
101+
102+
struct ::stat st;
103+
104+
/* Check that symlinks are not followed and targets are not changed. */
105+
106+
EXPECT_NO_THROW(
107+
try { fchmodatTryNoFollow(dirFd.get(), CanonPath("filelink"), 0777); } catch (SysError & e) {
108+
if (e.errNo != EOPNOTSUPP)
109+
throw;
110+
});
111+
ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0);
112+
EXPECT_EQ(st.st_mode & 0777, 0644);
113+
114+
EXPECT_NO_THROW(
115+
try { fchmodatTryNoFollow(dirFd.get(), CanonPath("dirlink"), 0777); } catch (SysError & e) {
116+
if (e.errNo != EOPNOTSUPP)
117+
throw;
118+
});
119+
ASSERT_EQ(stat((tmpDir / "dir").c_str(), &st), 0);
120+
EXPECT_EQ(st.st_mode & 0777, 0755);
121+
122+
/* Check fchmodatTryNoFollow works on regular files and directories. */
123+
124+
EXPECT_NO_THROW(fchmodatTryNoFollow(dirFd.get(), CanonPath("file"), 0600));
125+
ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0);
126+
EXPECT_EQ(st.st_mode & 0777, 0600);
127+
128+
EXPECT_NO_THROW((fchmodatTryNoFollow(dirFd.get(), CanonPath("dir"), 0700), 0));
129+
ASSERT_EQ(stat((tmpDir / "dir").c_str(), &st), 0);
130+
EXPECT_EQ(st.st_mode & 0777, 0700);
131+
}
132+
133+
#ifdef __linux__
134+
135+
TEST(fchmodatTryNoFollow, fallbackWithoutProc)
136+
{
137+
if (!userNamespacesSupported())
138+
GTEST_SKIP() << "User namespaces not supported";
139+
140+
std::filesystem::path tmpDir = nix::createTempDir();
141+
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);
142+
143+
{
144+
RestoreSink sink(/*startFsync=*/false);
145+
sink.dstPath = tmpDir;
146+
sink.dirFd = openDirectory(tmpDir);
147+
sink.createRegularFile(CanonPath("file"), [](CreateRegularFileSink & crf) {});
148+
sink.createSymlink(CanonPath("link"), "file");
149+
}
150+
151+
ASSERT_EQ(chmod((tmpDir / "file").c_str(), 0644), 0);
152+
153+
Pid pid = startProcess(
154+
[&] {
155+
if (unshare(CLONE_NEWNS) == -1)
156+
_exit(1);
157+
158+
if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1)
159+
_exit(1);
160+
161+
if (mount("tmpfs", "/proc", "tmpfs", 0, 0) == -1)
162+
_exit(1);
163+
164+
AutoCloseFD dirFd = openDirectory(tmpDir);
165+
if (!dirFd)
166+
exit(1);
167+
168+
try {
169+
fchmodatTryNoFollow(dirFd.get(), CanonPath("file"), 0600);
170+
} catch (SysError & e) {
171+
_exit(1);
172+
}
173+
174+
try {
175+
fchmodatTryNoFollow(dirFd.get(), CanonPath("link"), 0777);
176+
} catch (SysError & e) {
177+
if (e.errNo == EOPNOTSUPP)
178+
_exit(0); /* Success. */
179+
}
180+
181+
_exit(1); /* Didn't throw the expected exception. */
182+
},
183+
{.cloneFlags = CLONE_NEWUSER});
184+
185+
int status = pid.wait();
186+
ASSERT_TRUE(statusOk(status));
187+
188+
struct ::stat st;
189+
ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0);
190+
EXPECT_EQ(st.st_mode & 0777, 0600);
191+
}
192+
#endif
193+
194+
} // namespace nix

src/libutil-tests/unix/meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sources += files(
2+
'file-descriptor.cc',
3+
)

src/libutil/include/nix/util/file-descriptor.hh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,17 @@ namespace unix {
257257
*/
258258
Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode = 0);
259259

260+
/**
261+
* Try to change the mode of file named by \ref path relative to the parent directory denoted by \ref dirFd.
262+
*
263+
* @note When on linux without fchmodat2 support and without procfs mounted falls back to fchmodat without
264+
* AT_SYMLINK_NOFOLLOW, since it's the best we can do without failing.
265+
*
266+
* @pre path.isRoot() is false
267+
* @throws SysError if any operation fails
268+
*/
269+
void fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t mode);
270+
260271
} // namespace unix
261272
#endif
262273

0 commit comments

Comments
 (0)