fix(stdio): correct position tracking on mode switch and free owned buffers on close
This commit is contained in:
+7
-1
@@ -206,7 +206,13 @@ fseeko(FILE *stream, off_t offset, int whence);
|
|||||||
long
|
long
|
||||||
ftell(FILE *stream);
|
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);
|
ftello(FILE *stream);
|
||||||
|
|
||||||
/* Rewind to the start and clear feof/ferror (equivalent to
|
/* Rewind to the start and clear feof/ferror (equivalent to
|
||||||
|
|||||||
+40
-4
@@ -278,6 +278,10 @@ stdio_alloc_file(int fd, int m)
|
|||||||
f->wstop = f->buf + BUFSIZ;
|
f->wstop = f->buf + BUFSIZ;
|
||||||
f->fd = fd;
|
f->fd = fd;
|
||||||
f->flags = m | F_OWNBUF | F_HEAP;
|
f->flags = m | F_OWNBUF | F_HEAP;
|
||||||
|
if ((m & F_READ) && (m & F_WRITE))
|
||||||
|
{
|
||||||
|
f->flags |= F_RDWR;
|
||||||
|
}
|
||||||
f->pos = 0;
|
f->pos = 0;
|
||||||
f->ungot = 0;
|
f->ungot = 0;
|
||||||
f->next = open_list;
|
f->next = open_list;
|
||||||
@@ -320,6 +324,10 @@ stdio_discard_read(FILE *f)
|
|||||||
f->flags |= F_ERR;
|
f->flags |= F_ERR;
|
||||||
return -1;
|
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;
|
f->rpos = f->rstop = f->buf;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -462,7 +470,11 @@ freopen(const char *restrict path, const char *restrict mode, FILE *restrict str
|
|||||||
if (path == NULL)
|
if (path == NULL)
|
||||||
{
|
{
|
||||||
/* Change the mode only; keep the descriptor. */
|
/* 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);
|
stream->flags &= ~(F_EOF | F_ERR | F_PUSHED);
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
@@ -483,7 +495,12 @@ freopen(const char *restrict path, const char *restrict mode, FILE *restrict str
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
stream->fd = newfd;
|
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->rpos = stream->rstop = stream->buf;
|
||||||
stream->wpos = stream->buf;
|
stream->wpos = stream->buf;
|
||||||
stream->pos = 0;
|
stream->pos = 0;
|
||||||
@@ -522,6 +539,16 @@ fclose(FILE *stream)
|
|||||||
{
|
{
|
||||||
*link = stream->next;
|
*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);
|
__libc_free(stream);
|
||||||
}
|
}
|
||||||
return rc;
|
return rc;
|
||||||
@@ -675,14 +702,16 @@ int
|
|||||||
fputc(int c, FILE *stream)
|
fputc(int c, FILE *stream)
|
||||||
{
|
{
|
||||||
stdio_init_if_needed(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;
|
stream->flags |= F_ERR;
|
||||||
return EOF;
|
return EOF;
|
||||||
}
|
}
|
||||||
if (stream->rpos < stream->rstop)
|
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)
|
if (stdio_discard_read(stream) < 0)
|
||||||
{
|
{
|
||||||
return EOF;
|
return EOF;
|
||||||
@@ -808,6 +837,13 @@ fgets(char *restrict s, int n, FILE *restrict stream)
|
|||||||
{
|
{
|
||||||
return NULL;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
s[i] = (char)c;
|
s[i] = (char)c;
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
*
|
*
|
||||||
* Mode switching: reading while write data is pending flushes it first;
|
* Mode switching: reading while write data is pending flushes it first;
|
||||||
* writing while unread buffered data is pending seeks the kernel back to
|
* writing while unread buffered data is pending seeks the kernel back to
|
||||||
* the logical position (discarding the unread data). F_PUSHED marks the
|
* the logical position (discarding the unread data). F_WRITE doubles as
|
||||||
* single guaranteed ungetc pushback byte in `ungot`; a successful seek
|
* the current-mode marker and is cleared by the read switch; F_RDWR
|
||||||
* discards it.
|
* 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
|
* The helper prototypes below are shared with the formatted-I/O todos
|
||||||
* (16: vfprintf, 17: vfscanf) so they operate on the same buffer state.
|
* (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_OWNBUF 0x080 /* buf was allocated by the library */
|
||||||
#define F_HEAP 0x100 /* the FILE struct itself is heap-allocated */
|
#define F_HEAP 0x100 /* the FILE struct itself is heap-allocated */
|
||||||
#define F_PUSHED 0x200 /* ungot holds a pushed-back byte */
|
#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. */
|
/* Flush pending write data; returns 0, or -1 with F_ERR set. */
|
||||||
hidden int
|
hidden int
|
||||||
|
|||||||
@@ -25,6 +25,11 @@
|
|||||||
* changes the mode only.
|
* changes the mode only.
|
||||||
* 9. remove/rename lifecycle; tmpfile write+rewind+read.
|
* 9. remove/rename lifecycle; tmpfile write+rewind+read.
|
||||||
* 10. Level 2: tmpnam/ctermid/setbuffer/setlinebuf/fopen64.
|
* 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
|
* 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)
|
* and no crash; fopen of a nonexistent path returns NULL; fdopen(-1)
|
||||||
@@ -50,6 +55,11 @@
|
|||||||
|
|
||||||
#include "../src/internal/syscall.h"
|
#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;
|
static int failures;
|
||||||
|
|
||||||
/* Write a NUL-terminated string to fd via the raw syscall layer. */
|
/* 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)
|
#if VLIBC_LEVEL_GE(2)
|
||||||
|
|
||||||
/* 10. Level 2 additions. */
|
/* 10. Level 2 additions. */
|
||||||
@@ -702,6 +784,8 @@ main(int argc, char **argv)
|
|||||||
fdopen_scenario();
|
fdopen_scenario();
|
||||||
freopen_scenario();
|
freopen_scenario();
|
||||||
name_scenario();
|
name_scenario();
|
||||||
|
modeswitch_scenario();
|
||||||
|
fclose_leak_scenario();
|
||||||
#if VLIBC_LEVEL_GE(2)
|
#if VLIBC_LEVEL_GE(2)
|
||||||
level2_scenario();
|
level2_scenario();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user