Skip to content

Commit 4634b0a

Browse files
committed
android: probe openat2 usability behind a SIGSYS handler
Android's seccomp sandbox traps openat2() with SECCOMP_RET_TRAP, which raises SIGSYS and kills the process instead of returning ENOSYS, so the secure resolver cannot simply try openat2() and inspect errno. Add openat2_usable() in a new android.c: it probes openat2() once behind a temporary SIGSYS handler and caches the result. Gate every SYS_openat2 call on openat2_usable(): in the resolver via an openat2_beneath() wrapper, and in t_chmod_secure's kernel probe directly, so a blocked openat2 reports ENOSYS and the caller falls back to the portable O_NOFOLLOW resolver. Only openat2 is gated -- a plain openat() (e.g. opening an operator-trusted absolute basedir) is left free. The probe body compiles only on Android -- __ANDROID__ is a Bionic target macro, so it is set for NDK cross-builds and native Termux alike and unset everywhere else, where openat2_usable() collapses to a constant 1. Link android.o into the secure-resolver test helpers too so their self-tests survive on Termux. Adapted from PR #909.
1 parent 83a24c2 commit 4634b0a

4 files changed

Lines changed: 107 additions & 10 deletions

File tree

Makefile.in

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \
4444
zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
4545
zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
4646
OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
47-
util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o
47+
util1.o util2.o main.o checksum.o match.o syscall.o android.o log.o backup.o delete.o
4848
OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
4949
usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
5050
OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@
@@ -53,7 +53,7 @@ popt_OBJS= popt/popt.o popt/poptconfig.o \
5353
popt/popthelp.o popt/poptparse.o popt/poptint.o
5454
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
5555

56-
TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
56+
TLS_OBJ = tls.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
5757

5858
# Programs we must have to run the test cases
5959
CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
@@ -172,19 +172,19 @@ getgroups$(EXEEXT): getgroups.o
172172
getfsdev$(EXEEXT): getfsdev.o
173173
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS)
174174

175-
TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o
175+
TRIMSLASH_OBJ = trimslash.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o
176176
trimslash$(EXEEXT): $(TRIMSLASH_OBJ)
177177
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS)
178178

179-
T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
179+
T_UNSAFE_OBJ = t_unsafe.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
180180
t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)
181181
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)
182182

183-
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
183+
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
184184
t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)
185185
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)
186186

187-
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
187+
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
188188
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
189189
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
190190

android.c

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Android-specific helpers.
3+
*
4+
* openat2() usability probe
5+
* -------------------------
6+
* openat2(2) is invoked directly via syscall() because the C library lacked a
7+
* wrapper for it for years. Under a seccomp filter that uses
8+
* SECCOMP_RET_TRAP -- as the Android application sandbox does -- a disallowed
9+
* syscall raises SIGSYS and *kills the process* rather than failing with
10+
* ENOSYS, so inspecting errno after the call is too late. We therefore probe
11+
* openat2() once, behind a temporary SIGSYS handler, so a trapped syscall is
12+
* caught and secure_relative_open_linux() can fall back to the portable
13+
* per-component O_NOFOLLOW resolver instead of the whole process dying.
14+
*
15+
* This is only needed on Android, so the probe body is compiled only there.
16+
* __ANDROID__ is defined by Bionic's headers and reflects the *target*, not
17+
* the build host: it is set both for NDK cross-compiles (from a Linux/macOS
18+
* host) and for native Termux builds, and is unset on every other platform.
19+
* That makes it a reliable compile-time switch for cross builds -- there is
20+
* nothing to detect in configure. Everywhere else openat2() is never
21+
* seccomp-trapped to SIGSYS (a missing syscall simply returns ENOSYS), so
22+
* openat2_usable() collapses to a constant 1 with no run-time cost.
23+
*/
24+
25+
#include "rsync.h"
26+
27+
#if defined(__ANDROID__) && defined(HAVE_OPENAT2)
28+
29+
#include <setjmp.h>
30+
#include <sys/syscall.h>
31+
#include <linux/openat2.h>
32+
33+
static sigjmp_buf openat2_probe_env;
34+
35+
static void openat2_probe_handler(int signo)
36+
{
37+
(void)signo;
38+
siglongjmp(openat2_probe_env, 1);
39+
}
40+
41+
#endif
42+
43+
int openat2_usable(void)
44+
{
45+
#if defined(__ANDROID__) && defined(HAVE_OPENAT2)
46+
static int cached = -1;
47+
struct sigaction sa, old_sa;
48+
49+
if (cached >= 0)
50+
return cached;
51+
52+
memset(&sa, 0, sizeof sa);
53+
sa.sa_handler = openat2_probe_handler;
54+
sigemptyset(&sa.sa_mask);
55+
if (sigaction(SIGSYS, &sa, &old_sa) != 0)
56+
return cached = 0;
57+
58+
if (sigsetjmp(openat2_probe_env, 1) != 0) {
59+
/* SIGSYS delivered: openat2 is blocked by a seccomp filter. */
60+
cached = 0;
61+
} else {
62+
struct open_how how;
63+
int fd;
64+
memset(&how, 0, sizeof how);
65+
how.flags = O_RDONLY | O_DIRECTORY;
66+
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
67+
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
68+
if (fd >= 0)
69+
close(fd);
70+
/* Usable only if the probe actually succeeded. Any failure --
71+
* ENOSYS (kernel < 5.6), a seccomp SECCOMP_RET_ERRNO denial
72+
* (EPERM/EACCES), or EINVAL (RESOLVE_BENEATH unsupported) --
73+
* means we must fall back to the portable O_NOFOLLOW walk. */
74+
cached = fd >= 0;
75+
}
76+
77+
sigaction(SIGSYS, &old_sa, NULL);
78+
return cached;
79+
#else
80+
return 1;
81+
#endif
82+
}

syscall.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,19 @@ static int path_has_dotdot_component(const char *path)
17061706
}
17071707

17081708
#if defined(__linux__) && defined(HAVE_OPENAT2)
1709+
/* openat2(RESOLVE_BENEATH) via the raw syscall, gated on openat2_usable() so a
1710+
* seccomp filter that traps openat2 with SIGSYS (e.g. the Android sandbox)
1711+
* makes us report ENOSYS and fall back rather than killing the process. Only
1712+
* the openat2 call is gated here; a plain openat() is always safe to attempt. */
1713+
static int openat2_beneath(int dirfd, const char *path, const struct open_how *how)
1714+
{
1715+
if (!openat2_usable()) {
1716+
errno = ENOSYS;
1717+
return -1;
1718+
}
1719+
return syscall(SYS_openat2, dirfd, path, how, sizeof *how);
1720+
}
1721+
17091722
static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
17101723
{
17111724
struct open_how how;
@@ -1734,12 +1747,12 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath,
17341747
memset(&bhow, 0, sizeof bhow);
17351748
bhow.flags = O_RDONLY | O_DIRECTORY;
17361749
bhow.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
1737-
dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow);
1750+
dirfd = openat2_beneath(AT_FDCWD, basedir, &bhow);
17381751
if (dirfd == -1)
17391752
return -1;
17401753
}
17411754

1742-
retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how);
1755+
retfd = openat2_beneath(dirfd, relpath, &how);
17431756

17441757
if (dirfd != AT_FDCWD)
17451758
close(dirfd);

t_chmod_secure.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ static int errs = 0;
4444
* other than the kernel rejecting the requested confinement flag. */
4545
static int kernel_resolve_beneath_supported(void)
4646
{
47+
#if (defined(__linux__) && defined(HAVE_OPENAT2)) || defined(O_RESOLVE_BENEATH)
4748
int fd;
49+
#endif
4850
#if defined(__linux__) && defined(HAVE_OPENAT2)
49-
{
51+
if (openat2_usable()) {
5052
struct open_how how;
5153
memset(&how, 0, sizeof how);
5254
how.flags = O_RDONLY | O_DIRECTORY;
@@ -56,7 +58,7 @@ static int kernel_resolve_beneath_supported(void)
5658
close(fd);
5759
return 1;
5860
}
59-
/* ENOSYS = kernel < 5.6. Fall through to the O_RESOLVE_BENEATH
61+
/* ENOSYS = kernel < 5.6 or openat2 seccomp-blocked. Fall through to the O_RESOLVE_BENEATH
6062
* probe in case we're a Linux build running on a kernel that
6163
* gained O_RESOLVE_BENEATH via some out-of-tree backport. */
6264
}

0 commit comments

Comments
 (0)