feat(unistd): file I/O syscall wrappers

This commit is contained in:
2026-09-05 01:05:45 -04:00
parent 34408380f0
commit 7b11452b87
14 changed files with 1019 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
#ifndef VLIBC_UNISTD_H
#define VLIBC_UNISTD_H
/*
* vlibc — <unistd.h>.
*
* File descriptors, file I/O, and the access/whence symbolic constants
* (POSIX.1-2008). Every function here is an unbuffered pass-through to the
* kernel: failures are reported as -1 (or the fd/offset on success) with
* errno set by the syscall layer.
*
* Level 1 (onlyposix): read, write, pread, pwrite, open, openat, close,
* lseek, dup, dup2, pipe, fsync, fdatasync, ftruncate,
* sync, access, faccessat.
* Level 2 (muslmimic): dup3, pipe2 (Linux extensions), truncate (XSI),
* lseek64 (glibc LFS alias of lseek on x86_64).
*
* The open-flag constants (O_RDONLY, O_CREAT, O_CLOEXEC, ...) belong to
* <fcntl.h> and are deliberately not defined here; the oflag arguments below
* are plain int and take their values from that header. The optional mode
* argument of open/openat is a mode_t supplied only when oflag contains
* O_CREAT or O_TMPFILE.
*
* None of these declarations carry an intent attribute: every function
* performs I/O with side effects and reports failures through errno, so
* const/pure would be unsound.
*/
#include <vlibc/features.h>
#include <stddef.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Level 1 (POSIX base). */
/* Access-check modes for access() and faccessat(). */
#define F_OK 0 /* existence only */
#define X_OK 1 /* execute (search for a directory) */
#define W_OK 2 /* write */
#define R_OK 4 /* read */
/* whence values for lseek() and lseek64() (also defined by <stdio.h>). */
#define SEEK_SET 0 /* from the beginning of the file */
#define SEEK_CUR 1 /* from the current position */
#define SEEK_END 2 /* from the end of the file */
/*
* Read up to nbyte bytes from fildes into buf and return the number of
* bytes read, 0 at end of file, or -1 with errno set on error. Unbuffered.
*/
ssize_t
read(int fildes, void *buf, size_t nbyte);
/*
* Write up to nbyte bytes from buf to fildes and return the number of
* bytes written, or -1 with errno set on error. Unbuffered.
*/
ssize_t
write(int fildes, const void *buf, size_t nbyte);
/*
* Read nbyte bytes from fildes starting at offset, without changing the
* file position; return the number of bytes read, or -1 with errno set.
*/
ssize_t
pread(int fildes, void *buf, size_t nbyte, off_t offset);
/*
* Write nbyte bytes from buf to fildes starting at offset, without
* changing the file position; return the number of bytes written, or -1
* with errno set.
*/
ssize_t
pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
/*
* Open path with the oflag access mode (from <fcntl.h>) and return a file
* descriptor, or -1 with errno set. A mode argument is required — and read
* from the varargs — only when oflag contains O_CREAT or O_TMPFILE.
*/
int
open(const char *path, int oflag, ...);
/*
* Like open(), but path is relative to the directory named by fd (use
* AT_FDCWD from <fcntl.h> for the current working directory). The mode
* varargs rule is the same as open().
*/
int
openat(int fd, const char *path, int oflag, ...);
/*
* Close the file descriptor fildes; return 0, or -1 with errno set.
*/
int
close(int fildes);
/*
* Reposition the file offset of fildes to offset per whence (SEEK_SET,
* SEEK_CUR, SEEK_END) and return the resulting offset, or (off_t)-1 with
* errno set. The full 64-bit offset is returned; errno is untouched on
* success.
*/
off_t
lseek(int fildes, off_t offset, int whence);
/*
* Duplicate fildes to the lowest-numbered free descriptor; return it, or
* -1 with errno set. The copy shares the file description (position,
* flags, locks) with the original.
*/
int
dup(int fildes);
/*
* Duplicate fildes onto fildes2, closing fildes2 first if it was open;
* return fildes2, or -1 with errno set. dup2(f, f) returns f without
* doing anything (POSIX).
*/
int
dup2(int fildes, int fildes2);
/*
* Create a pipe: fildes[0] becomes the read end, fildes[1] the write end.
* Return 0, or -1 with errno set. No descriptor flags are set (unlike
* pipe2, this is plain POSIX).
*/
int
pipe(int fildes[2]);
/*
* Flush all buffered modifications of fildes and its metadata to stable
* storage; return 0, or -1 with errno set.
*/
int
fsync(int fildes);
/*
* Like fsync(), but may skip the metadata work needed only to preserve
* file contents; return 0, or -1 with errno set.
*/
int
fdatasync(int fildes);
/*
* Truncate fildes to length bytes; return 0, or -1 with errno set.
*/
int
ftruncate(int fildes, off_t length);
/*
* Flush all filesystem caches to stable storage. Returns nothing.
*/
void
sync(void);
/*
* Check accessibility of path under amode (R_OK, W_OK, X_OK, F_OK); return
* 0, or -1 with errno set. Uses the real IDs of the calling process.
*/
int
access(const char *path, int amode);
/*
* Like access(), but path is relative to the directory named by fd (use
* AT_FDCWD for the current working directory) and flag may hold
* AT_EACCESS; return 0, or -1 with errno set.
*/
int
faccessat(int fd, const char *path, int amode, int flag);
#if VLIBC_LEVEL_GE(2)
/* Level 2 (muslmimic): Linux extensions + XSI. */
/*
* Like dup2(), but with descriptor flags (O_CLOEXEC from <fcntl.h>) applied
* atomically; return fildes2, or -1 with errno set. Linux-specific.
*/
int
dup3(int fildes, int fildes2, int flags);
/*
* Like pipe(), but with descriptor flags (e.g. O_CLOEXEC) applied
* atomically; return 0, or -1 with errno set. Linux-specific.
*/
int
pipe2(int fildes[2], int flags);
/*
* Truncate the file named by path to length bytes; return 0, or -1 with
* errno set. XSI.
*/
int
truncate(const char *path, off_t length);
/*
* glibc LFS alias of lseek(): on x86_64 the LFS and non-LFS off_t are
* identical (both 64-bit), so this simply calls lseek(). Provided for
* source compatibility only.
*/
off_t
lseek64(int fildes, off_t offset, int whence);
#endif /* VLIBC_LEVEL_GE(2) */
#ifdef __cplusplus
}
#endif
#endif /* VLIBC_UNISTD_H */
+25
View File
@@ -0,0 +1,25 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
#include "unistd_impl.h"
/*
* access/faccessat over SYS_faccessat (the *at form is the modern kernel
* ABI; SYS_faccessat2 is deliberately not used — the classic syscall
* covers the POSIX amode set). access() passes AT_FDCWD and no flags.
*/
int
access(const char *path, int amode)
{
return syscall_ret(__syscall4(SYS_faccessat, VLIBC_UNISTD_AT_FDCWD, (long)path, amode, 0));
}
int
faccessat(int fd, const char *path, int amode, int flag)
{
return syscall_ret(__syscall4(SYS_faccessat, fd, (long)path, amode, flag));
}
+17
View File
@@ -0,0 +1,17 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* close: unbuffered pass-through. There is nothing to flush — vlibc I/O is
* unbuffered at this layer, and the stdio layer owns its own buffers.
*/
int
close(int fildes)
{
return syscall_ret(__syscall1(SYS_close, fildes));
}
+38
View File
@@ -0,0 +1,38 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* dup/dup2: descriptor duplication. The copies share the file description
* (position, status flags, locks) with the original. On x86_64 SYS_dup2 is
* a two-argument syscall.
*/
int
dup(int fildes)
{
return syscall_ret(__syscall1(SYS_dup, fildes));
}
int
dup2(int fildes, int fildes2)
{
return syscall_ret(__syscall2(SYS_dup2, fildes, fildes2));
}
#if VLIBC_LEVEL_GE(2)
/*
* dup3: dup2 with descriptor flags (O_CLOEXEC) applied atomically.
* Linux-specific; the kernel rejects fildes == fildes2 with EINVAL.
*/
int
dup3(int fildes, int fildes2, int flags)
{
return syscall_ret(__syscall3(SYS_dup3, fildes, fildes2, flags));
}
#endif /* VLIBC_LEVEL_GE(2) */
+23
View File
@@ -0,0 +1,23 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* fsync/fdatasync: flush a descriptor's dirty data (and for fsync, the
* metadata) to stable storage.
*/
int
fsync(int fildes)
{
return syscall_ret(__syscall1(SYS_fsync, fildes));
}
int
fdatasync(int fildes)
{
return syscall_ret(__syscall1(SYS_fdatasync, fildes));
}
+44
View File
@@ -0,0 +1,44 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <unistd.h>
#include "../internal/syscall.h"
/*
* lseek returns a full-width off_t: on x86_64 the offset is a 64-bit long
* that can legitimately exceed INT_MAX, so the value must not be routed
* through syscall_ret() (which narrows to int). The error translation is
* inlined here with the same semantics: errno set from -r and (off_t)-1 on
* error, errno untouched on success.
*/
off_t
lseek(int fildes, off_t offset, int whence)
{
long r = __syscall3(SYS_lseek, fildes, offset, whence);
if (r < 0 && r > -4096)
{
errno = (int)-r;
return (off_t)-1;
}
return (off_t)r;
}
#if VLIBC_LEVEL_GE(2)
/*
* lseek64: glibc LFS alias. On x86_64 the LFS and non-LFS off_t are
* identical (both 64-bit) and SYS_lseek is the single ABI, so this is a
* plain delegation. Provided for source compatibility only.
*/
off_t
lseek64(int fildes, off_t offset, int whence)
{
return lseek(fildes, offset, whence);
}
#endif /* VLIBC_LEVEL_GE(2) */
+51
View File
@@ -0,0 +1,51 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdarg.h>
#include <unistd.h>
#include "../internal/syscall.h"
#include "unistd_impl.h"
/*
* open/openat over SYS_openat (the *at syscall family is the modern kernel
* ABI; SYS_open exists only for compatibility and is not used). The
* optional fourth mode argument is read from the varargs only when oflag
* contains O_CREAT or O_TMPFILE — otherwise the caller supplied no mode
* and 0 is passed, which the kernel ignores. mode_t promotes to unsigned
* int in the varargs list.
*/
int
open(const char *path, int oflag, ...)
{
unsigned mode = 0;
if ((oflag & VLIBC_UNISTD_O_CREAT) != 0 || (oflag & VLIBC_UNISTD_O_TMPFILE) != 0)
{
va_list ap;
va_start(ap, oflag);
mode = va_arg(ap, unsigned int);
va_end(ap);
}
return syscall_ret(
__syscall4(SYS_openat, VLIBC_UNISTD_AT_FDCWD, (long)path, oflag, (long)mode));
}
int
openat(int fd, const char *path, int oflag, ...)
{
unsigned mode = 0;
if ((oflag & VLIBC_UNISTD_O_CREAT) != 0 || (oflag & VLIBC_UNISTD_O_TMPFILE) != 0)
{
va_list ap;
va_start(ap, oflag);
mode = va_arg(ap, unsigned int);
va_end(ap);
}
return syscall_ret(__syscall4(SYS_openat, fd, (long)path, oflag, (long)mode));
}
+31
View File
@@ -0,0 +1,31 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* pipe: plain POSIX pipe via SYS_pipe. Deliberately does NOT set
* O_CLOEXEC — pipe2 is the flag-taking variant (level 2).
*/
int
pipe(int fildes[2])
{
return syscall_ret(__syscall1(SYS_pipe, (long)fildes));
}
#if VLIBC_LEVEL_GE(2)
/*
* pipe2: pipe with descriptor flags (O_CLOEXEC) applied atomically.
* Linux-specific.
*/
int
pipe2(int fildes[2], int flags)
{
return syscall_ret(__syscall2(SYS_pipe2, (long)fildes, flags));
}
#endif /* VLIBC_LEVEL_GE(2) */
+25
View File
@@ -0,0 +1,25 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* pread/pwrite: positional I/O over SYS_pread64/SYS_pwrite64. On x86_64
* the offset is a single 64-bit register argument (off_t == long), so
* there is no lo/hi split. Positional I/O never touches the file position
* — the kernel handles that.
*/
ssize_t
pread(int fildes, void *buf, size_t nbyte, off_t offset)
{
return syscall_ret(__syscall4(SYS_pread64, fildes, (long)buf, (long)nbyte, offset));
}
ssize_t
pwrite(int fildes, const void *buf, size_t nbyte, off_t offset)
{
return syscall_ret(__syscall4(SYS_pwrite64, fildes, (long)buf, (long)nbyte, offset));
}
+25
View File
@@ -0,0 +1,25 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* read/write: unbuffered pass-through to the kernel. The raw result goes
* through syscall_ret(), which returns the byte count on success and -1
* with errno set on error; errno is untouched on success. No buffering, no
* argument inspection.
*/
ssize_t
read(int fildes, void *buf, size_t nbyte)
{
return syscall_ret(__syscall3(SYS_read, fildes, (long)buf, (long)nbyte));
}
ssize_t
write(int fildes, const void *buf, size_t nbyte)
{
return syscall_ret(__syscall3(SYS_write, fildes, (long)buf, (long)nbyte));
}
+18
View File
@@ -0,0 +1,18 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* sync: flush all filesystem caches to stable storage. The kernel's SYS_sync
* always returns 0 and the POSIX interface is void, so the raw result is
* deliberately not inspected (there is no failure to report).
*/
void
sync(void)
{
__syscall0(SYS_sync);
}
+30
View File
@@ -0,0 +1,30 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#include "../internal/syscall.h"
/*
* ftruncate: truncate an open descriptor; POSIX base form.
*/
int
ftruncate(int fildes, off_t length)
{
return syscall_ret(__syscall2(SYS_ftruncate, fildes, length));
}
#if VLIBC_LEVEL_GE(2)
/*
* truncate: truncate by path name; XSI, so it is gated at level 2
* (ftruncate is the POSIX base form).
*/
int
truncate(const char *path, off_t length)
{
return syscall_ret(__syscall2(SYS_truncate, (long)path, length));
}
#endif /* VLIBC_LEVEL_GE(2) */
+33
View File
@@ -0,0 +1,33 @@
#ifndef VLIBC_UNISTD_INTERNAL_H
#define VLIBC_UNISTD_INTERNAL_H
/*
* vlibc — internal constants for the unistd wrappers (temporary).
*
* include/fcntl.h is owned by todo 21; until it lands, the open/openat and
* access wrappers need the O_* flag bits and AT_FDCWD locally. These names
* carry the VLIBC_UNISTD_ prefix so they cannot collide with the real
* constants todo 21 will publish. The values are kernel UAPI facts
* (asm-generic/fcntl.h), transcribed, not invented:
*
* O_RDONLY 0x0, O_WRONLY 0x1, O_RDWR 0x2, O_CREAT 0x40, O_EXCL 0x80,
* O_TRUNC 0x200, O_APPEND 0x400, O_CLOEXEC 0x80000,
* O_TMPFILE 0x410000 (__O_TMPFILE 0x400000 | O_DIRECTORY 0x10000),
* AT_FDCWD -100.
*/
/* fcntl open-flag bits (kernel UAPI; see the header comment above). */
#define VLIBC_UNISTD_O_RDONLY 0x0
#define VLIBC_UNISTD_O_WRONLY 0x1
#define VLIBC_UNISTD_O_RDWR 0x2
#define VLIBC_UNISTD_O_CREAT 0x40
#define VLIBC_UNISTD_O_EXCL 0x80
#define VLIBC_UNISTD_O_TRUNC 0x200
#define VLIBC_UNISTD_O_APPEND 0x400
#define VLIBC_UNISTD_O_CLOEXEC 0x80000
#define VLIBC_UNISTD_O_TMPFILE 0x410000
/* *at syscall base directory (kernel UAPI). */
#define VLIBC_UNISTD_AT_FDCWD (-100)
#endif /* VLIBC_UNISTD_INTERNAL_H */
+445
View File
@@ -0,0 +1,445 @@
/*
* 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;
}