diff --git a/include/sys/uio.h b/include/sys/uio.h new file mode 100644 index 0000000..e08e6a9 --- /dev/null +++ b/include/sys/uio.h @@ -0,0 +1,75 @@ +#ifndef VLIBC_SYS_UIO_H +#define VLIBC_SYS_UIO_H + +/* + * vlibc — . + * + * Scatter/gather I/O: readv() and writev() transfer data between a file + * descriptor and a vector of memory buffers (POSIX.1-2008 base). Both are + * unbuffered pass-throughs to the kernel: the iovec array is handed to + * SYS_readv/SYS_writev as-is, and the kernel applies its own validation + * (iovcnt in 0..IOV_MAX, iov NULL with iovcnt > 0 faults as EFAULT). Failures + * are reported as -1 with errno set by the syscall layer. + * + * Level 1 (onlyposix): readv, writev. + * + * The Linux extensions that build on this ABI (preadv/pwritev, + * process_vm_readv/process_vm_writev, ...) are deliberately absent — they are + * not POSIX and belong to a later compatibility profile. + * + * struct iovec matches the kernel's struct iovec (x86_64: a pointer and a + * size_t, no padding). IOV_MAX is 1024, the Linux UIO_MAXIOV; the kernel + * rejects any iovcnt above it with EINVAL. + * + * 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 + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Maximum number of iovec entries a single readv()/writev() call accepts. */ +#define IOV_MAX 1024 + +/* + * A scatter/gather segment: iov_base points at the buffer (or, for readv, + * the buffer to fill) and iov_len is its size in bytes. Kernel layout, as + * above. + */ +struct iovec +{ + void *iov_base; /* starting address of the segment */ + size_t iov_len; /* number of bytes in the segment */ +}; + +/* + * Read up to the sum of iov_len bytes from fd into the iovcnt buffers of + * iov, in order; return the number of bytes read, 0 at end of file, or -1 + * with errno set on error. Buffers are filled completely before the next + * one is touched. + */ +ssize_t +readv(int fd, const struct iovec *iov, int iovcnt); + +/* + * Write up to the sum of iov_len bytes from the iovcnt buffers of iov to + * fd, in order; return the number of bytes written, or -1 with errno set + * on error. The kernel applies its own validation (iovcnt 0..IOV_MAX, iov + * NULL with iovcnt > 0 faults as EFAULT). + */ +ssize_t +writev(int fd, const struct iovec *iov, int iovcnt); + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_UIO_H */ diff --git a/src/uio/readv.c b/src/uio/readv.c new file mode 100644 index 0000000..4e7a431 --- /dev/null +++ b/src/uio/readv.c @@ -0,0 +1,21 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * readv: scatter read — the kernel fills the iovcnt buffers of iov in order + * and returns the total byte count. The iovec array is passed straight to + * SYS_readv: no argument validation here, the kernel applies its own rules + * (iovcnt 0..IOV_MAX, iov NULL with iovcnt > 0 faults as EFAULT). The raw + * result goes through syscall_ret(), which returns the count on success and + * -1 with errno set on error; errno is untouched on success. + */ +ssize_t +readv(int fd, const struct iovec *iov, int iovcnt) +{ + return syscall_ret(__syscall3(SYS_readv, fd, (long)iov, (long)iovcnt)); +} diff --git a/src/uio/writev.c b/src/uio/writev.c new file mode 100644 index 0000000..1f6914d --- /dev/null +++ b/src/uio/writev.c @@ -0,0 +1,21 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * writev: gather write — the kernel writes the iovcnt buffers of iov in + * order and returns the total byte count. The iovec array is passed straight + * to SYS_writev: no argument validation here, the kernel applies its own + * rules (iovcnt 0..IOV_MAX, iov NULL with iovcnt > 0 faults as EFAULT). The + * raw result goes through syscall_ret(), which returns the count on success + * and -1 with errno set on error; errno is untouched on success. + */ +ssize_t +writev(int fd, const struct iovec *iov, int iovcnt) +{ + return syscall_ret(__syscall3(SYS_writev, fd, (long)iov, (long)iovcnt)); +} diff --git a/tests/test_uio.c b/tests/test_uio.c new file mode 100644 index 0000000..e6e6399 --- /dev/null +++ b/tests/test_uio.c @@ -0,0 +1,332 @@ +/* + * vlibc — readv/writev scatter/gather test (todo 30). + * + * Exercises src/uio/readv.c and src/uio/writev.c end to end over pipe + * pairs created with the raw SYS_pipe syscall (deliberately not the + * pipe() wrapper of todo 19 — the uio functions are what is under test): + * + * Default-mode scenarios (in run order): + * + * 1. writev scatter: writev of the three buffers {"ab","cd","ef"} into a + * pipe returns 6, and a raw read back returns the contiguous 6 bytes + * "abcdef" (byte compare). + * 2. readv gather: "abcdef" written to a pipe; readv over three len-2 + * buffers fills each in order and returns 6; each buffer holds its + * expected slice. + * 3. empty iov: writev(fd, NULL, 0) — iovcnt 0 — returns 0 (the kernel + * never dereferences the iov pointer when iovcnt is 0). + * 4. readv returns total: writev writes the three buffers (6 bytes); a + * single-buffer readv gathers the whole stream back and returns the + * total byte count. + * 5. writev to a closed fd returns -1 (assert -1 only). + * + * The negative path in scenario 5 makes 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 that call is bracketed with a + * save/restore of the slot (task 13 technique) — only vlibc/raw-syscall + * code runs between the write and the restore. 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 + +#include "../include/sys/uio.h" + +#include "../src/internal/syscall.h" + +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; +} + +/* Byte-compare buf against want over n bytes (no string.h in this TU). */ +static int +bytes_eq(const char *buf, const char *want, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + { + if (buf[i] != want[i]) + { + return 0; + } + } + return 1; +} + +/* The three scatter buffers every scenario writes. */ +static void +fill_write_iov(struct iovec *io, size_t n) +{ + static const char ab[2] = {'a', 'b'}; + static const char cd[2] = {'c', 'd'}; + static const char ef[2] = {'e', 'f'}; + + io[0].iov_base = (void *)ab; + io[0].iov_len = 2; + io[1].iov_base = (void *)cd; + io[1].iov_len = 2; + io[2].iov_base = (void *)ef; + io[2].iov_len = 2; + (void)n; +} + +/* 1+4. writev scatter -> contiguous read / readv-gather of the total. */ +static int +scatter_then_gather(void) +{ + struct iovec wio[3]; + struct iovec rio; + char flat[8]; + int fds[2]; + int ok = 1; + + if (__syscall1(SYS_pipe, (long)fds) != 0) + { + check(0, "scatter_then_gather: raw SYS_pipe setup failed"); + return 0; + } + fill_write_iov(wio, 3); + check(writev(fds[1], wio, 3) == 6, "writev of three buffers returns 6"); + check(__syscall3(SYS_read, fds[0], (long)flat, 6) == 6, "raw read back of the pipe returns 6"); + check(bytes_eq(flat, "abcdef", 6), "read-back bytes are the contiguous \"abcdef\""); + check(__syscall1(SYS_close, fds[0]) == 0 && __syscall1(SYS_close, fds[1]) == 0, + "close of the first pipe pair"); + + /* Scenario 4: a fresh pipe, single-buffer readv gathers the total. */ + if (__syscall1(SYS_pipe, (long)fds) != 0) + { + check(0, "readv-total: raw SYS_pipe setup failed"); + return 0; + } + fill_write_iov(wio, 3); + check(writev(fds[1], wio, 3) == 6, "writev fills the second pipe with 6 bytes"); + rio.iov_base = flat; + rio.iov_len = 6; + check(readv(fds[0], &rio, 1) == 6, "readv of a 6-byte iovec returns 6 (the total)"); + check(bytes_eq(flat, "abcdef", 6), "single-buffer readv gathered the whole stream"); + check(__syscall1(SYS_close, fds[0]) == 0 && __syscall1(SYS_close, fds[1]) == 0, + "close of the second pipe pair"); + return ok; +} + +/* 2. readv gathers into three separate buffers, in order. */ +static void +readv_gather_scenario(void) +{ + struct iovec wio[1]; + struct iovec rio[3]; + char r0[2], r1[2], r2[2]; + static const char ab[2] = {'a', 'b'}; + static const char cd[2] = {'c', 'd'}; + static const char ef[2] = {'e', 'f'}; + int fds[2]; + + if (__syscall1(SYS_pipe, (long)fds) != 0) + { + check(0, "readv gather: raw SYS_pipe setup failed"); + return; + } + wio[0].iov_base = (void *)ab; + wio[0].iov_len = 2; + check(writev(fds[1], wio, 1) == 2, "writev writes the \"ab\" segment"); + wio[0].iov_base = (void *)cd; + wio[0].iov_len = 2; + check(writev(fds[1], wio, 1) == 2, "writev writes the \"cd\" segment"); + wio[0].iov_base = (void *)ef; + wio[0].iov_len = 2; + check(writev(fds[1], wio, 1) == 2, "writev writes the \"ef\" segment"); + + rio[0].iov_base = r0; + rio[0].iov_len = 2; + rio[1].iov_base = r1; + rio[1].iov_len = 2; + rio[2].iov_base = r2; + rio[2].iov_len = 2; + check(readv(fds[0], rio, 3) == 6, "readv over three len-2 buffers returns 6"); + check(bytes_eq(r0, "ab", 2) && bytes_eq(r1, "cd", 2) && bytes_eq(r2, "ef", 2), + "each readv buffer holds its slice in order"); + check(__syscall1(SYS_close, fds[0]) == 0 && __syscall1(SYS_close, fds[1]) == 0, + "close of the gather pipe pair"); +} + +/* 3. empty iov (iovcnt 0): the kernel never touches the NULL iov pointer. */ +static void +empty_iov_scenario(void) +{ + int fds[2]; + + if (__syscall1(SYS_pipe, (long)fds) != 0) + { + check(0, "empty iov: raw SYS_pipe setup failed"); + return; + } + check(writev(fds[1], NULL, 0) == 0, "writev(fd, NULL, 0) returns 0"); + check(__syscall1(SYS_close, fds[0]) == 0 && __syscall1(SYS_close, fds[1]) == 0, + "close of the empty-iov pipe pair"); +} + +/* 5. writev on a closed fd -> -1 (errno bracketed in default mode). */ +static void +closed_fd_scenario(void) +{ + struct iovec wio[1]; + static const char ab[2] = {'a', 'b'}; + unsigned long saved; + int fds[2]; + + if (__syscall1(SYS_pipe, (long)fds) != 0) + { + check(0, "closed fd: raw SYS_pipe setup failed"); + return; + } + check(__syscall1(SYS_close, fds[1]) == 0, "close of the write end returns 0"); + wio[0].iov_base = (void *)ab; + wio[0].iov_len = 2; + saved = tcb_slot1(); + check(writev(fds[1], wio, 1) == -1, "writev on the closed write end returns -1"); + tcb_slot1_set(saved); + check(__syscall1(SYS_close, fds[0]) == 0, "close of the read end returns 0"); +} + +/* + * 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). readv(fd, NULL, 1) is the QA + * EFAULT case: with iovcnt 1 the kernel copies the iovec array out of the + * NULL pointer and faults — errno EFAULT, asserted as -1 only. + */ +static int +failure_scenarios(void) +{ + struct iovec io[1]; + char buf[2]; + int rc = 0; + + io[0].iov_base = buf; + io[0].iov_len = 2; + if (readv(-1, io, 1) != -1) + { + say(2, "FAIL: readv(-1, valid iov, 1) did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: readv(-1, valid iov, 1) -> -1\n"); + } + if (readv(0, NULL, 1) != -1) + { + say(2, "FAIL: readv(fd, NULL, 1) did not return -1\n"); + rc = 1; + } + else + { + say(1, "PASS: readv(fd, NULL, 1) -> -1 (kernel EFAULT)\n"); + } + return rc; +} + +int +main(int argc, char **argv) +{ + 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. + */ + int rc = failure_scenarios(); + + __syscall1(SYS_exit_group, rc); + return rc; /* not reached */ + } + + scatter_then_gather(); + readv_gather_scenario(); + empty_iov_scenario(); + closed_fd_scenario(); + + if (failures > 0) + { + say(2, "FAILED ("); + say_dec(2, (unsigned long)failures); + say(2, " check(s))\n"); + return 1; + } + say(1, "all readv/writev tests passed\n"); + return 0; +}