251 lines
7.6 KiB
C
251 lines
7.6 KiB
C
/*
|
|
* vlibc — syscall layer test (todo 1).
|
|
*
|
|
* Exercises the raw __syscall<n>() entry points and the errno TCB-slot
|
|
* mechanism end to end:
|
|
*
|
|
* 1. happy path: __syscall3(SYS_write, 1, "x", 1) emits "x" on stdout and
|
|
* returns 1 (one byte written);
|
|
* 2. failure path: syscall_ret(SYS_openat on a nonexistent path) returns -1
|
|
* and leaves errno == ENOENT in the calling thread's TCB slot;
|
|
* 3. TCB offset mechanics: a fake TCB (a local buffer whose first word is
|
|
* its own address, as the real TCB layout will have) is installed as
|
|
* the FS thread pointer via SYS_arch_prctl; __errno_location() must
|
|
* then point exactly at fake + VLIBC_TCB_ERRNO_OFF, a failing syscall
|
|
* must land its errno in that slot, and the real thread pointer is
|
|
* restored afterwards.
|
|
* 4. register pinning: a 4-arg (SYS_rt_sigprocmask) and a 5-arg
|
|
* (SYS_ppoll) syscall whose later arguments are load-bearing must see
|
|
* them in r10/r8, where the x86_64 kernel ABI places args 4 and 5.
|
|
*
|
|
* All diagnostics go through raw SYS_write (no printf): between installing
|
|
* and restoring the fake thread pointer the test must not call any libc
|
|
* function, whose TLS reads would see the fake TCB.
|
|
*
|
|
* Not part of the library proper; compiled manually for this todo (the
|
|
* tests/ + make check wiring is owned by a later todo).
|
|
*/
|
|
|
|
#include "../src/internal/errno.h"
|
|
#include "../src/internal/syscall.h"
|
|
#include "../src/internal/types.h"
|
|
|
|
/* Flag constants the public <fcntl.h> will own; test-local copies. */
|
|
#define TEST_AT_FDCWD (-100)
|
|
#define TEST_O_RDONLY 0
|
|
#define TEST_O_CLOEXEC 0x80000
|
|
|
|
/* Constants the public <signal.h>/<poll.h> will own; test-local copies. */
|
|
#define TEST_SIG_BLOCK 0
|
|
#define TEST_SIG_SETMASK 2
|
|
#define TEST_SIGUSR1 10
|
|
|
|
/* arch_prctl codes (kernel UAPI). */
|
|
#define TEST_ARCH_SET_FS 0x1002
|
|
#define TEST_ARCH_GET_FS 0x1003
|
|
|
|
static int failures;
|
|
|
|
/* Write a NUL-terminated string to fd via the raw syscall layer. */
|
|
static void
|
|
say(int fd, const char *s)
|
|
{
|
|
ssize_t n = 0;
|
|
|
|
while (s[n] != '\0')
|
|
{
|
|
n++;
|
|
}
|
|
syscall_ret(__syscall3(SYS_write, fd, (long)s, (long)n));
|
|
}
|
|
|
|
static void
|
|
check(int cond, const char *what)
|
|
{
|
|
if (!cond)
|
|
{
|
|
say(2, "FAIL: ");
|
|
say(2, what);
|
|
say(2, "\n");
|
|
failures++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* A stand-in thread control block. The real TCB layout (owned by the
|
|
* startup todo) will store the TCB's own address at offset 0 — the self
|
|
* pointer __builtin_thread_pointer() reads through %fs:0 — and errno at
|
|
* VLIBC_TCB_ERRNO_OFF. This buffer mirrors exactly those two slots.
|
|
*/
|
|
static unsigned long fake_tcb[32];
|
|
|
|
/*
|
|
* The fake TCB must be installed over a saved copy of the real FS base:
|
|
* the kernel's arch_prctl only reads or writes the current base, so the
|
|
* save must happen while the real thread pointer is still installed.
|
|
*/
|
|
static int
|
|
install_fake_tcb(unsigned long *real_fs) // NOLINT(readability-non-const-parameter)
|
|
{
|
|
long r;
|
|
|
|
fake_tcb[0] = (unsigned long)fake_tcb; /* TCB self pointer at slot 0 */
|
|
r = syscall_ret(__syscall2(SYS_arch_prctl, TEST_ARCH_GET_FS, (long)real_fs));
|
|
if (r != 0)
|
|
{
|
|
return 1;
|
|
}
|
|
r = syscall_ret(__syscall2(SYS_arch_prctl, TEST_ARCH_SET_FS, (long)fake_tcb));
|
|
if (r != 0)
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
restore_real_tcb(unsigned long real_fs)
|
|
{
|
|
return syscall_ret(__syscall2(SYS_arch_prctl, TEST_ARCH_SET_FS, (long)real_fs)) != 0;
|
|
}
|
|
|
|
struct test_timespec
|
|
{
|
|
long tv_sec;
|
|
long tv_nsec;
|
|
};
|
|
|
|
/*
|
|
* 4-arg regression probe: block SIGUSR1, read the mask back through a NULL
|
|
* set, restore. The 4th argument (sigsetsize) must reach the kernel in r10;
|
|
* placed anywhere else it fails with -EINVAL.
|
|
*/
|
|
static int
|
|
probe_rt_sigprocmask(void)
|
|
{
|
|
unsigned long mask = 1UL << (TEST_SIGUSR1 - 1);
|
|
unsigned long oldmask = 0;
|
|
unsigned long readback = 0;
|
|
|
|
if (__syscall4(SYS_rt_sigprocmask, TEST_SIG_BLOCK, (long)&mask, (long)&oldmask, 8) != 0)
|
|
{
|
|
return 0;
|
|
}
|
|
if (__syscall4(SYS_rt_sigprocmask, TEST_SIG_BLOCK, 0, (long)&readback, 8) != 0)
|
|
{
|
|
return 0;
|
|
}
|
|
if (__syscall4(SYS_rt_sigprocmask, TEST_SIG_SETMASK, (long)&oldmask, 0, 8) != 0)
|
|
{
|
|
return 0;
|
|
}
|
|
return (readback & (1UL << (TEST_SIGUSR1 - 1))) != 0;
|
|
}
|
|
|
|
/*
|
|
* 5-arg regression probe: ppoll with no fds and a zero timeout must return
|
|
* 0. The 4th argument (sigmask) must reach r10 and the 5th (sigsetsize) r8;
|
|
* misplaced, the kernel reads a pointer value where the set size belongs and
|
|
* answers -EINVAL.
|
|
*/
|
|
static int
|
|
probe_ppoll(void)
|
|
{
|
|
struct test_timespec ts = {0, 0};
|
|
unsigned long mask = 0;
|
|
|
|
return __syscall5(SYS_ppoll, 0, 0, (long)&ts, (long)&mask, 8) == 0;
|
|
}
|
|
|
|
/*
|
|
* Failure scenario (-f): SYS_openat on a nonexistent path must return -1
|
|
* and leave errno == ENOENT in the TCB slot. Prints the observed result to
|
|
* stdout; exits 0 only when the failure behaved exactly as specified.
|
|
*/
|
|
static int
|
|
failure_scenario(void)
|
|
{
|
|
unsigned long real_fs = 0;
|
|
long r;
|
|
int saved_errno;
|
|
|
|
if (install_fake_tcb(&real_fs))
|
|
{
|
|
return 1;
|
|
}
|
|
r = syscall_ret(__syscall4(SYS_openat, TEST_AT_FDCWD, (long)"/nonexistent/vlibc",
|
|
TEST_O_RDONLY | TEST_O_CLOEXEC, 0));
|
|
saved_errno = errno;
|
|
if (restore_real_tcb(real_fs))
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (r == -1 && saved_errno == ENOENT)
|
|
{
|
|
say(1, "openat=-1 errno=ENOENT\n");
|
|
return 0;
|
|
}
|
|
say(1, "openat FAIL\n");
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
long r;
|
|
unsigned long real_fs = 0;
|
|
|
|
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
|
|
{
|
|
return failure_scenario();
|
|
}
|
|
|
|
/* 1. Happy path: one raw byte to stdout, byte count returned. */
|
|
r = __syscall3(SYS_write, 1, (long)"x", 1);
|
|
check(r == 1, "SYS_write returned byte count");
|
|
|
|
/* 2. Point FS at the fake TCB and verify the errno slot mechanics. */
|
|
check(install_fake_tcb(&real_fs) == 0, "arch_prctl fake TCB install");
|
|
|
|
{
|
|
unsigned long now_fs = 0;
|
|
|
|
r = syscall_ret(__syscall2(SYS_arch_prctl, TEST_ARCH_GET_FS, (long)&now_fs));
|
|
check(r == 0 && now_fs == (unsigned long)fake_tcb,
|
|
"arch_prctl(ARCH_GET_FS) reads fake base");
|
|
}
|
|
|
|
check((char *)__errno_location() == (char *)fake_tcb + VLIBC_TCB_ERRNO_OFF,
|
|
"__errno_location == fake TCB + VLIBC_TCB_ERRNO_OFF");
|
|
|
|
r = syscall_ret(__syscall4(SYS_openat, TEST_AT_FDCWD, (long)"/nonexistent/vlibc",
|
|
TEST_O_RDONLY | TEST_O_CLOEXEC, 0));
|
|
check(r == -1, "SYS_openat on nonexistent path returns -1");
|
|
check(errno == ENOENT, "errno == ENOENT via TCB slot");
|
|
|
|
/* 3. Restore the real thread pointer; the fake slot keeps its value. */
|
|
check(restore_real_tcb(real_fs) == 0, "arch_prctl(ARCH_SET_FS, real TCB) restored");
|
|
check(*(int *)((char *)fake_tcb + VLIBC_TCB_ERRNO_OFF) == ENOENT,
|
|
"fake TCB errno slot retains ENOENT after restore");
|
|
|
|
/* 4. Register pinning for 4- and 5-argument syscalls (r10/r8). */
|
|
check(probe_rt_sigprocmask(), "rt_sigprocmask 4-arg r10 pinning");
|
|
check(probe_ppoll(), "ppoll 5-arg r10/r8 pinning");
|
|
|
|
/*
|
|
* VLIBC_TCB_ERRNO_OFF addresses slot 1 of whichever TCB the FS thread
|
|
* pointer selects; under the host libc that slot is its private TLS
|
|
* state, so an errno round-trip through the real thread pointer would
|
|
* corrupt the host libc. The fake-TCB path proves the offset mechanics;
|
|
* the real TCB is exercised once the startup todo installs vlibc's own
|
|
* thread pointer.
|
|
*/
|
|
|
|
if (failures > 0)
|
|
{
|
|
say(2, "FAILED\n");
|
|
}
|
|
return failures == 0 ? 0 : 1;
|
|
}
|