extract: fs flags: use get/set to influence only specific flags, #9039, macOS only.

preserve UF_COMPRESSED and SF_DATALESS when restoring flags,
get-modify-set in macOS set_flags, keeping system-managed read-only flags.

(cherry picked from commit 83571aa00d)
This commit is contained in:
Thomas Waldmann 2025-10-29 20:56:29 +01:00
parent 73b6193cc5
commit 10ce4f4fd3
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
3 changed files with 87 additions and 0 deletions

View file

@ -40,3 +40,4 @@ elif is_darwin: # pragma: darwin only
from .darwin import API_VERSION as OS_API_VERSION
from .darwin import listxattr, getxattr, setxattr
from .darwin import acl_get, acl_set
from .darwin import set_flags

View file

@ -1,5 +1,6 @@
import os
from libc cimport errno
from libc.stdint cimport uint32_t
from .posix import user2uid, group2gid
@ -153,3 +154,65 @@ def acl_set(path, item, numeric_ids=False, fd=None):
acl_set_link_np(path, ACL_TYPE_EXTENDED, acl)
finally:
acl_free(acl)
# macOS flags handling: only modify flags documented as settable; preserve all others, see #9090.
# The man page states UF_COMPRESSED and SF_DATALESS are internal flags and must not be modified
# from user space. We therefore only modify flags that are documented to be settable by owner or
# super-user and preserve everything else (including unknown or future flags).
cdef extern from "sys/stat.h":
int chflags(const char *path, uint32_t flags)
int lchflags(const char *path, uint32_t flags)
int fchflags(int fd, uint32_t flags)
# Known-good settable flags from macOS chflags(2). We intentionally do NOT include
# internal flags like UF_COMPRESSED and SF_DATALESS. Resolved once at import time.
# getattr(..., 0) keeps this importable on non-Darwin platforms or Python versions
# missing some constants.
import stat as stat_mod
SETTABLE_FLAG_NAMES = (
# Owner-settable (UF_*)
'UF_NODUMP',
'UF_IMMUTABLE',
'UF_APPEND',
'UF_OPAQUE',
'UF_NOUNLINK',
'UF_HIDDEN',
# Super-user-settable (SF_*)
'SF_ARCHIVED',
'SF_IMMUTABLE',
'SF_APPEND',
# SF_NOUNLINK exists on some BSDs; include defensively
'SF_NOUNLINK',
)
cdef uint32_t SETTABLE_FLAGS_MASK = 0
for _name in SETTABLE_FLAG_NAMES:
SETTABLE_FLAGS_MASK |= <uint32_t> getattr(stat_mod, _name, 0)
def set_flags(path, bsd_flags, fd=None):
"""Set BSD-style flags on macOS, preserving system-managed read-only flags."""
# Determine current flags.
try:
if fd is not None:
st = os.fstat(fd)
else:
st = os.lstat(path)
current = st.st_flags
except (OSError, AttributeError):
# We can't determine the current flags, so better give up than corrupting anything.
return
new_flags = (current & ~SETTABLE_FLAGS_MASK) | (bsd_flags & SETTABLE_FLAGS_MASK)
# Apply flags.
cdef uint32_t c_flags = <uint32_t> new_flags
if fd is not None:
if fchflags(fd, c_flags) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), path)
else:
path_bytes = os.fsencode(path)
if lchflags(path_bytes, c_flags) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))

View file

@ -1521,6 +1521,29 @@ class ArchiverTestCase(ArchiverTestCaseBase):
assert same_ts_ns(mtime_extracted, mtime_expected)
# assert same_ts_ns(atime_extracted, atime_expected) # still broken, but not really important.
@pytest.mark.skipif(not is_darwin, reason='only for macOS')
def test_extract_restores_append_flag(self):
if not has_lchflags or not hasattr(stat, 'UF_APPEND'):
pytest.skip('BSD flags or UF_APPEND not supported on this platform')
# create a file and set the append flag on it
self.create_regular_file('appendflag', size=1)
src_path = os.path.join(self.input_path, 'appendflag')
platform.set_flags(src_path, stat.UF_APPEND)
# Verify the flag actually got set; otherwise skip (filesystem may not support it)
st = os.lstat(src_path)
if (platform.get_flags(src_path, st) & stat.UF_APPEND) == 0:
pytest.skip('UF_APPEND not settable on this filesystem')
# archive and extract
self.cmd('init', '--encryption=repokey', self.repository_location)
archive = self.repository_location + '::test'
self.cmd('create', archive, 'input')
with changedir('output'):
self.cmd('extract', archive)
out_path = os.path.join('input', 'appendflag')
st2 = os.lstat(out_path)
flags = platform.get_flags(out_path, st2)
assert (flags & stat.UF_APPEND) == stat.UF_APPEND
def test_path_normalization(self):
self.cmd('init', '--encryption=repokey', self.repository_location)
self.create_regular_file('dir1/dir2/file', size=1024 * 80)