Skip to content

Commit 2100b42

Browse files
committed
#4489: Fix usage of fd-based functions to new api introduced earlier today
Also add an explicit test for safe implementation usage on supported platforms. As a side effect, this commit adds a module-level attribute 'rmtree_is_safe' which offers introspection whether the current rmtree implementation is safe against symlink attacks.
1 parent 1641cea commit 2100b42

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

Doc/library/shutil.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ Directory and files operations
195195
The default :func:`rmtree` function is susceptible to a symlink attack:
196196
given proper timing and circumstances, attackers can use it to delete
197197
files they wouldn't be able to access otherwise. Thus -- on platforms
198-
that support the necessary fd-based functions :func:`os.openat` and
199-
:func:`os.unlinkat` -- a safe version of :func:`rmtree` is used, which
200-
isn't vulnerable.
198+
that support the necessary fd-based functions -- a safe version of
199+
:func:`rmtree` is used, which isn't vulnerable. In this case
200+
:data:`rmtree_is_safe` is set to True.
201201

202202
If *onerror* is provided, it must be a callable that accepts three
203203
parameters: *function*, *path*, and *excinfo*.
@@ -210,8 +210,15 @@ Directory and files operations
210210

211211
.. versionchanged:: 3.3
212212
Added a safe version that is used automatically if platform supports
213-
the fd-based functions :func:`os.openat` and :func:`os.unlinkat`.
213+
fd-based functions.
214214

215+
.. data:: rmtree_is_safe
216+
217+
Indicates whether the current platform and implementation has a symlink
218+
attack-proof version of :func:`rmtree`. Currently this is only true for
219+
platforms supporting fd-based directory access functions.
220+
221+
.. versionadded:: 3.3
215222

216223
.. function:: move(src, dst)
217224

Lib/shutil.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,9 @@ def _rmtree_unsafe(path, onerror):
362362
_rmtree_unsafe(fullname, onerror)
363363
else:
364364
try:
365-
os.remove(fullname)
365+
os.unlink(fullname)
366366
except os.error:
367-
onerror(os.remove, fullname, sys.exc_info())
367+
onerror(os.unlink, fullname, sys.exc_info())
368368
try:
369369
os.rmdir(path)
370370
except os.error:
@@ -374,21 +374,21 @@ def _rmtree_unsafe(path, onerror):
374374
def _rmtree_safe_fd(topfd, path, onerror):
375375
names = []
376376
try:
377-
names = os.flistdir(topfd)
377+
names = os.listdir(topfd)
378378
except os.error:
379-
onerror(os.flistdir, path, sys.exc_info())
379+
onerror(os.listdir, path, sys.exc_info())
380380
for name in names:
381381
fullname = os.path.join(path, name)
382382
try:
383-
orig_st = os.fstatat(topfd, name)
383+
orig_st = os.stat(name, dir_fd=topfd)
384384
mode = orig_st.st_mode
385385
except os.error:
386386
mode = 0
387387
if stat.S_ISDIR(mode):
388388
try:
389-
dirfd = os.openat(topfd, name, os.O_RDONLY)
389+
dirfd = os.open(name, os.O_RDONLY, dir_fd=topfd)
390390
except os.error:
391-
onerror(os.openat, fullname, sys.exc_info())
391+
onerror(os.open, fullname, sys.exc_info())
392392
else:
393393
try:
394394
if os.path.samestat(orig_st, os.fstat(dirfd)):
@@ -397,21 +397,22 @@ def _rmtree_safe_fd(topfd, path, onerror):
397397
os.close(dirfd)
398398
else:
399399
try:
400-
os.unlinkat(topfd, name)
400+
os.unlink(name, dir_fd=topfd)
401401
except os.error:
402-
onerror(os.unlinkat, fullname, sys.exc_info())
402+
onerror(os.unlink, fullname, sys.exc_info())
403403
try:
404404
os.rmdir(path)
405405
except os.error:
406406
onerror(os.rmdir, path, sys.exc_info())
407407

408-
_use_fd_functions = hasattr(os, 'openat') and hasattr(os, 'unlinkat')
408+
rmtree_is_safe = _use_fd_functions = (os.unlink in os.supports_dir_fd and
409+
os.open in os.supports_dir_fd)
409410
def rmtree(path, ignore_errors=False, onerror=None):
410411
"""Recursively delete a directory tree.
411412
412413
If ignore_errors is set, errors are ignored; otherwise, if onerror
413414
is set, it is called to handle the error with arguments (func,
414-
path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
415+
path, exc_info) where func is platform and implementation dependent;
415416
path is the argument to that function that caused it to fail; and
416417
exc_info is a tuple returned by sys.exc_info(). If ignore_errors
417418
is false and onerror is None, an exception is raised.

Lib/test/test_shutil.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,7 @@ def check_args_to_onerror(self, func, arg, exc):
159159
# at os.listdir. The first failure may legally
160160
# be either.
161161
if 0 <= self.errorState < 2:
162-
if (func is os.remove or
163-
hasattr(os, 'unlinkat') and func is os.unlinkat):
162+
if func is os.unlink:
164163
self.assertIn(arg, [self.child_file_path, self.child_dir_path])
165164
else:
166165
if self.errorState == 1:
@@ -486,6 +485,26 @@ def test_copyfile_symlinks(self):
486485
shutil.copyfile(link, dst)
487486
self.assertFalse(os.path.islink(dst))
488487

488+
def test_rmtree_uses_safe_fd_version_if_available(self):
489+
if os.unlink in os.supports_dir_fd and os.open in os.supports_dir_fd:
490+
self.assertTrue(shutil._use_fd_functions)
491+
self.assertTrue(shutil.rmtree_is_safe)
492+
tmp_dir = self.mkdtemp()
493+
d = os.path.join(tmp_dir, 'a')
494+
os.mkdir(d)
495+
try:
496+
real_rmtree = shutil._rmtree_safe_fd
497+
class Called(Exception): pass
498+
def _raiser(*args, **kwargs):
499+
raise Called
500+
shutil._rmtree_safe_fd = _raiser
501+
self.assertRaises(Called, shutil.rmtree, d)
502+
finally:
503+
shutil._rmtree_safe_fd = real_rmtree
504+
else:
505+
self.assertFalse(shutil._use_fd_functions)
506+
self.assertFalse(shutil.rmtree_is_safe)
507+
489508
def test_rmtree_dont_delete_file(self):
490509
# When called on a file instead of a directory, don't delete it.
491510
handle, path = tempfile.mkstemp()

0 commit comments

Comments
 (0)