1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
|
From 41d076ace558cfae01e233f9746dc955dcf78dba Mon Sep 17 00:00:00 2001
From: Andrey Semashev <andrey.semashev@gmail.com>
Date: Sun, 30 Jan 2022 23:41:06 +0300
Subject: [PATCH] Added protection for CVE-2022-21658 in remove_all on POSIX
systems.
Another process could replace the directory being processed by remove_all
with a symlink after remove_all called symlink_status but before
it creates a directory iterator. As a result, remove_all would remove
the linked directory contents instead of removing the symlink.
On POSIX systems that support fdopendir and O_NOFOLLOW flag for open(2),
this can be prevented by opening the directory with O_NOFOLLOW before
iterating. This will fail if the directory was replaced with a symlink.
No protection on other systems.
Reported in https://github.com/boostorg/filesystem/issues/224.
---
libs/filesystem/CMakeLists.txt | 4 ++
libs/filesystem/build/Jamfile.v2 | 1 +
libs/filesystem/config/Jamfile.v2 | 2 +
libs/filesystem/config/has_fdopendir_nofollow.cpp | 20 ++++++
boost/filesystem/directory.hpp | 3 +-
libs/filesystem/src/directory.cpp | 53 ++++++++++++++--
libs/filesystem/src/operations.cpp | 86 ++++++++++++++++----------
7 files changed, 131 insertions(+), 38 deletions(-)
create mode 100644 config/has_fdopendir_nofollow.cpp
diff --git a/libs/filesystem/CMakeLists.txt b/libs/filesystem/CMakeLists.txt
index 5a8678309..37ef7e161 100644
--- a/libs/filesystem/CMakeLists.txt
+++ b/libs/filesystem/CMakeLists.txt
@@ -59,6 +59,7 @@ if(NOT BOOST_FILESYSTEM_DISABLE_STATX)
check_cxx_source_compiles("#include <${CMAKE_CURRENT_SOURCE_DIR}/config/has_statx_syscall.cpp>" BOOST_FILESYSTEM_HAS_STATX_SYSCALL)
endif()
endif()
+check_cxx_source_compiles("#include <${CMAKE_CURRENT_SOURCE_DIR}/config/has_fdopendir_nofollow.cpp>" BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
if(WIN32 AND NOT BOOST_FILESYSTEM_DISABLE_BCRYPT)
set(CMAKE_REQUIRED_LIBRARIES bcrypt)
check_cxx_source_compiles("#include <${CMAKE_CURRENT_SOURCE_DIR}/config/has_bcrypt.cpp>" BOOST_FILESYSTEM_HAS_BCRYPT)
@@ -170,6 +171,9 @@ endif()
if(BOOST_FILESYSTEM_HAS_STATX_SYSCALL)
target_compile_definitions(boost_filesystem PRIVATE BOOST_FILESYSTEM_HAS_STATX_SYSCALL)
endif()
+if(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+ target_compile_definitions(boost_filesystem PRIVATE BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+endif()
target_link_libraries(boost_filesystem
PUBLIC
diff --git a/libs/filesystem/build/Jamfile.v2 b/libs/filesystem/build/Jamfile.v2
index 6ab58ddb8..02e3b8971 100644
--- a/libs/filesystem/build/Jamfile.v2
+++ b/libs/filesystem/build/Jamfile.v2
@@ -112,6 +112,7 @@ project boost/filesystem
[ check-target-builds ../config//has_stat_st_birthtim "has stat::st_birthtim" : <define>BOOST_FILESYSTEM_HAS_STAT_ST_BIRTHTIM ]
[ check-target-builds ../config//has_stat_st_birthtimensec "has stat::st_birthtimensec" : <define>BOOST_FILESYSTEM_HAS_STAT_ST_BIRTHTIMENSEC ]
[ check-target-builds ../config//has_stat_st_birthtimespec "has stat::st_birthtimespec" : <define>BOOST_FILESYSTEM_HAS_STAT_ST_BIRTHTIMESPEC ]
+ [ check-target-builds ../config//has_fdopendir_nofollow "has fdopendir(O_NOFOLLOW)" : <define>BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW ]
<conditional>@check-statx
<conditional>@select-windows-crypto-api
<conditional>@check-cxx20-atomic-ref
diff --git a/libs/filesystem/config/Jamfile.v2 b/libs/filesystem/config/Jamfile.v2
index 2e9ee4bd6..5ea7dc6c7 100644
--- a/libs/filesystem/config/Jamfile.v2
+++ b/libs/filesystem/config/Jamfile.v2
@@ -27,6 +27,8 @@ obj has_stat_st_birthtimensec : has_stat_st_birthtimensec.cpp : <include>../src
explicit has_stat_st_birthtimensec ;
obj has_stat_st_birthtimespec : has_stat_st_birthtimespec.cpp : <include>../src ;
explicit has_stat_st_birthtimespec ;
+obj has_fdopendir_nofollow : has_fdopendir_nofollow.cpp : <include>../src ;
+explicit has_fdopendir_nofollow ;
lib bcrypt ;
explicit bcrypt ;
diff --git a/libs/filesystem/config/has_fdopendir_nofollow.cpp b/libs/filesystem/config/has_fdopendir_nofollow.cpp
new file mode 100644
index 000000000..009a73084
--- /dev/null
+++ b/libs/filesystem/config/has_fdopendir_nofollow.cpp
@@ -0,0 +1,20 @@
+// Copyright 2022 Andrey Semashev
+
+// Distributed under the Boost Software License, Version 1.0.
+// See http://www.boost.org/LICENSE_1_0.txt
+
+// See library home page at http://www.boost.org/libs/filesystem
+
+#include "platform_config.hpp"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <fcntl.h>
+
+int main()
+{
+ int fd = open(".", O_DIRECTORY | O_RDONLY | O_NOFOLLOW);
+ DIR* dir = fdopendir(fd);
+ return dir != 0;
+}
diff --git a/boost/filesystem/directory.hpp b/boost/filesystem/directory.hpp
index f487633fc..fa1e69be5 100644
--- a/boost/filesystem/directory.hpp
+++ b/boost/filesystem/directory.hpp
@@ -247,7 +247,8 @@ BOOST_SCOPED_ENUM_UT_DECLARE_BEGIN(directory_options, unsigned int)
skip_dangling_symlinks = 1u << 2, // non-standard extension for recursive_directory_iterator: don't follow dangling directory symlinks,
pop_on_error = 1u << 3, // non-standard extension for recursive_directory_iterator: instead of producing an end iterator on errors,
// repeatedly invoke pop() until it succeeds or the iterator becomes equal to end iterator
- _detail_no_push = 1u << 4 // internal use only
+ _detail_no_follow = 1u << 4, // internal use only
+ _detail_no_push = 1u << 5 // internal use only
}
BOOST_SCOPED_ENUM_DECLARE_END(directory_options)
diff --git a/libs/filesystem/src/directory.cpp b/libs/filesystem/src/directory.cpp
index 554277580..8b41ae5e1 100644
--- a/libs/filesystem/src/directory.cpp
+++ b/libs/filesystem/src/directory.cpp
@@ -2,7 +2,7 @@
// Copyright 2002-2009, 2014 Beman Dawes
// Copyright 2001 Dietmar Kuehl
-// Copyright 2019 Andrey Semashev
+// Copyright 2019, 2022 Andrey Semashev
// Distributed under the Boost Software License, Version 1.0.
// See http://www.boost.org/LICENSE_1_0.txt
@@ -33,6 +33,8 @@
#ifdef BOOST_POSIX_API
+#include <sys/types.h>
+#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
@@ -47,6 +49,14 @@
#define BOOST_FILESYSTEM_USE_READDIR_R
#endif
+// At least Mac OS X 10.6 and older doesn't support O_CLOEXEC
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#define BOOST_FILESYSTEM_NO_O_CLOEXEC
+#endif
+
+#include "posix_tools.hpp"
+
#else // BOOST_WINDOWS_API
#include <cwchar>
@@ -206,13 +216,46 @@ inline std::size_t path_max()
#endif // BOOST_FILESYSTEM_USE_READDIR_R
-error_code dir_itr_first(void*& handle, void*& buffer, const char* dir, std::string& target, fs::file_status&, fs::file_status&)
+error_code dir_itr_first(void*& handle, void*& buffer, const char* dir, std::string& target, unsigned int opts, fs::file_status&, fs::file_status&)
{
- if ((handle = ::opendir(dir)) == 0)
+#if defined(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+ int flags = O_DIRECTORY | O_RDONLY | O_NDELAY | O_CLOEXEC;
+ if ((opts & static_cast< unsigned int >(directory_options::_detail_no_follow)) != 0u)
+ flags |= O_NOFOLLOW;
+
+ int fd = ::open(dir, flags);
+ if (BOOST_UNLIKELY(fd < 0))
+ {
+ const int err = errno;
+ return error_code(err, system_category());
+ }
+
+#if defined(BOOST_FILESYSTEM_NO_O_CLOEXEC) && defined(FD_CLOEXEC)
+ int res = ::fcntl(fd, F_SETFD, FD_CLOEXEC);
+ if (BOOST_UNLIKELY(res < 0))
+ {
+ const int err = errno;
+ close_fd(fd);
+ return error_code(err, system_category());
+ }
+#endif
+
+ handle = ::fdopendir(fd);
+ if (BOOST_UNLIKELY(!handle))
{
const int err = errno;
+ close_fd(fd);
return error_code(err, system_category());
}
+#else // defined(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+ handle = ::opendir(dir);
+ if (BOOST_UNLIKELY(!handle))
+ {
+ const int err = errno;
+ return error_code(err, system_category());
+ }
+#endif // defined(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+
target.assign("."); // string was static but caused trouble
// when iteration called from dtor, after
// static had already been destroyed
@@ -342,7 +385,7 @@ error_code dir_itr_increment(void*& handle, void*& buffer, std::string& target,
#else // BOOST_WINDOWS_API
-error_code dir_itr_first(void*& handle, fs::path const& dir, std::wstring& target, fs::file_status& sf, fs::file_status& symlink_sf)
+error_code dir_itr_first(void*& handle, fs::path const& dir, std::wstring& target, unsigned int opts, fs::file_status& sf, fs::file_status& symlink_sf)
// Note: an empty root directory has no "." or ".." entries, so this
// causes a ERROR_FILE_NOT_FOUND error which we do not considered an
// error. It is treated as eof instead.
@@ -512,7 +555,7 @@ void directory_iterator_construct(directory_iterator& it, path const& p, unsigne
#if defined(BOOST_POSIX_API)
imp->buffer,
#endif
- p.c_str(), filename, file_stat, symlink_file_stat);
+ p.c_str(), filename, opts, file_stat, symlink_file_stat);
if (result)
{
diff --git a/libs/filesystem/src/operations.cpp b/libs/filesystem/src/operations.cpp
index e14b598b7..bf42105b9 100644
--- a/libs/filesystem/src/operations.cpp
+++ b/libs/filesystem/src/operations.cpp
@@ -945,51 +945,73 @@ inline bool remove_impl(path const& p, error_code* ec)
//! remove_all() implementation
uintmax_t remove_all_impl(path const& p, error_code* ec)
{
- fs::file_type type;
+ for (unsigned int attempt = 0u; attempt < 5u; ++attempt)
{
- error_code local_ec;
- type = fs::detail::symlink_status(p, &local_ec).type();
+ fs::file_type type;
+ {
+ error_code local_ec;
+ type = fs::detail::symlink_status(p, &local_ec).type();
- if (type == fs::file_not_found)
- return 0u;
+ if (type == fs::file_not_found)
+ return 0u;
- if (BOOST_UNLIKELY(type == fs::status_error))
- {
- if (!ec)
- BOOST_FILESYSTEM_THROW(filesystem_error("boost::filesystem::remove_all", p, local_ec));
+ if (BOOST_UNLIKELY(type == fs::status_error))
+ {
+ if (!ec)
+ BOOST_FILESYSTEM_THROW(filesystem_error("boost::filesystem::remove_all", p, local_ec));
- *ec = local_ec;
- return static_cast< uintmax_t >(-1);
+ *ec = local_ec;
+ return static_cast< uintmax_t >(-1);
+ }
}
- }
- uintmax_t count = 0u;
+ uintmax_t count = 0u;
- if (type == fs::directory_file) // but not a directory symlink
- {
- fs::directory_iterator itr;
- fs::detail::directory_iterator_construct(itr, p, static_cast< unsigned int >(directory_options::none), ec);
- if (ec && *ec)
- return static_cast< uintmax_t >(-1);
-
- const fs::directory_iterator end_dit;
- while (itr != end_dit)
+ if (type == fs::directory_file) // but not a directory symlink
{
- count += fs::detail::remove_all_impl(itr->path(), ec);
- if (ec && *ec)
- return static_cast< uintmax_t >(-1);
+ fs::directory_iterator itr;
+ error_code local_ec;
+ fs::detail::directory_iterator_construct(itr, p, static_cast< unsigned int >(directory_options::_detail_no_follow), &local_ec);
+ if (BOOST_UNLIKELY(!!local_ec))
+ {
+#if defined(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
+ // If open(2) with O_NOFOLLOW fails with ELOOP, this means that either the path contains a loop
+ // of symbolic links, or the last element of the path is a symbolic link. Given that lstat(2) above
+ // did not fail, most likely it is the latter case. I.e. between the lstat above and this open call
+ // the filesystem was modified so that the path no longer refers to a directory file (as opposed to a symlink).
+ if (local_ec == error_code(ELOOP, system_category()))
+ continue;
+#endif // defined(BOOST_FILESYSTEM_HAS_FDOPENDIR_NOFOLLOW)
- fs::detail::directory_iterator_increment(itr, ec);
- if (ec && *ec)
+ if (!ec)
+ BOOST_FILESYSTEM_THROW(filesystem_error("boost::filesystem::remove_all", p, local_ec));
+
+ *ec = local_ec;
return static_cast< uintmax_t >(-1);
+ }
+
+ const fs::directory_iterator end_dit;
+ while (itr != end_dit)
+ {
+ count += fs::detail::remove_all_impl(itr->path(), ec);
+ if (ec && *ec)
+ return static_cast< uintmax_t >(-1);
+
+ fs::detail::directory_iterator_increment(itr, ec);
+ if (ec && *ec)
+ return static_cast< uintmax_t >(-1);
+ }
}
- }
- count += fs::detail::remove_impl(p, type, ec);
- if (ec && *ec)
- return static_cast< uintmax_t >(-1);
+ count += fs::detail::remove_impl(p, type, ec);
+ if (ec && *ec)
+ return static_cast< uintmax_t >(-1);
- return count;
+ return count;
+ }
+
+ emit_error(ELOOP, p, ec, "boost::filesystem::remove_all: path cannot be opened as a directory");
+ return static_cast< uintmax_t >(-1);
}
#else // defined(BOOST_POSIX_API)
|