446 lines
14 KiB
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;
|
|
}
|