Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-83074: Add preserve_security_context to shutil #23720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ Directory and files operations
.. versionchanged:: 3.3
Added *follow_symlinks* argument.

.. function:: copystat(src, dst, *, follow_symlinks=True)
.. function:: copystat(src, dst, *, follow_symlinks=True, \
preserve_security_context=False)

Copy the permission bits, last access time, last modification time, and
flags from *src* to *dst*. On Linux, :func:`copystat` also copies the
Expand All @@ -123,6 +124,10 @@ Directory and files operations
*src* symbolic link, and writing the information to the
*dst* symbolic link.

If the optional flag *preserve_security_context* is set, extended
attributes in the security namespace are copied as well. By default,
attributes like ``security.selinux`` are ignored.

.. note::

Not all platforms provide the ability to examine and
Expand Down Expand Up @@ -155,6 +160,10 @@ Directory and files operations
.. versionchanged:: 3.3
Added *follow_symlinks* argument and support for Linux extended attributes.

.. versionchanged:: 3.10
Added *preserve_security_context* argument.
Skip xattrs in the security namespace by default.

.. function:: copy(src, dst, *, follow_symlinks=True)

Copies the file *src* to the file or directory *dst*. *src* and *dst*
Expand Down Expand Up @@ -186,7 +195,8 @@ Directory and files operations
copy the file more efficiently. See
:ref:`shutil-platform-dependent-efficient-copy-operations` section.

.. function:: copy2(src, dst, *, follow_symlinks=True)
.. function:: copy2(src, dst, *, follow_symlinks=True, \
preserve_security_context=False)

Identical to :func:`~shutil.copy` except that :func:`copy2`
also attempts to preserve file metadata.
Expand Down Expand Up @@ -218,6 +228,10 @@ Directory and files operations
copy the file more efficiently. See
:ref:`shutil-platform-dependent-efficient-copy-operations` section.

.. versionchanged:: 3.10
Added *preserve_security_context* argument.
Skip xattrs in the security namespace by default.

.. function:: ignore_patterns(*patterns)

This factory function creates a function that can be used as a callable for
Expand All @@ -227,7 +241,7 @@ Directory and files operations

.. function:: copytree(src, dst, symlinks=False, ignore=None, \
copy_function=copy2, ignore_dangling_symlinks=False, \
dirs_exist_ok=False)
dirs_exist_ok=False, preserve_security_context=False)

Recursively copy an entire directory tree rooted at *src* to a directory
named *dst* and return the destination directory. *dirs_exist_ok* dictates
Expand Down Expand Up @@ -286,6 +300,10 @@ Directory and files operations
.. versionadded:: 3.8
The *dirs_exist_ok* parameter.

.. versionchanged:: 3.10
Added *preserve_security_context* argument.
Skip xattrs in the security namespace by default.

.. function:: rmtree(path, ignore_errors=False, onerror=None)

.. index:: single: directory; deleting
Expand Down Expand Up @@ -334,7 +352,8 @@ Directory and files operations
.. versionadded:: 3.3


.. function:: move(src, dst, copy_function=copy2)
.. function:: move(src, dst, copy_function=copy2, \
preserve_security_context=False)

Recursively move a file or directory (*src*) to another location (*dst*)
and return the destination.
Expand Down Expand Up @@ -374,6 +393,10 @@ Directory and files operations
.. versionchanged:: 3.9
Accepts a :term:`path-like object` for both *src* and *dst*.

.. versionchanged:: 3.10
Added *preserve_security_context* argument.
Skip xattrs in the security namespace by default.

.. function:: disk_usage(path)

Return disk usage statistics about the given path as a :term:`named tuple`
Expand Down
73 changes: 58 additions & 15 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,21 +309,25 @@ def copymode(src, dst, *, follow_symlinks=True):
chmod_func(dst, stat.S_IMODE(st.st_mode))

if hasattr(os, 'listxattr'):
def _copyxattr(src, dst, *, follow_symlinks=True):
def _copyxattr(src, dst, *, follow_symlinks=True, names_filter=None):
"""Copy extended filesystem attributes from `src` to `dst`.

Overwrite existing attributes.

If `follow_symlinks` is false, symlinks won't be followed.

`names_filter` is a callback to filter xattr names.
"""

try:
names = os.listxattr(src, follow_symlinks=follow_symlinks)
except OSError as e:
if e.errno not in (errno.ENOTSUP, errno.ENODATA, errno.EINVAL):
raise
return
if not names:
return
if names_filter is not None:
names = names_filter(names)
for name in names:
try:
value = os.getxattr(src, name, follow_symlinks=follow_symlinks)
Expand All @@ -336,7 +340,8 @@ def _copyxattr(src, dst, *, follow_symlinks=True):
def _copyxattr(*args, **kwargs):
pass

def copystat(src, dst, *, follow_symlinks=True):
def copystat(src, dst, *, follow_symlinks=True,
preserve_security_context=False):
"""Copy file metadata

Copy the permission bits, last access time, last modification time, and
Expand All @@ -347,6 +352,14 @@ def copystat(src, dst, *, follow_symlinks=True):

If the optional flag `follow_symlinks` is not set, symlinks aren't
followed if and only if both `src` and `dst` are symlinks.

If the optional flag `preserve_security_context` is set, extended
attributes in the security namespace are copied as well. By default,
attributes like `security.selinux` are ignored.

`preserve_security_context=False` is equivalent to
`cp -p --preserve=xattr`. `preserve_security_context=True` behaves like
`cp -p --preserve=xattr,context`.
"""
sys.audit("shutil.copystat", src, dst)

Expand All @@ -368,6 +381,16 @@ def lookup(name):
return fn
return _nop

# don't copy security xattr by default
# see coreutils src/copy.c:check_selinux_attr()
if not preserve_security_context:
def names_filter(names):
return [
name for name in names if not name.startswith("security.")
]
else:
names_filter = None

if isinstance(src, os.DirEntry):
st = src.stat(follow_symlinks=follow)
else:
Expand All @@ -377,7 +400,7 @@ def lookup(name):
follow_symlinks=follow)
# We must copy extended attributes before the file is (potentially)
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
_copyxattr(src, dst, follow_symlinks=follow)
_copyxattr(src, dst, follow_symlinks=follow, names_filter=names_filter)
try:
lookup("chmod")(dst, mode, follow_symlinks=follow)
except NotImplementedError:
Expand Down Expand Up @@ -420,7 +443,7 @@ def copy(src, dst, *, follow_symlinks=True):
copymode(src, dst, follow_symlinks=follow_symlinks)
return dst

def copy2(src, dst, *, follow_symlinks=True):
def copy2(src, dst, *, follow_symlinks=True, preserve_security_context=False):
"""Copy data and metadata. Return the file's destination.

Metadata is copied with copystat(). Please see the copystat function
Expand All @@ -430,11 +453,16 @@ def copy2(src, dst, *, follow_symlinks=True):

If follow_symlinks is false, symlinks won't be followed. This
resembles GNU's "cp -P src dst".

If the optional flag `preserve_security_context` is set, extended
attributes in the security namespace are copied as well. By default,
attributes like `security.selinux` are ignored.
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst, follow_symlinks=follow_symlinks)
copystat(src, dst, follow_symlinks=follow_symlinks)
copystat(src, dst, follow_symlinks=follow_symlinks,
preserve_security_context=preserve_security_context)
return dst

def ignore_patterns(*patterns):
Expand All @@ -450,7 +478,8 @@ def _ignore_patterns(path, names):
return _ignore_patterns

def _copytree(entries, src, dst, symlinks, ignore, copy_function,
ignore_dangling_symlinks, dirs_exist_ok=False):
ignore_dangling_symlinks, dirs_exist_ok=False,
preserve_security_context=False):
if ignore is not None:
ignored_names = ignore(os.fspath(src), [x.name for x in entries])
else:
Expand Down Expand Up @@ -481,7 +510,8 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
# code with a custom `copy_function` may rely on copytree
# doing the right thing.
os.symlink(linkto, dstname)
copystat(srcobj, dstname, follow_symlinks=not symlinks)
copystat(srcobj, dstname, follow_symlinks=not symlinks,
preserve_security_context=preserve_security_context)
else:
# ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks:
Expand All @@ -494,7 +524,8 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
copy_function(srcobj, dstname)
elif srcentry.is_dir():
copytree(srcobj, dstname, symlinks, ignore, copy_function,
dirs_exist_ok=dirs_exist_ok)
dirs_exist_ok=dirs_exist_ok,
preserve_security_context=preserve_security_context)
else:
# Will raise a SpecialFileError for unsupported file types
copy_function(srcobj, dstname)
Expand All @@ -505,7 +536,7 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
except OSError as why:
errors.append((srcname, dstname, str(why)))
try:
copystat(src, dst)
copystat(src, dst, preserve_security_context=preserve_security_context)
except OSError as why:
# Copying file access times may fail on Windows
if getattr(why, 'winerror', None) is None:
Expand All @@ -515,7 +546,8 @@ def _copytree(entries, src, dst, symlinks, ignore, copy_function,
return dst

def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
ignore_dangling_symlinks=False, dirs_exist_ok=False):
ignore_dangling_symlinks=False, dirs_exist_ok=False,
preserve_security_context=False):
"""Recursively copy a directory tree and return the destination directory.

dirs_exist_ok dictates whether to raise an exception in case dst or any
Expand Down Expand Up @@ -551,14 +583,18 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
destination path as arguments. By default, copy2() is used, but any
function that supports the same signature (like copy()) can be used.

If the optional flag `preserve_security_context` is set, extended
attributes in the security namespace are copied as well. By default,
attributes like `security.selinux` are ignored.
"""
sys.audit("shutil.copytree", src, dst)
with os.scandir(src) as itr:
entries = list(itr)
return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks,
ignore=ignore, copy_function=copy_function,
ignore_dangling_symlinks=ignore_dangling_symlinks,
dirs_exist_ok=dirs_exist_ok)
dirs_exist_ok=dirs_exist_ok,
preserve_security_context=preserve_security_context)

if hasattr(os.stat_result, 'st_file_attributes'):
# Special handling for directory junctions to make them behave like
Expand Down Expand Up @@ -762,7 +798,7 @@ def _basename(path):
sep = os.path.sep + (os.path.altsep or '')
return os.path.basename(path.rstrip(sep))

def move(src, dst, copy_function=copy2):
def move(src, dst, copy_function=copy2, preserve_security_context=False):
"""Recursively move a file or directory to another location. This is
similar to the Unix "mv" command. Return the file or directory's
destination.
Expand Down Expand Up @@ -821,10 +857,17 @@ def move(src, dst, copy_function=copy2):
"'%s': Lacking write permission to '%s'."
% (src, src))
copytree(src, real_dst, copy_function=copy_function,
symlinks=True)
symlinks=True,
preserve_security_context=preserve_security_context)
rmtree(src)
else:
copy_function(src, real_dst)
if preserve_security_context:
copy_function(
src, real_dst,
preserve_security_context=preserve_security_context
)
else:
copy_function(src, real_dst)
os.unlink(src)
return real_dst

Expand Down
83 changes: 53 additions & 30 deletions Lib/test/support/os_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,38 +199,61 @@ def can_xattr():
if _can_xattr is not None:
return _can_xattr
if not hasattr(os, "setxattr"):
can = False
else:
import platform
tmp_dir = tempfile.mkdtemp()
tmp_fp, tmp_name = tempfile.mkstemp(dir=tmp_dir)
try:
with open(TESTFN, "wb") as fp:
try:
# TESTFN & tempfile may use different file systems with
# different capabilities
os.setxattr(tmp_fp, b"user.test", b"")
os.setxattr(tmp_name, b"trusted.foo", b"42")
os.setxattr(fp.fileno(), b"user.test", b"")
# Kernels < 2.6.39 don't respect setxattr flags.
kernel_version = platform.release()
m = re.match(r"2.6.(\d{1,2})", kernel_version)
can = m is None or int(m.group(1)) >= 39
except OSError:
can = False
finally:
unlink(TESTFN)
unlink(tmp_name)
rmdir(tmp_dir)
_can_xattr = can
return can
_can_xattr = False, False
return _can_xattr

import platform
# Kernels < 2.6.39 don't respect setxattr flags.
kernel_version = platform.release()
m = re.match(r"2.6.(\d{1,2})", kernel_version)
if m is not None and int(m.group(1)) < 39:
_can_xattr = False, False
return _can_xattr

def skip_unless_xattr(test):
"""Skip decorator for tests that require functional extended attributes"""
ok = can_xattr()
msg = "no non-broken extended attribute support"
return test if ok else unittest.skip(msg)(test)
try:
with open(TESTFN, "wb") as fp:
try:
os.setxattr(fp.fileno(), b"user.test", b"42")
os.setxattr(TESTFN, b"user.test", b"42")
except OSError:
can_user = False
else:
can_user = True
try:
# check write access to trusted namespace
os.setxattr(fp.fileno(), b"trusted.test", b"23")
os.setxattr(TESTFN, b"trusted.test", b"23")
except OSError:
can_trusted = False
else:
can_trusted = True
finally:
unlink(TESTFN)

_can_xattr = can_user, can_trusted
return _can_xattr


def skip_unless_xattr_user(test):
"""Skip decorator for tests that require functional extended attributes

Only verifies write access to "user" namespace. The "security", "system",
and "trusted" namespace are restricted. "trusted" is only writable to
processes with CAP_SYS_ADMIN.

Note: The decorator only checks the filesystem of CWD. /tmp on tmpfs does
not support "user" extended attributes.
"""
can_user, can_trusted = can_xattr()
msg = "xattr is not available or user namespace not writable"
return test if can_user else unittest.skip(msg)(test)


def skip_unless_xattr_trusted(test):
"""Skip decorator for tests that require write access to trusted xattr"""
can_user, can_trusted = can_xattr()
msg = "xattr is not available or trusted namespace not writable"
return test if can_trusted else unittest.skip(msg)(test)


def unlink(filename):
Expand Down
Loading