Skip to content

Commit 1ede79b

Browse files
Merge pull request #9143 from ThomasWaldmann/feature/8753-linux-acl-any-text
linux ACL: use acl_to_any_text to avoid libacl name lookups, fixes #8753
2 parents 753ac70 + 3419d93 commit 1ede79b

File tree

10 files changed

+234
-106
lines changed

10 files changed

+234
-106
lines changed

src/borg/platform/__init__.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
Public APIs are documented in platform.base.
55
"""
66

7+
from types import ModuleType
8+
79
from ..platformflags import is_win32, is_linux, is_freebsd, is_netbsd, is_darwin, is_cygwin
810

911
from .base import ENOATTR, API_VERSION
1012
from .base import SaveFile, sync_dir, fdatasync, safe_fadvise
1113
from .base import get_process_id, fqdn, hostname, hostid
1214

15+
platform_ug: ModuleType | None = None # make mypy happy
16+
1317
if is_linux: # pragma: linux only
1418
from .linux import API_VERSION as OS_API_VERSION
1519
from .linux import listxattr, getxattr, setxattr
@@ -19,7 +23,8 @@
1923
from .posix import process_alive, local_pid_alive
2024
from .posix import swidth
2125
from .posix import get_errno
22-
from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
26+
from .posix import getosusername
27+
from . import posix_ug as platform_ug
2328
elif is_freebsd: # pragma: freebsd only
2429
from .freebsd import API_VERSION as OS_API_VERSION
2530
from .freebsd import listxattr, getxattr, setxattr
@@ -30,7 +35,8 @@
3035
from .posix import process_alive, local_pid_alive
3136
from .posix import swidth
3237
from .posix import get_errno
33-
from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
38+
from .posix import getosusername
39+
from . import posix_ug as platform_ug
3440
elif is_netbsd: # pragma: netbsd only
3541
from .netbsd import API_VERSION as OS_API_VERSION
3642
from .netbsd import listxattr, getxattr, setxattr
@@ -40,7 +46,8 @@
4046
from .posix import process_alive, local_pid_alive
4147
from .posix import swidth
4248
from .posix import get_errno
43-
from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
49+
from .posix import getosusername
50+
from . import posix_ug as platform_ug
4451
elif is_darwin: # pragma: darwin only
4552
from .darwin import API_VERSION as OS_API_VERSION
4653
from .darwin import listxattr, getxattr, setxattr
@@ -52,7 +59,8 @@
5259
from .posix import process_alive, local_pid_alive
5360
from .posix import swidth
5461
from .posix import get_errno
55-
from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
62+
from .posix import getosusername
63+
from . import posix_ug as platform_ug
5664
elif not is_win32: # pragma: posix only
5765
# Generic code for all other POSIX OSes
5866
OS_API_VERSION = API_VERSION
@@ -63,7 +71,8 @@
6371
from .posix import process_alive, local_pid_alive
6472
from .posix import swidth
6573
from .posix import get_errno
66-
from .posix import uid2user, user2uid, gid2group, group2gid, getosusername
74+
from .posix import getosusername
75+
from . import posix_ug as platform_ug
6776
else: # pragma: win32 only
6877
# Win32-specific stuff
6978
OS_API_VERSION = API_VERSION
@@ -73,7 +82,8 @@
7382
from .base import SyncFile
7483
from .windows import process_alive, local_pid_alive
7584
from .base import swidth
76-
from .windows import uid2user, user2uid, gid2group, group2gid, getosusername
85+
from .windows import getosusername
86+
from . import windows_ug as platform_ug
7787

7888

7989
def get_birthtime_ns(st, path, fd=None):
@@ -86,3 +96,21 @@ def get_birthtime_ns(st, path, fd=None):
8696
return int(st.st_birthtime * 10**9)
8797
else:
8898
return None
99+
100+
101+
# have some wrapper functions, so we can monkeypatch the functions in platform_ug.
102+
# for normal usage from outside the platform package, always import these:
103+
def uid2user(uid, default=None):
104+
return platform_ug._uid2user(uid, default)
105+
106+
107+
def gid2group(gid, default=None):
108+
return platform_ug._gid2group(gid, default)
109+
110+
111+
def user2uid(user, default=None):
112+
return platform_ug._user2uid(user, default)
113+
114+
115+
def group2gid(group, default=None):
116+
return platform_ug._group2gid(group, default)

src/borg/platform/darwin.pyx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from libc.stdint cimport uint32_t
44
from libc cimport errno
55
from posix.time cimport timespec
66

7-
from .posix import user2uid, group2gid
7+
from . import posix_ug
88
from ..helpers import safe_decode, safe_encode
99
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
1010

@@ -108,10 +108,10 @@ def _remove_numeric_id_if_possible(acl):
108108
if entry:
109109
fields = entry.split(':')
110110
if fields[0] == 'user':
111-
if user2uid(fields[2]) is not None:
111+
if posix_ug._user2uid(fields[2]) is not None:
112112
fields[1] = fields[3] = ''
113113
elif fields[0] == 'group':
114-
if group2gid(fields[2]) is not None:
114+
if posix_ug._group2gid(fields[2]) is not None:
115115
fields[1] = fields[3] = ''
116116
entries.append(':'.join(fields))
117117
return safe_encode('\n'.join(entries))

src/borg/platform/freebsd.pyx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
147147
If `numeric_ids` is True the user/group field is not preserved only uid/gid
148148
"""
149149
cdef int flags = ACL_TEXT_APPEND_ID
150+
# Note: likely this could be faster if we always used ACL_TEXT_NUMERIC_IDS,
151+
# and then used uid2user() and gid2group() to translate the numeric ids to names
152+
# inside borg (borg has a LRUcache for these lookups).
153+
# See how the Linux implementation does it.
150154
flags |= ACL_TEXT_NUMERIC_IDS if numeric_ids else 0
151155
if isinstance(path, str):
152156
path = os.fsencode(path)

src/borg/platform/linux.pyx

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import re
33
import stat
44

55
from .posix import posix_acl_use_stored_uid_gid
6-
from .posix import user2uid, group2gid
6+
from . import posix_ug
77
from ..helpers import workarounds
88
from ..helpers import safe_decode, safe_encode
99
from .base import SyncFile as BaseSyncFile
@@ -47,11 +47,12 @@ cdef extern from "sys/acl.h":
4747
int acl_set_file(const char *path, int type, acl_t acl)
4848
int acl_set_fd(int fd, acl_t acl)
4949
acl_t acl_from_text(const char *buf)
50-
char *acl_to_text(acl_t acl, ssize_t *len)
5150

5251
cdef extern from "acl/libacl.h":
5352
int acl_extended_file_nofollow(const char *path)
5453
int acl_extended_fd(int fd)
54+
char *acl_to_any_text(acl_t acl, const char *prefix, char separator, int options)
55+
int TEXT_NUMERIC_IDS
5556

5657
cdef extern from "linux/fs.h":
5758
# ioctls
@@ -203,46 +204,66 @@ def acl_use_local_uid_gid(acl):
203204
if entry:
204205
fields = entry.split(':')
205206
if fields[0] == 'user' and fields[1]:
206-
fields[1] = str(user2uid(fields[1], fields[3]))
207+
fields[1] = str(posix_ug._user2uid(fields[1], fields[3]))
207208
elif fields[0] == 'group' and fields[1]:
208-
fields[1] = str(group2gid(fields[1], fields[3]))
209+
fields[1] = str(posix_ug._group2gid(fields[1], fields[3]))
209210
entries.append(':'.join(fields[:3]))
210211
return safe_encode('\n'.join(entries))
211212

212213

213-
cdef acl_append_numeric_ids(acl):
214-
"""Extend the "POSIX 1003.1e draft standard 17" format with an additional uid/gid field
214+
def _acl_from_numeric_to_named_with_id(acl):
215+
"""Convert numeric-id ACL entries to name entries and append numeric id as 4th field.
216+
217+
Input format (Linux libacl): lines like 'user:1000:rwx' or 'group:100:r-x' or 'user::rwx'.
218+
Output format: for entries with a name/id field, become 'user:uname:rwx:uid' or 'group:gname:r-x:gid'.
215219
"""
216220
assert isinstance(acl, bytes)
217221
entries = []
218222
for entry in _comment_re.sub('', safe_decode(acl)).split('\n'):
219-
if entry:
220-
type, name, permission = entry.split(':')
221-
if name and type == 'user':
222-
entries.append(':'.join([type, name, permission, str(user2uid(name, name))]))
223-
elif name and type == 'group':
224-
entries.append(':'.join([type, name, permission, str(group2gid(name, name))]))
223+
if not entry:
224+
continue
225+
fields = entry.split(':')
226+
# Expected 3 fields: type, ugid_or_empty, perms
227+
if len(fields) >= 3:
228+
typ, ugid_str, perm = fields[0], fields[1], fields[2]
229+
if ugid_str and typ == 'user':
230+
try:
231+
uid = int(ugid_str)
232+
except ValueError:
233+
uid = None
234+
uname = posix_ug._uid2user(uid, ugid_str) if uid is not None else ugid_str
235+
entries.append(':'.join([typ, uname, perm, str(uid if uid is not None else ugid_str)]))
236+
elif ugid_str and typ == 'group':
237+
try:
238+
gid = int(ugid_str)
239+
except ValueError:
240+
gid = None
241+
gname = posix_ug._gid2group(gid, ugid_str) if gid is not None else ugid_str
242+
entries.append(':'.join([typ, gname, perm, str(gid if gid is not None else ugid_str)]))
225243
else:
226-
entries.append(entry)
244+
# owner, group_obj, mask, other (empty ugid_str field) stay as-is
245+
entries.append(':'.join([typ, '', perm]))
246+
else:
247+
entries.append(entry)
227248
return safe_encode('\n'.join(entries))
228249

229250

230-
cdef acl_numeric_ids(acl):
231-
"""Replace the "POSIX 1003.1e draft standard 17" user/group field with uid/gid
232-
"""
251+
def _acl_from_numeric_to_numeric_with_id(acl):
252+
"""Keep numeric ids in name field and append the same id as 4th field where applicable."""
233253
assert isinstance(acl, bytes)
234254
entries = []
235255
for entry in _comment_re.sub('', safe_decode(acl)).split('\n'):
236-
if entry:
237-
type, name, permission = entry.split(':')
238-
if name and type == 'user':
239-
uid = str(user2uid(name, name))
240-
entries.append(':'.join([type, uid, permission, uid]))
241-
elif name and type == 'group':
242-
gid = str(group2gid(name, name))
243-
entries.append(':'.join([type, gid, permission, gid]))
256+
if not entry:
257+
continue
258+
fields = entry.split(':')
259+
if len(fields) >= 3:
260+
typ, ugid, perm = fields[0], fields[1], fields[2]
261+
if ugid and (typ == 'user' or typ == 'group'):
262+
entries.append(':'.join([typ, ugid, perm, ugid]))
244263
else:
245-
entries.append(entry)
264+
entries.append(':'.join([typ, '', perm]))
265+
else:
266+
entries.append(entry)
246267
return safe_encode('\n'.join(entries))
247268

248269

@@ -266,17 +287,17 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
266287
# note: this should also be the case for symlink fs objects, as they can not have ACLs.
267288
return
268289
if numeric_ids:
269-
converter = acl_numeric_ids
290+
converter = _acl_from_numeric_to_numeric_with_id
270291
else:
271-
converter = acl_append_numeric_ids
292+
converter = _acl_from_numeric_to_named_with_id
272293
try:
273294
if fd is not None:
274295
access_acl = acl_get_fd(fd)
275296
else:
276297
access_acl = acl_get_file(path, ACL_TYPE_ACCESS)
277298
if access_acl == NULL:
278299
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
279-
access_text = acl_to_text(access_acl, NULL)
300+
access_text = acl_to_any_text(access_acl, NULL, '\n', TEXT_NUMERIC_IDS)
280301
if access_text == NULL:
281302
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
282303
item['acl_access'] = converter(access_text)
@@ -289,7 +310,7 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
289310
default_acl = acl_get_file(path, ACL_TYPE_DEFAULT)
290311
if default_acl == NULL:
291312
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
292-
default_text = acl_to_text(default_acl, NULL)
313+
default_text = acl_to_any_text(default_acl, NULL, '\n', TEXT_NUMERIC_IDS)
293314
if default_text == NULL:
294315
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
295316
item['acl_default'] = converter(default_text)

src/borg/platform/posix.pyx

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import errno
22
import os
3-
import grp
4-
import pwd
5-
from functools import lru_cache
3+
4+
from . import posix_ug
65

76
from libc.errno cimport errno as c_errno
87

@@ -77,42 +76,6 @@ def local_pid_alive(pid):
7776
return True
7877

7978

80-
@lru_cache(maxsize=None)
81-
def uid2user(uid, default=None):
82-
try:
83-
return pwd.getpwuid(uid).pw_name
84-
except KeyError:
85-
return default
86-
87-
88-
@lru_cache(maxsize=None)
89-
def user2uid(user, default=None):
90-
if not user:
91-
return default
92-
try:
93-
return pwd.getpwnam(user).pw_uid
94-
except KeyError:
95-
return default
96-
97-
98-
@lru_cache(maxsize=None)
99-
def gid2group(gid, default=None):
100-
try:
101-
return grp.getgrgid(gid).gr_name
102-
except KeyError:
103-
return default
104-
105-
106-
@lru_cache(maxsize=None)
107-
def group2gid(group, default=None):
108-
if not group:
109-
return default
110-
try:
111-
return grp.getgrnam(group).gr_gid
112-
except KeyError:
113-
return default
114-
115-
11679
def posix_acl_use_stored_uid_gid(acl):
11780
"""Replace the user/group field with the stored uid/gid."""
11881
assert isinstance(acl, bytes)
@@ -131,4 +94,4 @@ def posix_acl_use_stored_uid_gid(acl):
13194
def getosusername():
13295
"""Return the OS username."""
13396
uid = os.getuid()
134-
return uid2user(uid, uid)
97+
return posix_ug._uid2user(uid, uid)

src/borg/platform/posix_ug.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import grp
2+
import pwd
3+
from functools import lru_cache
4+
5+
6+
@lru_cache(maxsize=None)
7+
def _uid2user(uid, default=None):
8+
try:
9+
return pwd.getpwuid(uid).pw_name
10+
except KeyError:
11+
return default
12+
13+
14+
@lru_cache(maxsize=None)
15+
def _user2uid(user, default=None):
16+
if not user:
17+
return default
18+
try:
19+
return pwd.getpwnam(user).pw_uid
20+
except KeyError:
21+
return default
22+
23+
24+
@lru_cache(maxsize=None)
25+
def _gid2group(gid, default=None):
26+
try:
27+
return grp.getgrgid(gid).gr_name
28+
except KeyError:
29+
return default
30+
31+
32+
@lru_cache(maxsize=None)
33+
def _group2gid(group, default=None):
34+
if not group:
35+
return default
36+
try:
37+
return grp.getgrnam(group).gr_gid
38+
except KeyError:
39+
return default

0 commit comments

Comments
 (0)