/* * vlibc — getline/getdelim, stream locking, memory streams test (todo 18). * * Exercises the todo-18 additions end to end: * * 1. getline on a 1 MiB line returns 1000001 (delimiter included) with a * NUL terminator, growing a NULL/zero-capacity buffer from scratch; * the next call at end of file returns -1. Reuse keeps the grown * buffer. * 2. getdelim with ',' over "a,b,c\n" yields "a,", "b,", then the * partial final field "c\n" at end of file, then -1. * 3. Embedded NUL bytes survive: getline on "ab\0cd\n" returns 6 with * the NUL at line[2] intact (length, not strlen). * 4. A small preallocated buffer (malloc(4)) grows to fit a long line. * 5. flockfile recursion: two nested flockfile calls, ftrylockfile * failing (nonzero) under the recursion and succeeding (0) when the * stream is free again. * 6. getc_unlocked/putc_unlocked under a held flockfile move real data; * getchar_unlocked/putchar_unlocked and puts work over freopen'd * stdin/stdout. * 7. fmemopen over a fixed buffer: write, rewind, read back * byte-identical; seeking past the size reads EOF (no error); * fmemopen(NULL, ...) grows and is freed by fclose (leak-checked); * "a" starts at the current data length. * 8. open_memstream: after fflush/fclose *sizeloc is the length and the * buffer is NUL-terminated; *ptr tracks the (possibly moved) buffer. * 9. fclose over many fmemopen/open_memstream cycles leaks nothing. * * Failure mode (-f): getline on a stream whose underlying descriptor was * closed underneath it returns -1 (the read hits EBADF); the return value * is asserted, never errno. -f exits via a raw SYS_exit_group before any * host-libc cleanup runs (the tests/syscall_test.c discipline). * * errno is never READ here; the default mode keeps every library errno * write out of the exercised paths. All diagnostics go through raw * SYS_write and the only headers are vlibc's own. * * Not part of the library proper; compiled manually for this todo. */ #include #include #include "../src/internal/syscall.h" /* The allocator's heap-walk consistency probe (hidden; linked in via * malloc.c). Returns the live block count, or (size_t)-1 on disagreement. */ extern size_t __vlibc_malloc_check(void); // NOLINT(bugprone-reserved-identifier) static int failures; /* Write a NUL-terminated string to fd via the raw syscall layer. */ static 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++; } } /* Byte-compare two buffers. noipa keeps -O2 from folding the check. */ // NOLINTBEGIN(clang-analyzer-unix.Stream) static __attribute__((noipa)) int mem_eq(const unsigned char *a, const unsigned char *b, unsigned long n) { unsigned long i; for (i = 0; i < n; i++) { if (a[i] != b[i]) { return 0; } } return 1; } /* Byte-compare two NUL-terminated strings. */ static __attribute__((noipa)) int str_eq(const char *a, const char *b) { while (*a == *b && *a != '\0') { a++; b++; } return *a == *b; } /* Read a whole small file through raw syscalls into buf; returns size. */ static long raw_read_all(const char *path, char *buf, unsigned long cap) { long fd = __syscall4(SYS_openat, -100, (long)path, 0x0, 0); long got = -1; long total = 0; if (fd < 0) { return -1; } while (total < (long)cap) { got = __syscall3(SYS_read, fd, (long)(buf + total), (long)(cap - (unsigned long)total)); if (got <= 0) { break; } total += got; } __syscall1(SYS_close, fd); return got < 0 ? -1 : total; } /* Write path bytes to a fresh file, then reopen it for reading. */ // NOLINTBEGIN(bugprone-easily-swappable-parameters) static FILE * write_file(const char *path, const char *data, unsigned long n) { FILE *f = fopen(path, "w"); if (f == NULL) { return NULL; } if (fwrite(data, 1, n, f) != n) { (void)fclose(f); return NULL; } if (fclose(f) != 0) { return NULL; } f = fopen(path, "r"); if (f == NULL) { say(2, "FAIL: write_file reopen\n"); failures++; } return f; } // NOLINTEND(bugprone-easily-swappable-parameters) static char onemeg[1000001]; /* 1/3/4: getline length, growth, EOF, embedded NUL. */ static void line_scenario(void) { const char bigpath[] = "/tmp/vlibc-test-t18-bigline.txt"; const char nulpath[] = "/tmp/vlibc-test-t18-nul.txt"; const char longpath[] = "/tmp/vlibc-test-t18-long.txt"; const char longline[] = "this line is way longer than sixteen bytes\n"; FILE *f; char *line; size_t n; ssize_t got; unsigned long i; for (i = 0; i < 1000000; i++) { onemeg[i] = 'x'; } onemeg[1000000] = '\n'; f = fopen(bigpath, "w"); check(f != NULL, "bigline fopen w succeeds"); if (f == NULL) { return; } check(fwrite(onemeg, 1, sizeof(onemeg), f) == sizeof(onemeg), "bigline fwrite 1 MiB + newline"); check(fclose(f) == 0, "bigline fclose writer"); f = fopen(bigpath, "r"); check(f != NULL, "bigline fopen r succeeds"); line = NULL; n = 0; got = getline(&line, &n, f); check(got == 1000001, "getline on the 1 MiB line returns 1000001"); check(line != NULL && n >= 1000002, "getline grew the buffer past 1 MiB + NUL"); if (line != NULL) { int allx = 1; for (i = 0; i < 1000000; i++) { if (line[i] != 'x') { allx = 0; break; } } check(allx != 0, "the 1 MiB line content is intact"); check(line[1000000] == '\n', "the newline is kept at line[1000000]"); check(line[1000001] == '\0', "the line is NUL-terminated"); } got = getline(&line, &n, f); check(got == -1, "getline at end of file returns -1"); free(line); check(fclose(f) == 0, "bigline fclose reader"); check(remove(bigpath) == 0, "bigline remove"); /* Embedded NUL bytes are data, not terminators. */ f = write_file(nulpath, "ab\0cd\n", 6); if (f != NULL) { line = NULL; n = 0; got = getline(&line, &n, f); check(got == 6, "getline over \"ab\\0cd\\n\" returns 6 bytes"); check(line != NULL && line[0] == 'a' && line[1] == 'b' && line[2] == '\0' && line[3] == 'c' && line[4] == 'd' && line[5] == '\n' && line[6] == '\0', "embedded NUL survives and the line is still NUL-terminated"); free(line); check(fclose(f) == 0, "nul fclose"); check(remove(nulpath) == 0, "nul remove"); } /* A small preallocated buffer grows past its initial capacity. */ f = write_file(longpath, longline, sizeof(longline) - 1); if (f != NULL) { line = malloc(4); check(line != NULL, "small-buffer malloc"); n = 4; got = getline(&line, &n, f); check(got == (ssize_t)(sizeof(longline) - 1), "getline with a 4-byte buffer reads the whole long line"); check(n > sizeof(longline) - 1, "the capacity grew past the line length"); check(line != NULL && str_eq(line, longline), "grown small buffer holds the line"); free(line); check(fclose(f) == 0, "long fclose"); check(remove(longpath) == 0, "long remove"); } } /* 2: getdelim with a custom delimiter. */ static void delim_scenario(void) { const char path[] = "/tmp/vlibc-test-t18-delim.txt"; FILE *f; char *line; size_t n; ssize_t got; f = write_file(path, "a,b,c\n", 6); check(f != NULL, "delim file setup"); if (f == NULL) { return; } line = NULL; n = 0; got = getdelim(&line, &n, ',', f); check(got == 2 && line != NULL && str_eq(line, "a,"), "getdelim first field is \"a,\""); got = getdelim(&line, &n, ',', f); check(got == 2 && line != NULL && str_eq(line, "b,"), "getdelim second field is \"b,\""); got = getdelim(&line, &n, ',', f); check(got == 2 && line != NULL && str_eq(line, "c\n"), "getdelim returns the partial final field at end of file"); got = getdelim(&line, &n, ',', f); check(got == -1, "getdelim at end of file returns -1"); free(line); check(fclose(f) == 0, "delim fclose"); check(remove(path) == 0, "delim remove"); } /* 5/6: stream locking and the four _unlocked forms. */ static void lock_scenario(void) { const char rpath[] = "/tmp/vlibc-test-t18-lockr.txt"; const char wpath[] = "/tmp/vlibc-test-t18-lockw.txt"; char got[8]; FILE *f; int r0; int r1; f = fopen(rpath, "w+"); check(f != NULL, "lock fopen w+ succeeds"); if (f == NULL) { return; } r0 = ftrylockfile(f); check(r0 == 0, "ftrylockfile on a free stream returns 0"); flockfile(f); /* recursion depth 2 */ flockfile(f); /* recursion depth 3 */ r1 = ftrylockfile(f); check(r1 != 0, "ftrylockfile under the recursion returns nonzero"); funlockfile(f); funlockfile(f); funlockfile(f); r0 = ftrylockfile(f); check(r0 == 0, "ftrylockfile succeeds again after all unlocks"); funlockfile(f); check(fclose(f) == 0, "lock fclose"); f = write_file(rpath, "MNO", 3); check(f != NULL, "unlocked-read setup"); if (f != NULL) { flockfile(f); check(getc_unlocked(f) == 'M', "getc_unlocked reads 'M' under the lock"); check(getc_unlocked(f) == 'N', "getc_unlocked reads 'N' under the lock"); funlockfile(f); check(fgetc(f) == 'O', "the locked fgetc still reads 'O' after unlock"); check(fclose(f) == 0, "unlocked-read fclose"); check(remove(rpath) == 0, "unlocked-read remove"); } f = fopen(wpath, "w"); check(f != NULL, "unlocked-write setup"); if (f != NULL) { flockfile(f); check(putc_unlocked('Q', f) == 'Q', "putc_unlocked writes 'Q' under the lock"); check(putc_unlocked('R', f) == 'R', "putc_unlocked writes 'R' under the lock"); funlockfile(f); check(fclose(f) == 0, "unlocked-write fclose"); check(raw_read_all(wpath, got, sizeof(got)) == 2 && got[0] == 'Q' && got[1] == 'R', "putc_unlocked data reached the file"); check(remove(wpath) == 0, "unlocked-write remove"); } } /* 7: fmemopen over a fixed buffer, past-size EOF, NULL-buffer growth, 'a'. */ static void fmemopen_scenario(void) { char buf[64]; char out[64]; FILE *f; size_t r; int c; f = fmemopen(buf, sizeof(buf), "w+"); check(f != NULL, "fmemopen w+ succeeds"); if (f == NULL) { return; } check(fputs("hello fmemopen", f) >= 0, "fputs into the fixed buffer"); check(ftello(f) == 14, "ftello == 14 after the write"); check(fseek(f, 0, SEEK_SET) == 0, "rewind via fseek(0, SEEK_SET)"); r = fread(out, 1, sizeof(out), f); check(r == 14 && mem_eq((const unsigned char *)out, (const unsigned char *)"hello fmemopen", 14), "fmemopen write/rewind/read is byte-identical"); c = fgetc(f); check(c == EOF && feof(f), "fmemopen read past the data returns EOF"); check(fclose(f) == 0, "fmemopen w+ fclose"); /* Seek beyond the buffer size, then read: EOF, not an error. */ f = fmemopen(buf, sizeof(buf), "w+"); if (f != NULL) { check(fputs("xyz", f) >= 0, "fmemopen setup writes xyz"); check(fseek(f, 100, SEEK_SET) == 0, "fseek past the size succeeds"); c = fgetc(f); check(c == EOF && feof(f) && ferror(f) == 0, "reading past the size is EOF with no error"); check(ftello(f) == 100, "ftello stays at the seek target"); check(fclose(f) == 0, "fmemopen past-size fclose"); } /* 'a' positions and appends at the current data length. */ { char abuf[64] = "hello"; FILE *g = fmemopen(abuf, sizeof(abuf), "a"); check(g != NULL, "fmemopen a succeeds"); if (g != NULL) { check(ftello(g) == 5, "fmemopen a starts at the current length"); check(fputs("XY", g) >= 0, "fmemopen a appends"); check(fclose(g) == 0, "fmemopen a fclose flushes"); check(mem_eq((const unsigned char *)abuf, (const unsigned char *)"helloXY", 7), "the appended data landed in the caller buffer"); check(abuf[7] == '\0', "string-mode fmemopen NUL-terminates at the length"); } } } /* open_memstream: publish on flush, grow, NUL-terminate, free on close. */ static void memstream_scenario(void) { char *p = NULL; size_t z = 0; FILE *f; f = open_memstream(&p, &z); check(f != NULL && p != NULL && z == 0, "open_memstream returns a stream and an empty buffer"); if (f == NULL) { return; } check(fputs("hello", f) >= 0, "open_memstream write 1"); check(fflush(f) == 0, "open_memstream fflush 1"); check(z == 5, "*sizeloc is the length after fflush"); check(p != NULL && p[5] == '\0' && mem_eq((const unsigned char *)p, (const unsigned char *)"hello", 5), "the flushed buffer is NUL-terminated at the length"); check(fputs(" world", f) >= 0, "open_memstream write 2"); check(fflush(f) == 0, "open_memstream fflush 2"); check(z == 11 && p != NULL && p[11] == '\0', "the grown length and NUL are published after the second fflush"); check(fclose(f) == 0, "open_memstream fclose"); check(z == 11 && p != NULL && p[11] == '\0', "fclose keeps the NUL-terminated result"); check(mem_eq((const unsigned char *)p, (const unsigned char *)"hello world", 11), "the open_memstream content is correct"); free(p); } /* 9: no leaks across repeated memory-stream open/close cycles. */ static void memstream_leak_scenario(void) { const char *data = "0123456789abcdef0123456789abcdef0123456789abcdef"; size_t before; size_t after; int i; before = __vlibc_malloc_check(); check(before != (size_t)-1, "allocator heap walk is consistent before the cycles"); if (before == (size_t)-1) { return; } for (i = 0; i < 100; i++) { FILE *g; char *p = NULL; size_t z = 0; g = fmemopen(NULL, 8, "w"); if (g == NULL || fputs(data, g) == EOF || fclose(g) != 0) { say(2, "FAIL: fmemopen cycle failed\n"); failures++; return; } g = open_memstream(&p, &z); if (g == NULL || fputs(data, g) == EOF || fclose(g) != 0) { say(2, "FAIL: open_memstream cycle failed\n"); failures++; return; } free(p); } after = __vlibc_malloc_check(); check(after == before, "fclose releases memory-stream FILEs and owned buffers (no leak over 100 cycles)"); } /* puts, putchar_unlocked, getchar_unlocked over redirected std streams. */ static void stream_redirect_scenario(void) { const char outpath[] = "/tmp/vlibc-test-t18-stdout.txt"; const char inpath[] = "/tmp/vlibc-test-t18-stdin.txt"; char buf[32]; long saved_out; long saved_in; long got; int r1; int r2; int r3; int r4; int c; /* stdout: no check() output on fd 1 while it names the file. */ saved_out = __syscall1(SYS_dup, 1); check(saved_out >= 0, "dup(1) saves the stdout descriptor"); r1 = (freopen(outpath, "w", stdout) == stdout); r2 = (puts("AA") >= 0); r3 = (putchar_unlocked('B') == 'B'); r4 = (fflush(stdout) == 0); __syscall2(SYS_dup2, saved_out, 1); __syscall1(SYS_close, saved_out); check(r1, "freopen stdout to a file"); check(r2, "puts writes through stdout"); check(r3, "putchar_unlocked writes through stdout"); check(r4, "fflush(stdout) after the freopen"); got = raw_read_all(outpath, buf, sizeof(buf)); check(got == 4 && buf[0] == 'A' && buf[1] == 'A' && buf[2] == '\n' && buf[3] == 'B', "puts + putchar_unlocked content reached the file as \"AA\\nB\""); check(remove(outpath) == 0, "remove the stdout file"); /* stdin: seed a file, rebind stdin, read a char, restore. */ { FILE *seed = write_file(inpath, "K", 1); check(seed != NULL, "stdin seed file written"); if (seed != NULL) { (void)fclose(seed); } saved_in = __syscall1(SYS_dup, 0); check(saved_in >= 0, "dup(0) saves the stdin descriptor"); r1 = (freopen(inpath, "r", stdin) == stdin); c = getchar_unlocked(); r2 = (c == 'K'); __syscall2(SYS_dup2, saved_in, 0); __syscall1(SYS_close, saved_in); check(r1, "freopen stdin from the seed file"); check(r2, "getchar_unlocked reads the seed character"); check(remove(inpath) == 0, "remove the stdin file"); } } /* Failure scenarios (-f). */ static int failure_scenarios(void) { const char path[] = "/tmp/vlibc-test-t18-fail.txt"; FILE *f; char *line = NULL; size_t n = 0; ssize_t got; f = fopen(path, "w+"); if (f == NULL) { say(2, "FAIL: -f setup fopen failed\n"); failures++; return 1; } check(fputs("data-data-data", f) >= 0, "-f setup fputs"); check(fflush(f) == 0, "-f setup fflush"); (void)__syscall1(SYS_close, fileno(f)); /* close the fd underneath */ got = getline(&line, &n, f); check(got == -1, "getline on a closed-underneath stream returns -1"); free(line); (void)fclose(f); check(remove(path) == 0, "-f remove"); return failures > 0 ? 1 : 0; } // NOLINTEND(clang-analyzer-unix.Stream) int main(int argc, char **argv) { if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f') { int rc = failure_scenarios(); __syscall1(SYS_exit_group, rc); return rc; /* not reached */ } line_scenario(); delim_scenario(); lock_scenario(); fmemopen_scenario(); memstream_scenario(); memstream_leak_scenario(); stream_redirect_scenario(); if (failures > 0) { say(2, "FAILED ("); say_dec(2, (unsigned long)failures); say(2, " check(s))\n"); return 1; } say(1, "all getline/locking/memstream tests passed\n"); return 0; }