Files
DroidDeck/tools/tests/test_openat2_preload.py
T
MaxsTechReview c5577478ca chore: remove source comments and unused code, translate every missing string (#1965)
* chore(i18n): translate every missing string in all locales

Adds the 87 strings the bottom-screen Dashboard and in-game stats pages
introduced to all 32 locales, plus the 51 older settings strings Japanese
and the 50 Chinese locales were still missing. Every locale now has all
3456 translatable strings with the same placeholders as English.

Also translates 36 entries that were still English where the language
does not use the English word, and makes es-419 touch_app_id use the same
%1$s placeholder as English.

* chore(patches): drop comments from the lines downstream patches add

Removes comment lines and trailing comments from lines our patches add,
with hunk headers recounted. Lines that a later patch uses as context
and the upstream backport 0123 are left as they are.

The gamescope, FEX, proot and xodus series still apply to their pinned
sources, and the patched trees differ only in those comments.
test_hud_frames now finds the HUD writer by its first declaration.

* docs: credit Greenlight, Better xCloud, viture-v2, GameNative and DiRTTelemetryErrorFix

These were credited only in source headers and NOTICE files.

* chore: remove source comments and unused functions

Removes explanatory comments from first-party Kotlin, Java, C, C++,
Rust, TypeScript, Python, shell, Gradle, CMake and workflow files.
License and attribution headers stay, and vendored third-party trees,
generated files and license files are untouched. A shellcheck directive
is kept, and udevmon's fallthrough comments became fallthrough attributes.

Syntax trees and token streams match the previous sources apart from the
comments and Python docstrings. Module docstrings that argparse prints
are kept, and device-smoke.sh prints the same --help text from a heredoc.

Also removes 37 functions and 4 fields that nothing referenced.

* chore: remove comments from the Syncthing integration and the latest main sources

* docs: credit Syncthing and the syncthing-android fork

* chore(i18n): translate the Syncthing strings in all locales

The 107 strings the Syncthing integration added are now in all 32 locales.
sync_web_path_locked names the Syncthing tab by its label.

* docs: credit @xXJSONDeruloXx for the HUD metric readers

The CPU, GPU load, thermal and battery readers come from their
PerformanceHudView pull requests #777, #877, #991, #1406 and #1573.
The FusionHUD credit line is unchanged.

* docs: credit the HUD metric readers to @xXJSONDeruloXx's work
2026-11-01 16:27:27 -05:00

434 lines
19 KiB
Python

import errno
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
FASTPATH = Path(__file__).resolve().parents[1] / "proot/fastpath/fastpath.c"
PROGRAM = r"""
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#ifndef SYS_openat2
#define SYS_openat2 437
#endif
struct how { uint64_t flags, mode, resolve; };
static char cases[256][512];
static int count;
static void run(long (*call)(long, ...), const char *tree) {
int root = open(tree, O_PATH | O_DIRECTORY | O_CLOEXEC), outer = open("/", O_PATH | O_DIRECTORY | O_CLOEXEC);
for (int i = 0; i < count; i++) {
char *flags = strchr(cases[i], '\t') + 1, *path = strchr(flags, '\t') + 1;
struct how h = {strtoull(flags, NULL, 0), 0, 0x10};
if (h.flags & O_CREAT) h.mode = 0644;
long fd = call(SYS_openat2, (long)(cases[i][0] == 'o' ? outer : root), (long)path, (long)&h, (long)sizeof(h));
struct stat st;
if (fd < 0) {
printf("ERR %d\n", errno);
} else {
fstat((int)fd, &st);
printf("OK %llu %llu\n", (unsigned long long)st.st_dev, (unsigned long long)st.st_ino);
close((int)fd);
}
}
}
int main(int argc, char **argv) {
if (argc < 3) return 2;
while (count < 256 && fgets(cases[count], sizeof(cases[count]), stdin)) {
cases[count][strcspn(cases[count], "\n")] = 0;
count++;
}
long (*kernel)(long, ...) = (long (*)(long, ...))dlsym(dlopen("libc.so.6", RTLD_LAZY | RTLD_NOLOAD), "syscall");
struct how probe = {O_RDONLY, 0, 0x10};
long fd = kernel(SYS_openat2, (long)AT_FDCWD, (long)".", (long)&probe, (long)sizeof(probe));
if (fd >= 0) {
close((int)fd);
printf("kernel\n");
run(kernel, argv[1]);
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_openat2, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ENOSYS),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {sizeof(filter) / sizeof(filter[0]), filter};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("seccomp");
return 3;
}
}
printf("preload\n");
run(syscall, argv[2]);
return 0;
}
"""
COUNTER = r"""
#define _GNU_SOURCE
#include <dlfcn.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
static int opens, stats, links, statxs;
int openat(int dirfd, const char *path, int flags, ...) {
va_list args;
va_start(args, flags);
mode_t mode = va_arg(args, int);
va_end(args);
opens++;
return ((int (*)(int, const char *, int, ...))dlsym(RTLD_NEXT, "openat"))(dirfd, path, flags, mode);
}
int fstatat(int dirfd, const char *path, struct stat *st, int flags) {
stats++;
return ((int (*)(int, const char *, struct stat *, int))dlsym(RTLD_NEXT, "fstatat"))(dirfd, path, st, flags);
}
int fstatat64(int dirfd, const char *path, struct stat64 *st, int flags) {
stats++;
return ((int (*)(int, const char *, struct stat64 *, int))dlsym(RTLD_NEXT, "fstatat64"))(dirfd, path, st, flags);
}
ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) {
links++;
return ((ssize_t (*)(int, const char *, char *, size_t))dlsym(RTLD_NEXT, "readlinkat"))(dirfd, path, buf, size);
}
int statx(int dirfd, const char *path, int flags, unsigned int mask, struct statx *out) {
statxs++;
return ((int (*)(int, const char *, int, unsigned int, struct statx *))dlsym(RTLD_NEXT, "statx"))(dirfd, path, flags, mask, out);
}
__attribute__((destructor)) static void report(void) {
fprintf(stderr, "calls %d %d %d %d\n", opens, stats, links, statxs);
}
"""
RAW_OPEN = r"""
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
int main(int argc, char **argv) {
char text[16] = "";
long fd = syscall(SYS_openat, AT_FDCWD, argv[1], O_RDONLY | O_CLOEXEC, 0);
if (fd < 0 || read((int)fd, text, sizeof(text) - 1) < 0) return 1;
printf("%s\n", text);
return syscall(SYS_openat, AT_FDCWD, argv[2], O_RDONLY | O_CLOEXEC, 0) == -1 ? 0 : 1;
}
"""
WRAPPER = r"""
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#ifndef SYS_openat2
#define SYS_openat2 437
#endif
struct how { uint64_t flags, mode, resolve; };
int main(int argc, char **argv) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_openat2, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ENOSYS),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {sizeof(filter) / sizeof(filter[0]), filter};
if (argc < 3 || prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) return 3;
int (*call)(int, const char *, struct how *, size_t) = (int (*)(int, const char *, struct how *, size_t))dlsym(RTLD_DEFAULT, "openat2");
if (!call) return 4;
int dir = open(argv[1], O_PATH | O_DIRECTORY | O_CLOEXEC);
struct how h = {O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644, 0x08};
int fd = call(dir, argv[2], &h, sizeof(h));
if (fd < 0) {
printf("ERR %d\n", errno);
return 1;
}
printf("OK\n");
return close(fd);
}
"""
RAW_LOOKUP = r"""
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
#ifdef SYS_newfstatat
#define RAW_FSTATAT SYS_newfstatat
#else
#define RAW_FSTATAT SYS_fstatat64
#endif
int main(int argc, char **argv) {
struct stat st;
struct statx sx;
char link[64] = "";
if (syscall(RAW_FSTATAT, AT_FDCWD, argv[1], &st, 0) != 0) return 1;
if (syscall(RAW_FSTATAT, AT_FDCWD, argv[2], &st, 0) != -1 || errno != ENOENT) return 2;
long n = syscall(SYS_readlinkat, AT_FDCWD, argv[3], link, sizeof(link) - 1);
if (n <= 0) return 3;
sx.stx_size = 0;
long found = syscall(SYS_statx, AT_FDCWD, argv[1], 0, STATX_SIZE, &sx);
int have_statx = !(found == -1 && errno == ENOSYS);
if (have_statx && found != 0) return 4;
long missing = syscall(SYS_statx, AT_FDCWD, argv[2], 0, STATX_SIZE, &sx);
if (have_statx && (missing != -1 || errno != ENOENT)) return 5;
printf("%lld %.*s %llu\n", (long long)st.st_size, (int)n, link, (unsigned long long)sx.stx_size);
return 0;
}
"""
O_PATH = getattr(os, "O_PATH", 0o10000000)
CASES = [
("r", os.O_RDONLY, "etc/alternatives/hn", "etc/hostname"),
("r", os.O_RDONLY, "/lib64/x86_64-linux-gnu/libfoo.so.1", "usr/lib/x86_64-linux-gnu/libfoo.so.1.2"),
("r", os.O_RDONLY, "lib/x86_64-linux-gnu/libfoo.so.1", "usr/lib/x86_64-linux-gnu/libfoo.so.1.2"),
("r", os.O_RDONLY, "etc/escape", "etc/hostname"),
("r", os.O_RDONLY, "dirlink/../etc/hostname", "etc/hostname"),
("r", os.O_RDONLY, "rootlink/rootlink//etc/./hostname", "etc/hostname"),
("r", os.O_RDONLY, "../../etc/hostname", "etc/hostname"),
("r", os.O_RDONLY | os.O_DIRECTORY, "/", "."),
("r", os.O_RDONLY | os.O_DIRECTORY, "usr/../..", "."),
("r", os.O_RDONLY, "chain/l2", "chain/end"),
("r", os.O_RDONLY, "chain/l1", "ELOOP"),
("r", os.O_RDONLY, "loop/a/x", "ELOOP"),
("r", os.O_RDONLY | os.O_NOFOLLOW, "etc/alternatives/hn", "ELOOP"),
("r", O_PATH | os.O_NOFOLLOW, "etc/alternatives/hn", "etc/alternatives/hn"),
("r", os.O_RDONLY | os.O_NOFOLLOW, "lib/x86_64-linux-gnu/libfoo.so.1.2", "usr/lib/x86_64-linux-gnu/libfoo.so.1.2"),
("r", os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY, "dirlink/", "usr"),
("r", os.O_RDONLY, "file/", "ENOTDIR"),
("r", os.O_RDONLY, "file/../etc/hostname", "ENOTDIR"),
("r", os.O_RDONLY, "nonexist/../etc/hostname", "ENOENT"),
("r", os.O_RDWR | os.O_CREAT | os.O_EXCL, "dangling", "EEXIST"),
("r", os.O_RDWR | os.O_CREAT, "dangling", "usr/newfile"),
("r", os.O_RDWR | os.O_CREAT, "missing/newfile", "ENOENT"),
("r", os.O_RDWR | os.O_CREAT, "newdir/", "EISDIR"),
("o", os.O_RDONLY | os.O_NONBLOCK, "proc/self/fd/0", "EXDEV"),
("o", os.O_RDONLY, "proc/self/exe", "EXDEV"),
("o", os.O_RDONLY, "proc/self/cwd/x", "EXDEV"),
("o", os.O_RDONLY, "proc/self/status", "proc"),
]
HOST_CASES = [
("r", os.O_RDONLY, "lib/x86_64-linux-gnu/libbar.so.2", "host:libbar.so.2"),
("r", os.O_RDONLY, "hostdir/sub/data", "host:sub/data"),
("r", os.O_RDONLY | os.O_DIRECTORY, "hostdir/sub", "host:sub"),
("r", os.O_RDONLY, "etc/alternatives/hn", "etc/hostname"),
("r", os.O_RDONLY, "hostdir/missing", "ENOENT"),
]
def make_tree(root, outside):
for folder in ("usr/lib/x86_64-linux-gnu", "etc/alternatives", "loop", "chain"):
(root / folder).mkdir(parents=True)
(root / "usr/lib/x86_64-linux-gnu/libfoo.so.1.2").write_text("x86")
(root / "etc/hostname").write_text("inside")
(root / "file").write_text("f")
(root / "chain/end").write_text("end")
links = {"usr/lib/x86_64-linux-gnu/libfoo.so.1": "libfoo.so.1.2", "lib": "usr/lib", "lib64": "/usr/lib",
"etc/alternatives/hn": "/etc/hostname", "etc/escape": "../../../../../../etc/hostname", "loop/a": "b",
"loop/b": "a", "dirlink": "/usr", "rootlink": "/", "dangling": "/usr/newfile"}
links.update({"chain/l%d" % i: ("l%d" % (i + 1) if i < 41 else "end") for i in range(1, 42)})
links.update({"usr/lib/x86_64-linux-gnu/libbar.so.2": str(outside / "libbar.so.2"), "hostdir": str(outside)})
for link, target in links.items():
os.symlink(target, root / link)
def make_outside(outside):
(outside / "sub").mkdir(parents=True)
(outside / "libbar.so.2").write_text("host")
(outside / "sub/data").write_text("host")
def labels(lines, root, outside):
inodes = {(os.lstat(root).st_dev, os.lstat(root).st_ino): "."}
for base, prefix in ((root, ""), (outside, "host:")):
for folder, dirs, files in os.walk(base):
for name in dirs + files:
st = os.lstat(os.path.join(folder, name))
inodes[(st.st_dev, st.st_ino)] = prefix + os.path.relpath(os.path.join(folder, name), base)
out = []
for line in lines:
kind, *values = line.split()
if kind == "OK":
out.append(inodes.get((int(values[0]), int(values[1])), "proc"))
else:
out.append(errno.errorcode[int(values[0])])
return out
@unittest.skipUnless(shutil.which("cc") and os.path.isdir("/proc/self"), "needs cc and procfs")
class Openat2PreloadTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = Path(tempfile.mkdtemp())
cls.lib = cls.tmp / "libopenat2.so"
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-o", str(cls.lib),
str(PRELOAD / "robust.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "noexec.c"), str(PRELOAD / "writewatch.c"), str(PRELOAD / "clockpause.c"), "-ldl"], check=True)
source = cls.tmp / "openat2.c"
source.write_text(PROGRAM)
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "openat2"), str(source), "-ldl"], check=True)
cls.trees = (cls.tmp / "kernel", cls.tmp / "preload")
cls.outside = cls.tmp / "outside"
make_outside(cls.outside)
for tree in cls.trees:
make_tree(tree, cls.outside)
every = CASES + HOST_CASES
cases = "".join("%s\t%d\t%s\n" % case[:3] for case in every)
result = subprocess.run([str(cls.tmp / "openat2"), *map(str, cls.trees)], input=cases, capture_output=True, text=True,
env=dict(os.environ, LD_PRELOAD=str(cls.lib)), timeout=60)
if result.returncode != 0:
raise AssertionError(result.stdout + result.stderr)
lines = result.stdout.splitlines()
cls.kernel = labels(lines[1:1 + len(CASES)], cls.trees[0], cls.outside) if lines[0] == "kernel" else None
cls.preload = labels(lines[-len(every):], cls.trees[1], cls.outside)
counter = cls.tmp / "counter.c"
counter.write_text(COUNTER)
(cls.tmp / "fp").mkdir()
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-o", str(cls.tmp / "fp/libblfastpath.so"),
str(counter), "-ldl"], check=True)
tree = cls.tmp / "counted"
make_tree(tree, cls.outside)
counted = subprocess.run([str(cls.tmp / "openat2"), str(tree), str(tree)], input=cases, capture_output=True,
text=True, env=dict(os.environ, LD_PRELOAD="%s:%s" % (cls.lib, cls.tmp / "fp/libblfastpath.so")),
timeout=60)
if counted.returncode != 0:
raise AssertionError(counted.stdout + counted.stderr)
cls.counted = labels(counted.stdout.splitlines()[-len(every):], tree, cls.outside)
cls.calls = [int(n) for n in counted.stderr.split("calls ")[-1].split()]
raw = cls.tmp / "raw_open.c"
raw.write_text(RAW_OPEN)
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "raw_open"), str(raw)], check=True)
cls.raw = subprocess.run([str(cls.tmp / "raw_open"), str(tree / "etc/hostname"), str(tree / "missing")],
capture_output=True, text=True, timeout=60,
env=dict(os.environ, LD_PRELOAD="%s:%s" % (cls.lib, cls.tmp / "fp/libblfastpath.so")))
lookup = cls.tmp / "raw_lookup.c"
lookup.write_text(RAW_LOOKUP)
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "raw_lookup"), str(lookup)], check=True)
cls.lookup_args = [str(tree / "etc/hostname"), str(tree / "missing"), str(tree / "etc/alternatives/hn")]
cls.lookup_plain = subprocess.run([str(cls.tmp / "raw_lookup"), *cls.lookup_args], capture_output=True, text=True,
timeout=60, env=dict(os.environ, LD_PRELOAD=str(cls.lib)))
cls.lookup = subprocess.run([str(cls.tmp / "raw_lookup"), *cls.lookup_args], capture_output=True, text=True, timeout=60,
env=dict(os.environ, LD_PRELOAD="%s:%s" % (cls.lib, cls.tmp / "fp/libblfastpath.so")))
(cls.tmp / "real").mkdir()
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-o",
str(cls.tmp / "real/libblfastpath.so"), str(FASTPATH), "-ldl"], check=True)
shutil.copy(cls.tmp / "raw_open", cls.tmp / "FEX")
try:
cls.fex = subprocess.run([str(cls.tmp / "FEX"), str(tree / "etc/hostname"), str(tree / "missing")],
capture_output=True, text=True, timeout=20,
env=dict(os.environ, PROOT_FP_ROOT="/", PROOT_FP_KEY="test",
LD_PRELOAD="%s:%s" % (cls.lib, cls.tmp / "real/libblfastpath.so")))
except subprocess.TimeoutExpired:
cls.fex = None
wrapper = cls.tmp / "wrapper.c"
wrapper.write_text(WRAPPER)
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "wrapper"), str(wrapper), "-ldl"], check=True)
(cls.tmp / "extract").mkdir()
cls.wrapper = subprocess.run([str(cls.tmp / "wrapper"), str(cls.tmp / "extract"), "file"], capture_output=True,
text=True, timeout=20, env=dict(os.environ, LD_PRELOAD=str(cls.lib)))
(cls.tmp / "fex-lookup").mkdir()
shutil.copy(cls.tmp / "raw_lookup", cls.tmp / "fex-lookup/FEX")
try:
cls.fex_lookup = subprocess.run([str(cls.tmp / "fex-lookup/FEX"), *cls.lookup_args], capture_output=True, text=True,
timeout=20, env=dict(os.environ, PROOT_FP_ROOT="/", PROOT_FP_KEY="test",
LD_PRELOAD="%s:%s" % (cls.lib, cls.tmp / "real/libblfastpath.so")))
except subprocess.TimeoutExpired:
cls.fex_lookup = None
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.tmp, ignore_errors=True)
def test_preload_resolves_in_the_root_without_openat2(self):
for case, got in zip(CASES, self.preload):
self.assertEqual(got, case[3], case)
self.assertTrue((self.trees[1] / "usr/newfile").exists())
def test_preload_finishes_a_link_to_a_path_only_outside_the_root_there(self):
for case, got in zip(HOST_CASES, self.preload[len(CASES):]):
self.assertEqual(got, case[3], case)
def test_the_walk_asks_the_fast_path_when_it_is_loaded(self):
self.assertEqual(self.counted, self.preload)
self.assertTrue(all(n > 0 for n in self.calls[:3]), self.calls)
def test_a_raw_openat_syscall_asks_the_fast_path_too(self):
self.assertEqual(self.raw.returncode, 0, self.raw.stderr)
self.assertEqual(self.raw.stdout, "inside\n")
self.assertEqual(self.raw.stderr.split("calls ")[-1].split(), ["2", "0", "0", "0"])
def test_raw_stat_readlink_and_statx_syscalls_ask_the_fast_path(self):
self.assertEqual(self.lookup_plain.returncode, 0, self.lookup_plain.stderr)
self.assertEqual(self.lookup.returncode, 0, self.lookup.stderr)
self.assertEqual(self.lookup.stdout, self.lookup_plain.stdout)
self.assertEqual(self.lookup.stderr.split("calls ")[-1].split(), ["0", "2", "1", "2"])
def test_raw_lookups_in_fex_answer_through_the_real_fast_path(self):
self.assertIsNotNone(self.fex_lookup, "hung")
self.assertEqual(self.fex_lookup.returncode, 0, self.fex_lookup.stderr)
self.assertEqual(self.fex_lookup.stdout, self.lookup_plain.stdout)
def test_a_first_raw_openat_in_fex_does_not_wait_on_the_fast_path_starting(self):
self.assertIsNotNone(self.fex, "hung")
self.assertEqual(self.fex.returncode, 0, self.fex.stderr)
self.assertEqual(self.fex.stdout, "inside\n")
def test_the_libc_openat2_wrapper_creates_files_without_openat2(self):
self.assertEqual(self.wrapper.returncode, 0, self.wrapper.stdout + self.wrapper.stderr)
self.assertTrue((self.tmp / "extract/file").exists())
def test_the_kernel_agrees(self):
if self.kernel is None:
self.skipTest("this kernel or sandbox has no openat2")
for case, got in zip(CASES, self.kernel):
self.assertEqual(got, case[3], case)
if __name__ == "__main__":
unittest.main()