Files
nsjail/unotify/syscall_defs.h
Nexory 935cd6e0b8 unotify: trace faccessat2, truncate, statfs and the xattr syscalls
These path based syscalls are not in kTracedSyscalls, and the generated policy
ends in DEFAULT ALLOW, so they run unobserved and never reach the report.

Measured against the report of a run under --seccomp_unotify, using the glibc
wrappers rather than raw syscall numbers, on a file and on a symlink to it:

  truncate() shrank the file from 21 to 3 bytes inside the jail, verified from
  outside. The report attributed no truncate to that path.

  setfattr set an attribute on a file inside the jail; the attribute was still
  there after the run, and the report attributed nothing to that path.

  glibc faccessat(AT_EACCESS) produced no entry at all, and no faccessat
  appeared either, so glibc had issued faccessat2.

faccessat2 is the same case as the statx entry added in c20246a: modern glibc
reaches for it first and only falls back to the traced faccessat on older
kernels. A plain access(2) is still traced by its own entry; what goes unseen
is the check made through faccessat().

truncate changes a file's contents without opening it, so nothing in the report
marks that path as written.

The l* variants are listed separately, the same way stat and lstat are, and are
exercised through a symlink so that all eleven entries are covered.

The flags argument of faccessat2 is left undecoded, the same as every other
AT_* entry in the table.
2026-08-25 05:17:02 +02:00

328 lines
12 KiB
C++

#ifndef NSJAIL_UNOTIFY_SYSCALL_DEFS_H
#define NSJAIL_UNOTIFY_SYSCALL_DEFS_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <sys/syscall.h>
namespace unotify {
enum class SyscallCategory : uint8_t {
FS,
NET,
};
/*
* Describes the role of each syscall argument for automatic decoding.
*
* The generic decoder processes args[0..5] left-to-right, maintaining:
* current_dirfd - set by DIRFD, consumed+reset by PATH
* last_socket_type - set by FD, consumed by SADDR
* last_path - set by PATH, consumed by FLAGS/OHOW
*/
enum class ArgRole : uint8_t {
SKIP, /* Don't decode this argument */
PATH, /* String ptr -> populatePathInfo (alternates path1/path2) */
DIRFD, /* Directory fd; sets context for next PATH */
FLAGS, /* Open flags -> getFileMode on last populated path */
OCTAL, /* Permission mode -> "mode=0NNN" */
ACCESS, /* Access mode -> "mode=R_OK|W_OK|X_OK" */
UID, /* User id -> "owner=N" */
GID, /* Group id -> "group=N" */
ARGV, /* String array ptr -> "argv[i]=..." */
ENVP, /* String array ptr -> "envp[i]=..." */
FD, /* Socket fd -> "fd=N" + getSocketType */
SADDR, /* sockaddr ptr; next arg must be ALEN */
ALEN, /* sockaddr length; consumed by preceding SADDR */
IFLAGS, /* Integer flags -> "flags=N" */
DOMAIN, /* Socket domain -> getDomainStr */
STYPE, /* Socket type -> getTypeStr */
PROTO, /* Protocol -> getProtocolStr */
OHOW, /* struct open_how ptr -> decode flags + resolve */
};
struct SyscallDef {
int nr; /* __NR_xxx */
const char* kafel_name; /* name for kafel policy ("newstat", "sendmsg") */
const char* display_name; /* name for stats output ("stat", "sendmsg") */
SyscallCategory category;
ArgRole args[6]; /* role of each syscall argument */
};
/* Shorthand for table readability */
using A = ArgRole;
/*
* THE SINGLE SOURCE OF TRUTH for which syscalls are traced via seccomp unotify.
*
* To add a new traced syscall: add one entry here.
* The kafel BPF policy, name lookup, and argument decoding all derive from this table.
*/
static constexpr SyscallDef kTracedSyscalls[] = {
/* FS - arg0 = path (AT_FDCWD implied) */
#ifdef __NR_open
{__NR_open, "open", "open", SyscallCategory::FS,
{A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_open */
#ifdef __NR_stat
{__NR_stat, "newstat", "stat", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_stat */
#ifdef __NR_lstat
{__NR_lstat, "newlstat", "lstat", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_lstat */
/* statfs(path, buf) reads filesystem state through a path, like stat above. */
#ifdef __NR_statfs
{__NR_statfs, "statfs", "statfs", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_statfs */
#ifdef __NR_access
{__NR_access, "access", "access", SyscallCategory::FS,
{A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_access */
#ifdef __NR_chmod
{__NR_chmod, "chmod", "chmod", SyscallCategory::FS,
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_chmod */
#ifdef __NR_chown
{__NR_chown, "chown", "chown", SyscallCategory::FS,
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_chown */
#ifdef __NR_lchown
{__NR_lchown, "lchown", "lchown", SyscallCategory::FS,
{A::PATH, A::UID, A::GID, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_lchown */
/*
* truncate(path, length) writes to a file without opening it, so without
* this entry the content change is not observed at all.
*/
#ifdef __NR_truncate
{__NR_truncate, "truncate", "truncate", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_truncate */
#ifdef __NR_readlink
{__NR_readlink, "readlink", "readlink", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_readlink */
#ifdef __NR_mkdir
{__NR_mkdir, "mkdir", "mkdir", SyscallCategory::FS,
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_mkdir */
#ifdef __NR_mknod
{__NR_mknod, "mknod", "mknod", SyscallCategory::FS,
{A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_mknod */
#ifdef __NR_rmdir
{__NR_rmdir, "rmdir", "rmdir", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_rmdir */
#ifdef __NR_unlink
{__NR_unlink, "unlink", "unlink", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_unlink */
#ifdef __NR_execve
{__NR_execve, "execve", "execve", SyscallCategory::FS,
{A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_execve */
#ifdef __NR_chdir
{__NR_chdir, "chdir", "chdir", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_chdir */
#ifdef __NR_chroot
{__NR_chroot, "chroot", "chroot", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_chroot */
/*
* Extended attributes are file metadata addressed by path, like chmod and
* chown above. setxattr and removexattr modify a file, getxattr and
* listxattr read it. The l* variants do not follow symlinks, the same
* distinction as stat and lstat.
*/
#ifdef __NR_setxattr
{__NR_setxattr, "setxattr", "setxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_setxattr */
#ifdef __NR_lsetxattr
{__NR_lsetxattr, "lsetxattr", "lsetxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_lsetxattr */
#ifdef __NR_getxattr
{__NR_getxattr, "getxattr", "getxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_getxattr */
#ifdef __NR_lgetxattr
{__NR_lgetxattr, "lgetxattr", "lgetxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_lgetxattr */
#ifdef __NR_listxattr
{__NR_listxattr, "listxattr", "listxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_listxattr */
#ifdef __NR_llistxattr
{__NR_llistxattr, "llistxattr", "llistxattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_llistxattr */
#ifdef __NR_removexattr
{__NR_removexattr, "removexattr", "removexattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_removexattr */
#ifdef __NR_lremovexattr
{__NR_lremovexattr, "lremovexattr", "lremovexattr", SyscallCategory::FS,
{A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_lremovexattr */
/* FS - arg0 = dirfd, arg1 = path */
#ifdef __NR_openat
{__NR_openat, "openat", "openat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::FLAGS, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_openat */
#ifdef __NR_openat2
{__NR_openat2, "openat2", "openat2", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::OHOW, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_openat2 */
#ifdef __NR_newfstatat
{__NR_newfstatat, "newfstatat", "newfstatat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_newfstatat */
/*
* statx(dirfd, path, flags, mask, statxbuf). Modern glibc/coreutils
* (ls, stat) call statx before newfstatat/openat, so without this entry
* a stat of a path that is not mounted in the jail is never observed.
* arg1 is the path (arg0 is the dirfd), same shape as newfstatat.
*/
#ifdef __NR_statx
{__NR_statx, "statx", "statx", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_statx */
#ifdef __NR_faccessat
{__NR_faccessat, "faccessat", "faccessat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_faccessat */
/*
* faccessat2(dirfd, path, mode, flags) is what glibc's faccessat() uses on
* modern kernels; it only falls back to faccessat when the kernel lacks it.
* Without this entry an access check made through glibc's faccessat() is
* never observed; a plain access(2) is still traced by its own entry.
*/
#ifdef __NR_faccessat2
{__NR_faccessat2, "faccessat2", "faccessat2", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::ACCESS, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_faccessat2 */
#ifdef __NR_fchmodat
{__NR_fchmodat, "fchmodat", "fchmodat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_fchmodat */
#ifdef __NR_fchownat
{__NR_fchownat, "fchownat", "fchownat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::UID, A::GID, A::SKIP, A::SKIP}},
#endif /* __NR_fchownat */
#ifdef __NR_readlinkat
{__NR_readlinkat, "readlinkat", "readlinkat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_readlinkat */
#ifdef __NR_mkdirat
{__NR_mkdirat, "mkdirat", "mkdirat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_mkdirat */
#ifdef __NR_mknodat
{__NR_mknodat, "mknodat", "mknodat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::OCTAL, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_mknodat */
#ifdef __NR_unlinkat
{__NR_unlinkat, "unlinkat", "unlinkat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_unlinkat */
#ifdef __NR_execveat
{__NR_execveat, "execveat", "execveat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::ARGV, A::ENVP, A::SKIP, A::SKIP}},
#endif /* __NR_execveat */
/* FS - two paths */
#ifdef __NR_rename
{__NR_rename, "rename", "rename", SyscallCategory::FS,
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_rename */
#ifdef __NR_link
{__NR_link, "link", "link", SyscallCategory::FS,
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_link */
#ifdef __NR_symlink
{__NR_symlink, "symlink", "symlink", SyscallCategory::FS,
{A::PATH, A::PATH, A::SKIP, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_symlink */
/* FS - two dirfd+path pairs */
#ifdef __NR_renameat
{__NR_renameat, "renameat", "renameat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
#endif /* __NR_renameat */
#ifdef __NR_renameat2
{__NR_renameat2, "renameat2", "renameat2", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
#endif /* __NR_renameat2 */
#ifdef __NR_linkat
{__NR_linkat, "linkat", "linkat", SyscallCategory::FS,
{A::DIRFD, A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP}},
#endif /* __NR_linkat */
/* symlinkat: arg0=target(CWD), arg1=newdirfd, arg2=newpath */
#ifdef __NR_symlinkat
{__NR_symlinkat, "symlinkat", "symlinkat", SyscallCategory::FS,
{A::PATH, A::DIRFD, A::PATH, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_symlinkat */
/* NET - sockaddr-based */
#ifdef __NR_connect
{__NR_connect, "connect", "connect", SyscallCategory::NET,
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_connect */
#ifdef __NR_bind
{__NR_bind, "bind", "bind", SyscallCategory::NET,
{A::FD, A::SADDR, A::ALEN, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_bind */
#ifdef __NR_sendto
{__NR_sendto, "sendto", "sendto", SyscallCategory::NET,
{A::FD, A::SKIP, A::SKIP, A::IFLAGS, A::SADDR, A::ALEN}},
#endif /* __NR_sendto */
/* NET - socket creation */
#ifdef __NR_socket
{__NR_socket, "socket", "socket", SyscallCategory::NET,
{A::DOMAIN, A::STYPE, A::PROTO, A::SKIP, A::SKIP, A::SKIP}},
#endif /* __NR_socket */
};
constexpr size_t kTracedSyscallCount = sizeof(kTracedSyscalls) / sizeof(kTracedSyscalls[0]);
/*
* Build the kafel policy string from the table.
* Called by sandbox.cc - no manual syscall list maintenance needed.
*
* The syscall number is used rather than the name. kafel resolves names from a
* per-architecture table, and those tables are not equally complete, so a name
* that resolves on one architecture can be unknown on another; kafel_compile()
* then fails and nsjail does not start at all. The number is a compile time
* constant of the architecture being built for. The name is kept as a comment
* so the generated policy stays readable.
*/
inline std::string buildKafelPolicy() {
std::string p = "POLICY unotify {\n USER_NOTIF {\n";
for (size_t i = 0; i < kTracedSyscallCount; i++) {
if (i > 0) p += ", ";
p += "SYSCALL[" + std::to_string(kTracedSyscalls[i].nr) + "] /* " +
kTracedSyscalls[i].kafel_name + " */";
}
p += "\n }\n}\nUSE unotify DEFAULT ALLOW\n";
return p;
}
} // namespace unotify
#endif /* NSJAIL_UNOTIFY_SYSCALL_DEFS_H */