Files
vlibc/tests/test_unistd_file.c

446 lines
14 KiB
C

/*
* vlibc — unistd file I/O test (todo 19).
*
* Exercises the raw syscall wrappers end to end:
*
* 1. open(O_RDWR|O_CREAT|O_EXCL, 0600) + 4 KiB write + seek/read
* round-trip, then unlink via raw SYS_unlinkat.
* 2. pread/pwrite positional I/O: pread(fd, buf, 4, 0) reads the first
* four bytes after the file position advanced to the end, and the
* position is untouched by positional calls.
* 3. lseek SEEK_SET/CUR/END offsets, including lseek(SEEK_END) after
* ftruncate to 100.
* 4. pipe + write + read transports bytes.
* 5. dup2 duplicates a descriptor (a write through the dup is visible
* through the original); level-2 dup3 rejects same-fd with -1 and
* duplicates with O_CLOEXEC.
* 6. fsync/fdatasync on the temp file return 0.
* 7. sync() completes.
* 8. access/faccessat on the existing file return 0; on a nonexistent
* path return -1.
* 9. open flag validation: O_RDONLY on the existing file, O_CREAT|O_EXCL
* on the existing file → -1, O_TRUNC truncates.
*
* Level-2 gated section: dup3, pipe2, truncate, lseek64.
*
* The negative paths make the LIBRARY write errno (syscall_ret), which
* under a host-linked binary targets glibc's private dtv slot at %fs:0+8.
* In the default mode each such call is bracketed with a save/restore of
* that slot (task 13 technique) — only vlibc/raw-syscall code runs between
* the write and the restore, so host state is intact when host code runs
* again. The test itself NEVER reads errno; every negative is asserted on
* the return value. The -f mode runs the failure scenarios and exits via
* raw SYS_exit_group (house pattern, tests/test_malloc.c).
*
* All diagnostics go through raw SYS_write (no stdio): under -Iinclude the
* vlibc public headers shadow GCC's internal ones, so a host header would
* not compile. Not part of the library proper; compiled manually for this
* todo (the tests/ + make check wiring is owned by a later todo).
*/
#include <stddef.h>
#include "../include/unistd.h"
#include "../src/internal/syscall.h"
/* Kernel-UAPI open flags, local to this test (include/fcntl.h is todo 21). */
#define T19_O_RDONLY 0x0
#define T19_O_WRONLY 0x1
#define T19_O_RDWR 0x2
#define T19_O_CREAT 0x40
#define T19_O_EXCL 0x80
#define T19_O_TRUNC 0x200
#define T19_O_CLOEXEC 0x80000
#define T19_AT_FDCWD (-100)
static int failures;
/* Write a NUL-terminated string to fd via the raw syscall layer. The
* optimize attribute keeps GCC from lowering the length loop into a
* strlen call, which would leave a vlibc-owned symbol undefined in this
* host-linked standalone binary (house idiom, see src/string). */
static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void
say(int fd, const char *s)
{
long n = 0;
while (s[n] != '\0')
{
n++;
}
__syscall3(SYS_write, fd, (long)s, n);
}
/* Write v in decimal to fd. */
static void
say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters)
{
char buf[24];
int i = (int)sizeof(buf);
buf[--i] = '\0';
do
{
buf[--i] = (char)('0' + (v % 10));
v /= 10;
} while (v != 0);
__syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i));
}
static void
check(int cond, const char *what)
{
if (cond)
{
say(1, "PASS: ");
say(1, what);
say(1, "\n");
}
else
{
say(2, "FAIL: ");
say(2, what);
say(2, "\n");
failures++;
}
}
/*
* Host-TCB slot-1 bracket: the library's errno write on a negative path
* lands at %fs:0+8, glibc's dtv pointer. Save and restore it around each
* such call; only vlibc/raw-syscall code runs in between (task 13
* technique).
*/
static unsigned long
tcb_slot1(void)
{
return *(unsigned long *)((char *)__builtin_thread_pointer() + 8);
}
static void
tcb_slot1_set(unsigned long value)
{
*(unsigned long *)((char *)__builtin_thread_pointer() + 8) = value;
}
/* 1+2+3: file round-trip, positional I/O, seeks, on one temp file. */
static int
file_scenarios(const char *path)
{
static unsigned char pattern[4096];
unsigned char rbuf[4096];
unsigned char small[4];
unsigned i;
int ok = 1;
int fd;
for (i = 0; i < sizeof(pattern); i++)
{
pattern[i] = (unsigned char)(i * 7 + 1);
}
fd = open(path, T19_O_RDWR | T19_O_CREAT | T19_O_EXCL, 0600);
check(fd >= 0, "open O_RDWR|O_CREAT|O_EXCL 0600 returns a descriptor");
if (fd < 0)
{
return -1;
}
check(write(fd, pattern, sizeof(pattern)) == (ssize_t)sizeof(pattern),
"write of 4096 bytes returns 4096");
check(lseek(fd, 0, SEEK_SET) == 0, "lseek SEEK_SET 0 returns 0");
check(read(fd, rbuf, sizeof(rbuf)) == (ssize_t)sizeof(rbuf), "read of 4096 bytes returns 4096");
for (i = 0; i < sizeof(pattern); i++)
{
if (rbuf[i] != pattern[i])
{
ok = 0;
}
}
check(ok, "read-back matches the written 4096 bytes");
/* The file position is now at EOF (4096). */
check(pread(fd, small, 4, 0) == 4, "pread(fd, buf, 4, 0) returns 4");
check(small[0] == pattern[0] && small[1] == pattern[1] && small[2] == pattern[2] &&
small[3] == pattern[3],
"pread reads the first four bytes");
check(lseek(fd, 0, SEEK_CUR) == 4096, "pread leaves the position at 4096");
small[0] = 'W';
small[1] = 'X';
small[2] = 'Y';
small[3] = 'Z';
check(pwrite(fd, small, 4, 100) == 4, "pwrite(fd, buf, 4, 100) returns 4");
small[0] = 0;
small[1] = 0;
small[2] = 0;
small[3] = 0;
check(pread(fd, small, 4, 100) == 4, "pread(fd, buf, 4, 100) returns 4");
check(small[0] == 'W' && small[1] == 'X' && small[2] == 'Y' && small[3] == 'Z',
"pwrite/pread round-trip at offset 100");
check(lseek(fd, 0, SEEK_CUR) == 4096, "position still 4096 after positional I/O");
/* Scenario 3: ftruncate + seek offsets. */
check(ftruncate(fd, 100) == 0, "ftruncate(fd, 100) returns 0");
check(lseek(fd, 0, SEEK_END) == 100, "lseek SEEK_END after ftruncate returns 100");
check(lseek(fd, 10, SEEK_SET) == 10, "lseek SEEK_SET 10 returns 10");
check(lseek(fd, 5, SEEK_CUR) == 15, "lseek SEEK_CUR +5 from 10 returns 15");
check(lseek(fd, -7, SEEK_CUR) == 8, "lseek SEEK_CUR -7 from 15 returns 8");
return fd;
}
/* 4. pipe + write + read transports bytes. */
static void
pipe_scenario(void)
{
int fds[2];
char buf[8];
long n = 0;
check(pipe(fds) == 0, "pipe returns 0");
check(write(fds[1], "hello", 5) == 5, "write to the pipe write end returns 5");
check(read(fds[0], buf, sizeof(buf)) == 5, "read from the pipe read end returns 5");
while (n < 5 && buf[n] == "hello"[n])
{
n++;
}
check(n == 5, "pipe transports the five bytes intact");
check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe ends returns 0");
}
/* 5. dup2 duplicates a descriptor sharing the file description. */
static void
dup_scenarios(int fd)
{
unsigned char b[1];
int d;
if (fd < 0)
{
return;
}
d = dup2(fd, 200);
check(d == 200, "dup2(fd, 200) returns 200");
if (d != 200)
{
return;
}
check(lseek(200, 0, SEEK_SET) == 0, "the dup shares the file position");
check(write(200, "Z", 1) == 1, "write through the dup returns 1");
check(pread(fd, b, 1, 0) == 1 && b[0] == 'Z', "write through dup visible on original");
check(dup2(200, 200) == 200, "dup2(x, x) returns x (no-op)");
check(close(200) == 0, "close(200) returns 0");
}
/* 6+7. fsync/fdatasync/sync on the temp file. */
static void
sync_scenarios(int fd)
{
if (fd < 0)
{
return;
}
check(fsync(fd) == 0, "fsync on the temp file returns 0");
check(fdatasync(fd) == 0, "fdatasync on the temp file returns 0");
sync();
check(1, "sync() completes");
}
/* 8. access/faccessat happy + negative (bracketed). */
static void
access_scenarios(const char *path)
{
unsigned long saved;
check(access(path, F_OK) == 0, "access on the existing file with F_OK returns 0");
check(access(path, R_OK) == 0, "access on the existing file with R_OK returns 0");
check(access(path, W_OK) == 0, "access on the existing file with W_OK returns 0");
check(faccessat(T19_AT_FDCWD, path, F_OK, 0) == 0,
"faccessat(AT_FDCWD, path, F_OK, 0) returns 0");
saved = tcb_slot1();
check(access("/nonexistent-vlibc-t19", F_OK) == -1, "access on a nonexistent path returns -1");
tcb_slot1_set(saved);
saved = tcb_slot1();
check(faccessat(T19_AT_FDCWD, "/nonexistent-vlibc-t19", F_OK, 0) == -1,
"faccessat on a nonexistent path returns -1");
tcb_slot1_set(saved);
}
/* 9. open flag validation (O_TRUNC runs last: it resets the size). */
static void
open_flag_scenarios(const char *path)
{
unsigned long saved;
int fd;
fd = open(path, T19_O_RDONLY);
check(fd >= 0, "open with O_RDONLY on the existing file succeeds");
if (fd >= 0)
{
check(close(fd) == 0, "close of the O_RDONLY descriptor returns 0");
}
saved = tcb_slot1();
check(open(path, T19_O_CREAT | T19_O_EXCL, 0600) == -1,
"open O_CREAT|O_EXCL on the existing file returns -1");
tcb_slot1_set(saved);
fd = open(path, T19_O_WRONLY | T19_O_TRUNC);
check(fd >= 0, "open with O_WRONLY|O_TRUNC succeeds");
if (fd >= 0)
{
check(lseek(fd, 0, SEEK_END) == 0, "O_TRUNC leaves the size at 0");
check(close(fd) == 0, "close of the O_TRUNC descriptor returns 0");
}
}
#if VLIBC_LEVEL_GE(2)
/* Level-2 gate proof: dup3, pipe2, truncate, lseek64. */
static void
level2_scenarios(int fd)
{
const char *path2 = "/tmp/vlibc-t19-l2";
unsigned long saved;
char buf[8];
int fds[2];
long n = 0;
int d;
int fd2;
if (fd < 0)
{
return;
}
saved = tcb_slot1();
check(dup3(fd, fd, 0) == -1, "dup3(fd, fd, 0) returns -1 (same-fd)");
tcb_slot1_set(saved);
d = dup3(fd, 201, T19_O_CLOEXEC);
check(d == 201, "dup3(fd, 201, O_CLOEXEC) returns 201");
if (d == 201)
{
check(close(201) == 0, "close of the dup3 descriptor returns 0");
}
check(pipe2(fds, T19_O_CLOEXEC) == 0, "pipe2 with O_CLOEXEC returns 0");
check(write(fds[1], "hi", 2) == 2, "write to the pipe2 write end returns 2");
check(read(fds[0], buf, sizeof(buf)) == 2, "read from the pipe2 read end returns 2");
while (n < 2 && buf[n] == "hi"[n])
{
n++;
}
check(n == 2, "pipe2 transports the two bytes intact");
check(close(fds[0]) == 0 && close(fds[1]) == 0, "close of both pipe2 ends returns 0");
fd2 = open(path2, T19_O_RDWR | T19_O_CREAT | T19_O_EXCL, 0600);
check(fd2 >= 0, "open creates the second temp file for truncate");
if (fd2 >= 0)
{
check(write(fd2, "abcdef", 6) == 6, "write six bytes to the second file");
check(truncate(path2, 42) == 0, "truncate(path, 42) returns 0");
check(lseek64(fd2, 0, SEEK_END) == 42, "lseek64 SEEK_END after truncate returns 42");
check(lseek64(fd2, 0, SEEK_SET) == 0, "lseek64 SEEK_SET 0 returns 0");
check(close(fd2) == 0, "close of the second temp file returns 0");
}
check(__syscall3(SYS_unlinkat, T19_AT_FDCWD, (long)path2, 0) == 0,
"unlink of the second temp file returns 0");
}
#endif /* VLIBC_LEVEL_GE(2) */
/*
* Failure scenarios (-f): every assertion is on the return value only, and
* the process exits through raw SYS_exit_group because the library writes
* errno on these paths (host-TCB hazard).
*/
static int
failure_scenarios(void)
{
const char *path = "/tmp/vlibc-t19-f";
int rc = 0;
int fd;
if (open("/nonexistent/vlibc/t19", T19_O_RDONLY) != -1)
{
say(2, "FAIL: open on a nonexistent path did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: open on a nonexistent path -> -1\n");
}
fd = open(path, T19_O_RDWR | T19_O_CREAT | T19_O_TRUNC, 0600);
if (fd < 0)
{
say(2, "FAIL: -f setup open failed\n");
return 1;
}
if (close(fd) != 0)
{
say(2, "FAIL: -f setup close failed\n");
rc = 1;
}
if (write(fd, "x", 1) != -1)
{
say(2, "FAIL: write on a closed fd did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: write on a closed fd -> -1\n");
}
if (lseek(fd, 0, SEEK_SET) != -1)
{
say(2, "FAIL: lseek on a closed fd did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: lseek on a closed fd -> -1\n");
}
__syscall3(SYS_unlinkat, T19_AT_FDCWD, (long)path, 0);
return rc;
}
int
main(int argc, char **argv)
{
const char *path = "/tmp/vlibc-t19-XXXX";
int rc;
int fd;
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
/*
* The failure scenarios write errno inside the library; under the
* host libc that slot is glibc's private TLS state, so leave via
* the raw syscall without running host cleanup.
*/
rc = failure_scenarios();
__syscall1(SYS_exit_group, rc);
return rc; /* not reached */
}
fd = file_scenarios(path);
pipe_scenario();
dup_scenarios(fd);
sync_scenarios(fd);
access_scenarios(path);
open_flag_scenarios(path);
#if VLIBC_LEVEL_GE(2)
level2_scenarios(fd);
#endif
if (fd >= 0)
{
check(close(fd) == 0, "close of the main temp file returns 0");
}
check(__syscall3(SYS_unlinkat, T19_AT_FDCWD, (long)path, 0) == 0,
"unlink of the temp file returns 0");
if (failures > 0)
{
say(2, "FAILED (");
say_dec(2, (unsigned long)failures);
say(2, " check(s))\n");
return 1;
}
say(1, "all unistd file I/O tests passed\n");
return 0;
}