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 ad89feb0ce
commit 44ac21ff29
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
3 changed files with 95 additions and 1 deletions

View file

@ -35,7 +35,8 @@ elif is_darwin: # pragma: darwin only
from .darwin import listxattr, getxattr, setxattr
from .darwin import acl_get, acl_set
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
from .base import set_flags, get_flags
from .darwin import set_flags
from .base import get_flags
from .base import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import swidth

View file

@ -198,3 +198,67 @@ def _get_birthtime_ns(path, follow_symlinks=False):
if result != 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
return stat_info.st_birthtimespec.tv_sec * 1_000_000_000 + stat_info.st_birthtimespec.tv_nsec
# 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

@ -2,11 +2,13 @@ import errno
import os
import shutil
import time
import stat
from unittest.mock import patch
import pytest
from ... import xattr
from ... import platform
from ...chunkers import has_seek_hole
from ...constants import * # NOQA
from ...helpers import EXIT_WARNING, BackupPermissionError, bin_to_hex
@ -606,6 +608,33 @@ def test_extract_xattrs_resourcefork(archivers, request):
# assert 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(archivers, request):
archiver = request.getfixturevalue(archivers)
# create a file and set the append flag on it
create_regular_file(archiver.input_path, "appendflag", size=1)
src_path = os.path.abspath("input/appendflag")
if not hasattr(stat, "UF_APPEND"):
pytest.skip("UF_APPEND not available on this platform")
try:
platform.set_flags(src_path, stat.UF_APPEND)
except Exception:
pytest.skip("setting UF_APPEND not supported on this filesystem")
# 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
cmd(archiver, "repo-create", "-e" "none")
cmd(archiver, "create", "test", "input")
with changedir("output"):
cmd(archiver, "extract", "test")
out_path = os.path.abspath("input/appendflag")
st2 = os.lstat(out_path)
flags = platform.get_flags(out_path, st2)
assert (flags & stat.UF_APPEND) == stat.UF_APPEND
def test_overwrite(archivers, request):
archiver = request.getfixturevalue(archivers)
if archiver.EXE: