Skip to content

build: auto-detect the presence of the openat2() syscall#925

Merged
tridge merged 4 commits into
RsyncProject:masterfrom
mmayer:openat2_autodetect
Jun 4, 2026
Merged

build: auto-detect the presence of the openat2() syscall#925
tridge merged 4 commits into
RsyncProject:masterfrom
mmayer:openat2_autodetect

Conversation

@mmayer
Copy link
Copy Markdown

@mmayer mmayer commented May 29, 2026

Let configure detect if the openat2() syscall is supported by the kernel headers we are building against. Do not attempt to use openat2() if support is not present.

Users can still disable using the openat2() syscall manually if so desired.

@mmayer
Copy link
Copy Markdown
Author

mmayer commented May 29, 2026

This is for issue 924.

@satmandu
Copy link
Copy Markdown

Note that this doesn't fix make check:

gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c tls.c -o tls.o
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c t_stub.c -o t_stub.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o tls tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o  -lacl -lpopt -llz4 -lzstd -lxxhash -lcrypto 
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c getgroups.c -o getgroups.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o getgroups getgroups.o -lacl -lpopt -llz4 -lzstd -lxxhash -lcrypto 
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c getfsdev.c -o getfsdev.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o getfsdev getfsdev.o -lacl -lpopt -llz4 -lzstd -lxxhash -lcrypto 
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c testrun.c -o testrun.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o testrun testrun.o
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c trimslash.c -o trimslash.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o trimslash trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o -lacl -lpopt -llz4 -lzstd -lxxhash -lcrypto 
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c t_unsafe.c -o t_unsafe.o
gcc -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W -flto=auto -o t_unsafe t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o -lacl -lpopt -llz4 -lzstd -lxxhash -lcrypto 
gcc -I. -I. -I./zlib -O3 -pipe -ffat-lto-objects -fPIC -fuse-ld=mold  -flto=auto -flto=auto -DHAVE_CONFIG_H -Wall -W  -c t_chmod_secure.c -o t_chmod_secure.o
t_chmod_secure.c:22:10: fatal error: linux/openat2.h: No such file or directory
   22 | #include <linux/openat2.h>
      |          ^~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:72: t_chmod_secure.o] Error 1

@mmayer
Copy link
Copy Markdown
Author

mmayer commented May 29, 2026

It works for me. I just tried again after "make distclean".

$ ls -l rsync -rwxrwxr-x 1 mmayer mmayer 2276384 May 29 10:04 rsync

This is with Ubuntu 18.04.

Did you regenerate configure.sh after applying the patch? What does config.log have to say about detecting the presence of SYS_openat2?

You should see something like this.

[...]
checking whether to enable rolling-checksum SIMD optimizations... yes (x86_64)
checking if assembler accepts noexecstack... yes
checking for openat2... no
checking for broken largefile support... no
checking for special C compiler options needed for large files... no
[...]

And in config.log:

[...]
configure.sh:6286: checking for openat2
configure.sh:6305: gcc -c -g -O2 -DHAVE_CONFIG_H -Wall -W  conftest.c >&5
conftest.c: In function 'main':
conftest.c:72:9: error: 'SYS_openat2' undeclared (first use in this function); did you mean 'SYS_openat'?
 int i = SYS_openat2
         ^~~~~~~~~~~
         SYS_openat
conftest.c:72:9: note: each undeclared identifier is reported only once for each function it appears in
conftest.c:72:5: warning: unused variable 'i' [-Wunused-variable]
 int i = SYS_openat2
     ^
[...]

@mmayer
Copy link
Copy Markdown
Author

mmayer commented May 29, 2026

But I see what you mean re: "make check". Let me take a look. ... That's because t_chmod_secure.c only has

#if defined(__linux__)

instead of

#if defined(__linux__) && defined(HAVE_OPENAT2)

Let me add that change to the pull request.

@mmayer
Copy link
Copy Markdown
Author

mmayer commented May 29, 2026

BTW, make check was already failing in this situation before my patch. So that is nothing new.

@mmayer
Copy link
Copy Markdown
Author

mmayer commented May 29, 2026

My pull request is now updated to fix t_chmod_secure.c (i.e. make check) as well.

@tridge
Copy link
Copy Markdown
Member

tridge commented May 30, 2026

Unfortunately we also need runtime detection as android blocks the syscall for openat2 with seccomp, which signals the process on attempted openat2() calls, so we need runtime fallback. AOSP refused to fix.

@satmandu
Copy link
Copy Markdown

Having runtime detection makes sense. In part also because if built against newer kernel headers but run on a system with an older kernel, we'll want to make sure the functionality is actually available before using it.

@cyphar
Copy link
Copy Markdown

cyphar commented May 31, 2026

openat2 is one of the newer syscalls that have uniform syscall numbers for basically all architectures, so you could just define __NR_openat2 yourself as a fallback with very little effort.

Compile-time detection of syscalls doesn't really make sense in the modern world of containers and seccomp, it makes far more sense to check this at runtime anyway. To be fair, modern glibc has wrappers for openat2 finally, so it makes more sense now than it did a few years ago, but I would still recommend just using your own syscall wrappers -- the Linux syscall ABI is guaranteed to never change, and if you want to make use of future struct open_how extensions it is probably easier to do it directly.

pterror added a commit to pterror/rsync that referenced this pull request May 31, 2026
…dings, sans openat2)

Squash-consolidates four campaign branches onto genuine master 3748c32,
delivering the full hardening, fuzzing, and invariant-test net plus the
discovered real-bug fixes. The openat2/R3 portability work is deliberately
EXCLUDED here (superseded by upstream PR RsyncProject#925).

Real-bug fixes (static-findings + fuzz-found):
- receiver.c (R1): fix discard-path NULL deref. A block-match token arriving
  while fname==NULL / fd==-1 reached full_fname(NULL) (remote DoS); the
  mapbuf==NULL protocol error is now restricted to real-output transfers
  (fd != -1, fname non-NULL) and a block-match on the discard path is absorbed
  benignly as normal protocol. Locked in by fuzz/fuzz_recv_discard.
- options.c (R2): zero-initialize *port_ptr so the later "!*port_ptr" reads can
  no longer observe an uninitialized caller value on the non-URL parse path.

Residual hardening (harden-residuals):
- hlink.c: lower-bound (ndx < flist->ndx_start) guards in match_gnums,
  check_prior, and finish_hard_link before indexing flist->files[].
- syscall.c: secure_mkstemp refuses templates with a ".." path component.
- options.c: a daemon auto-refuses --copy-as.

WS1/WS2/WS3 hardening net:
- WS1 invariant + transport-equivalence tests (testsuite/equiv_fns.py plus
  content-fidelity, metadata-fidelity, link-dest-variants, link-dest-equiv,
  idempotence, delete-backup-invariants, transport-equiv-meta), wired via the
  new "check-equiv" Makefile.in target. Includes the D2 root/fakeroot skip and
  comment-reword net-correctness fixes.
- WS2 libFuzzer harnesses under fuzz/: fuzz_io, fuzz_token, fuzz_deflated_token,
  fuzz_flist, fuzz_xattrs (live, linking the real recompiled flist.c/xattrs.c via
  RSYNC_FUZZ_* hooks), and fuzz_recv_discard; with stubs.c/globals.c, seed
  corpora, Makefile, and run-regression.sh (all six registered, ASan/UBSan).
- WS3 CI: .github/workflows/{sanitizers-asan-ubsan,hardened-build,
  analysis-gcc-fanalyzer,analysis-scan-build,analysis-cppcheck,analysis-codeql}
  plus .github/ubsan-suppressions.txt and the --enable-hardened configure knob.
- xattrs.c: zero-count UBSan source fixes (guard zero-length loops/memcpy).

Excludes the openat2/R3 lane entirely: no configure-header-guard_test.py, no
.github/workflows/portability.yml, no maint/check-openat2-portability.sh, and
none of our openat2 AC_CHECK_HEADER fix. Upstream's openat2 scaffolding present
in 3748c32 is left untouched. openat2/R3 superseded by upstream PR RsyncProject#925.

Adversarial-review fixes folded in (CODE-REVIEW.md MINOR RsyncProject#1/RsyncProject#3, NIT RsyncProject#4):
- test(equiv): equiv_fns.run_matrix asserts a per-scenario expected exit code
  instead of blanket-accepting 23, and idempotence_test _structural_fatal no
  longer drops only_in_* membership diffs (a dropped/spurious file on a
  round-trip leg is now fatal). Affected tests still pass; no divergence found.
- fuzz: narrow LSan suppressions (fuzz/lsan-suppressions.txt naming only
  recv_file_entry / receive_xattr) wired into run-regression.sh for the
  flist/xattrs targets, so the intentional process-lifetime static caches no
  longer mask a genuine new leak; regression reports all targets clean.
- fuzz: flist_for_ndx stub now aborts loudly instead of silently returning
  NULL, since no harness links the real implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tridge
Copy link
Copy Markdown
Member

tridge commented Jun 2, 2026

the Linux syscall ABI is guaranteed to never change

well, sort of ... when vendors like android use seccomp to block system calls (like they do for openat2) then from userspace point of view the ABI has changed .... sort of!

@cyphar
Copy link
Copy Markdown

cyphar commented Jun 2, 2026

I had a chat with @tridge and normally sandboxes use SECCOMP_RET_ERRNO(ENOSYS) which means that the ABI works but apparently Android does SECCOMP_RET_TRAP?! (This is completely crazy, by the way -- this would even break glibc which depends on ENOSYS for fallback detection!!).

That's really painful -- and you can't detect it at build-time either! (Maybe Android should have some #define SYSCALL_<N>_IS_A_LANDMINE 1 for stuff like this. 🤦)

@tridge tridge force-pushed the openat2_autodetect branch from fcd22c4 to b1d7780 Compare June 3, 2026 23:40
@tridge
Copy link
Copy Markdown
Member

tridge commented Jun 3, 2026

@mmayer @cyphar I've added in android.c which does the horrible signal catch to avoid the seccomp nonsense in android

Markus Mayer and others added 4 commits June 4, 2026 13:17
Let configure detect if the openat2() syscall is supported by the kernel
headers we are building against. Do not attempt to use openat2() if
support is not present.

Users can still disable using the openat2() syscall manually if so
desired.

Signed-off-by: Markus Mayer <mmayer@broadcom.com>
To prevent using openat2() in situations where it is not supported, use
    #if defined(__linux__) && defined(HAVE_OPENAT2)
in t_chmod_secure.c, just like it was already being done in syscall.c.

Signed-off-by: Markus Mayer <mmayer@broadcom.com>
The openat2 secure resolver in syscall.c needs struct open_how and
RESOLVE_BENEATH from <linux/openat2.h>, not only the SYS_openat2 syscall
number.  Some setups expose the syscall number via glibc without the
kernel header present, so probing SYS_openat2 alone still left the build
broken (RsyncProject#905).  Exercise the header and struct in the configure check so
HAVE_OPENAT2 is defined only when both are actually usable.
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 RsyncProject#909.
@tridge tridge force-pushed the openat2_autodetect branch from b1d7780 to 713c8d2 Compare June 4, 2026 03:17
@tridge tridge merged commit 4634b0a into RsyncProject:master Jun 4, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants