diff --git a/include/stdio.h b/include/stdio.h index 8472133..ec6b1f0 100644 --- a/include/stdio.h +++ b/include/stdio.h @@ -206,7 +206,13 @@ fseeko(FILE *stream, off_t offset, int whence); long ftell(FILE *stream); -__attribute__((pure)) off_t +/* + * Not marked pure: the implementation can allocate the stream buffer on + * first use (an observable side effect), so the compiler must not elide + * or reorder the call. (feof/ferror/fileno below are genuinely read-only + * and keep their pure attribute.) + */ +off_t ftello(FILE *stream); /* Rewind to the start and clear feof/ferror (equivalent to diff --git a/src/stdio/stdio.c b/src/stdio/stdio.c index 07a0dc5..f082f3f 100644 --- a/src/stdio/stdio.c +++ b/src/stdio/stdio.c @@ -278,6 +278,10 @@ stdio_alloc_file(int fd, int m) f->wstop = f->buf + BUFSIZ; f->fd = fd; f->flags = m | F_OWNBUF | F_HEAP; + if ((m & F_READ) && (m & F_WRITE)) + { + f->flags |= F_RDWR; + } f->pos = 0; f->ungot = 0; f->next = open_list; @@ -320,6 +324,10 @@ stdio_discard_read(FILE *f) f->flags |= F_ERR; return -1; } + /* The kernel now sits at the logical position; re-anchor `pos` + * there so the invariant holds after the buffer is emptied + * below (empty buffer => kernel == pos). */ + f->pos += (off_t)(f->rpos - f->buf); } f->rpos = f->rstop = f->buf; return 0; @@ -462,7 +470,11 @@ freopen(const char *restrict path, const char *restrict mode, FILE *restrict str if (path == NULL) { /* Change the mode only; keep the descriptor. */ - stream->flags = (stream->flags & ~(F_READ | F_WRITE | F_APPEND)) | m; + stream->flags = (stream->flags & ~(F_READ | F_WRITE | F_APPEND | F_RDWR)) | m; + if ((m & F_READ) && (m & F_WRITE)) + { + stream->flags |= F_RDWR; + } stream->flags &= ~(F_EOF | F_ERR | F_PUSHED); return stream; } @@ -483,7 +495,12 @@ freopen(const char *restrict path, const char *restrict mode, FILE *restrict str return NULL; } stream->fd = newfd; - stream->flags = (stream->flags & ~(F_READ | F_WRITE | F_APPEND | F_EOF | F_ERR | F_PUSHED)) | m; + stream->flags = + (stream->flags & ~(F_READ | F_WRITE | F_APPEND | F_EOF | F_ERR | F_PUSHED | F_RDWR)) | m; + if ((m & F_READ) && (m & F_WRITE)) + { + stream->flags |= F_RDWR; + } stream->rpos = stream->rstop = stream->buf; stream->wpos = stream->buf; stream->pos = 0; @@ -522,6 +539,16 @@ fclose(FILE *stream) { *link = stream->next; } + if (stream->flags & F_OWNBUF) + { + /* The FILE struct is heap-owned, so its library-allocated + * buffer is too (a user buffer via setvbuf clears F_OWNBUF + * and must not be freed). The static std streams are not + * F_HEAP, so their lazily allocated buffers are + * intentionally never freed: process-lifetime storage, + * matching their registry/global nature. */ + __libc_free(stream->buf); + } __libc_free(stream); } return rc; @@ -675,14 +702,16 @@ int fputc(int c, FILE *stream) { stdio_init_if_needed(stream); - if (!(stream->flags & F_WRITE)) + if (!(stream->flags & F_WRITE) && !(stream->flags & F_RDWR)) { + /* The open mode does not allow writes. */ stream->flags |= F_ERR; return EOF; } if (stream->rpos < stream->rstop) { - /* Pending read data: discard it before writing. */ + /* Pending read data: discard it before writing (the read -> write + * mode switch on an update stream). */ if (stdio_discard_read(stream) < 0) { return EOF; @@ -808,6 +837,13 @@ fgets(char *restrict s, int n, FILE *restrict stream) { return NULL; } + /* Mid-line end of input: a genuine EOF keeps the partial + * line, but a read error makes the array contents + * indeterminate and fgets must return NULL (C23 7.23.7.2). */ + if (stream->flags & F_ERR) + { + return NULL; + } break; } s[i] = (char)c; diff --git a/src/stdio/stdio_impl.h b/src/stdio/stdio_impl.h index 08cb8c4..94a1fbc 100644 --- a/src/stdio/stdio_impl.h +++ b/src/stdio/stdio_impl.h @@ -17,9 +17,11 @@ * * Mode switching: reading while write data is pending flushes it first; * writing while unread buffered data is pending seeks the kernel back to - * the logical position (discarding the unread data). F_PUSHED marks the - * single guaranteed ungetc pushback byte in `ungot`; a successful seek - * discards it. + * the logical position (discarding the unread data). F_WRITE doubles as + * the current-mode marker and is cleared by the read switch; F_RDWR + * records the write capability of update streams so the write switch can + * re-set F_WRITE. F_PUSHED marks the single guaranteed ungetc pushback + * byte in `ungot`; a successful seek discards it. * * The helper prototypes below are shared with the formatted-I/O todos * (16: vfprintf, 17: vfscanf) so they operate on the same buffer state. @@ -55,6 +57,7 @@ struct vlibc_FILE #define F_OWNBUF 0x080 /* buf was allocated by the library */ #define F_HEAP 0x100 /* the FILE struct itself is heap-allocated */ #define F_PUSHED 0x200 /* ungot holds a pushed-back byte */ +#define F_RDWR 0x400 /* update stream: reads and writes both enabled */ /* Flush pending write data; returns 0, or -1 with F_ERR set. */ hidden int diff --git a/tests/test_stdio.c b/tests/test_stdio.c index 77fad55..d49e122 100644 --- a/tests/test_stdio.c +++ b/tests/test_stdio.c @@ -25,6 +25,11 @@ * changes the mode only. * 9. remove/rename lifecycle; tmpfile write+rewind+read. * 10. Level 2: tmpnam/ctermid/setbuffer/setlinebuf/fopen64. + * 11. Read -> write mode switch: after a partial read the logical + * position survives the buffered-data discard, so an overwrite + * lands at the right offset. + * 12. fclose leak check: 200 open/write/close cycles leave no live + * allocations (probed with the allocator's heap walk). * * Failure mode (-f): fseek past EOF then fread returns 0 with feof set * and no crash; fopen of a nonexistent path returns NULL; fdopen(-1) @@ -50,6 +55,11 @@ #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. */ @@ -586,6 +596,78 @@ name_scenario(void) } } +/* 11. Read -> write mode switch keeps the logical position. */ +static void +modeswitch_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-modeswitch.bin"; + char got[16]; + FILE *f; + unsigned long r; + + f = fopen(path, "w+b"); + check(f != NULL, "modeswitch fopen w+b succeeds"); + if (f == NULL) + { + return; + } + check(fwrite("abcdefgh", 1, 8, f) == 8, "modeswitch writes the 8-byte seed"); + check(fflush(f) == 0, "modeswitch fflush returns 0"); + check(fseek(f, 0, SEEK_SET) == 0, "modeswitch seeks back to the start"); + check(fgetc(f) == 'a', "modeswitch reads 'a'"); + check(fgetc(f) == 'b', "modeswitch reads 'b' (6 bytes left buffered)"); + check(ftello(f) == 2, "modeswitch ftello == 2 after the two reads"); + check(fputc('X', f) == 'X', "modeswitch fputc('X') switches back to writing"); + check(ftello(f) == 3, "modeswitch ftello == 3 after the overwrite"); + check(fseeko(f, 0, SEEK_CUR) == 0, "modeswitch fseeko(0, SEEK_CUR) returns 0"); + check(ftello(f) == 3, "modeswitch ftello stays 3 after the no-op seek"); + check(fclose(f) == 0, "modeswitch fclose returns 0"); + + f = fopen(path, "rb"); + check(f != NULL, "modeswitch reopen rb succeeds"); + if (f != NULL) + { + r = fread(got, 1, 8, f); + check(r == 8 && mem_eq((const unsigned char *)got, (const unsigned char *)"abXdefgh", 8), + "modeswitch: the overwrite landed at offset 2 (\"abXdefgh\")"); + check(fclose(f) == 0, "modeswitch fclose after the verify returns 0"); + } + check(remove(path) == 0, "remove deletes the modeswitch file"); +} + +/* 12. fclose releases the owned buffer (no leak across many cycles). */ +static void +fclose_leak_scenario(void) +{ + const char path[] = "/tmp/vlibc-test-stdio-leak.txt"; + 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 < 200; i++) + { + FILE *f = fopen(path, "w"); + + if (f == NULL || fputc('x', f) == EOF || fclose(f) != 0) + { + (void)fclose(f); /* safe when f == NULL */ + say(2, "FAIL: leak-cycle fopen/fputc/fclose\n"); + failures++; + return; + } + } + after = __vlibc_malloc_check(); + check(after == before, + "fclose frees the FILE struct and its owned buffer (no leak over 200 cycles)"); + check(remove(path) == 0, "remove deletes the leak-cycle file"); +} + #if VLIBC_LEVEL_GE(2) /* 10. Level 2 additions. */ @@ -702,6 +784,8 @@ main(int argc, char **argv) fdopen_scenario(); freopen_scenario(); name_scenario(); + modeswitch_scenario(); + fclose_leak_scenario(); #if VLIBC_LEVEL_GE(2) level2_scenario(); #endif