git: 6a0891c6ebb7 - stable/12 - fflush: Add test for buffer handling in __sflush

From: Ed Maste <emaste_at_FreeBSD.org>
Date: Tue, 07 Nov 2023 14:30:24 UTC
The branch stable/12 has been updated by emaste:

URL: https://cgit.FreeBSD.org/src/commit/?id=6a0891c6ebb78bd59a3fc440675e31b6982de896

commit 6a0891c6ebb78bd59a3fc440675e31b6982de896
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2023-08-03 15:13:45 +0000
Commit:     Ed Maste <emaste@FreeBSD.org>
CommitDate: 2023-11-07 14:30:07 +0000

    fflush: Add test for buffer handling in __sflush
    
    Sponsored by:   Klara, Inc.
    
    (cherry picked from commit b8dbfb0a6c181a9aeab0b793deb0813d06052df9)
    (cherry picked from commit ba490dfc95e0941216420a2003757b3c4b5b2ec2)
    (cherry picked from commit 66d84f1362f63c23f6f451490e153e0703a7cda6)
    
    Approved by:    so
---
 lib/libc/tests/stdio/Makefile         |   1 +
 lib/libc/tests/stdio/flushlbuf_test.c | 155 ++++++++++++++++++++++++++++++++++
 2 files changed, 156 insertions(+)

diff --git a/lib/libc/tests/stdio/Makefile b/lib/libc/tests/stdio/Makefile
index 248a05bc45e7..5280fff8bfb4 100644
--- a/lib/libc/tests/stdio/Makefile
+++ b/lib/libc/tests/stdio/Makefile
@@ -3,6 +3,7 @@
 .include <bsd.own.mk>
 
 ATF_TESTS_C+=		fdopen_test
+ATF_TESTS_C+=		flushlbuf_test
 ATF_TESTS_C+=		fmemopen2_test
 ATF_TESTS_C+=		fopen2_test
 ATF_TESTS_C+=		freopen_test
diff --git a/lib/libc/tests/stdio/flushlbuf_test.c b/lib/libc/tests/stdio/flushlbuf_test.c
new file mode 100644
index 000000000000..11d9ea4ecc6c
--- /dev/null
+++ b/lib/libc/tests/stdio/flushlbuf_test.c
@@ -0,0 +1,155 @@
+/*-
+ * Copyright (c) 2023 Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include <atf-c.h>
+
+#define BUFSIZE 16
+
+static const char seq[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "abcdefghijklmnopqrstuvwxyz"
+    "0123456789+/";
+
+struct stream {
+	char buf[BUFSIZE];
+	unsigned int len;
+	unsigned int pos;
+};
+
+static int writefn(void *cookie, const char *buf, int len)
+{
+	struct stream *s = cookie;
+	int written = 0;
+
+	if (len <= 0)
+		return 0;
+	while (len > 0 && s->pos < s->len) {
+		s->buf[s->pos++] = *buf++;
+		written++;
+		len--;
+	}
+	if (written > 0)
+		return written;
+	errno = EAGAIN;
+	return -1;
+}
+
+ATF_TC_WITHOUT_HEAD(flushlbuf_partial);
+ATF_TC_BODY(flushlbuf_partial, tc)
+{
+	static struct stream s;
+	static char buf[BUFSIZE + 1];
+	FILE *f;
+	unsigned int i = 0;
+	int ret = 0;
+
+	/*
+	 * Create the stream and its buffer, print just enough characters
+	 * to the stream to fill the buffer without triggering a flush,
+	 * then check the state.
+	 */
+	s.len = BUFSIZE / 2; // write will fail after this amount
+	ATF_REQUIRE((f = fwopen(&s, writefn)) != NULL);
+	ATF_REQUIRE(setvbuf(f, buf, _IOLBF, BUFSIZE) == 0);
+	while (i < BUFSIZE)
+		if ((ret = fprintf(f, "%c", seq[i++])) < 0)
+			break;
+	ATF_CHECK_EQ(BUFSIZE, i);
+	ATF_CHECK_EQ(seq[i - 1], buf[BUFSIZE - 1]);
+	ATF_CHECK_EQ(1, ret);
+	ATF_CHECK_EQ(0, s.pos);
+
+	/*
+	 * At this point, the buffer is full but writefn() has not yet
+	 * been called.  The next fprintf() call will trigger a preemptive
+	 * fflush(), and writefn() will consume s.len characters before
+	 * returning EAGAIN, causing fprintf() to fail without having
+	 * written anything (which is why we don't increment i here).
+	 */
+	ret = fprintf(f, "%c", seq[i]);
+	ATF_CHECK_ERRNO(EAGAIN, ret < 0);
+	ATF_CHECK_EQ(s.len, s.pos);
+
+	/*
+	 * We have consumed s.len characters from the buffer, so continue
+	 * printing until it is full again and check that no overflow has
+	 * occurred yet.
+	 */
+	while (i < BUFSIZE + s.len)
+		fprintf(f, "%c", seq[i++]);
+	ATF_CHECK_EQ(BUFSIZE + s.len, i);
+	ATF_CHECK_EQ(seq[i - 1], buf[BUFSIZE - 1]);
+	ATF_CHECK_EQ(0, buf[BUFSIZE]);
+
+	/*
+	 * The straw that breaks the camel's back: libc fails to recognize
+	 * that the buffer is full and continues to write beyond its end.
+	 */
+	fprintf(f, "%c", seq[i++]);
+	ATF_CHECK_EQ(0, buf[BUFSIZE]);
+}
+
+ATF_TC_WITHOUT_HEAD(flushlbuf_full);
+ATF_TC_BODY(flushlbuf_full, tc)
+{
+	static struct stream s;
+	static char buf[BUFSIZE];
+	FILE *f;
+	unsigned int i = 0;
+	int ret = 0;
+
+	/*
+	 * Create the stream and its buffer, print just enough characters
+	 * to the stream to fill the buffer without triggering a flush,
+	 * then check the state.
+	 */
+	s.len = 0; // any attempt to write will fail
+	ATF_REQUIRE((f = fwopen(&s, writefn)) != NULL);
+	ATF_REQUIRE(setvbuf(f, buf, _IOLBF, BUFSIZE) == 0);
+	while (i < BUFSIZE)
+		if ((ret = fprintf(f, "%c", seq[i++])) < 0)
+			break;
+	ATF_CHECK_EQ(BUFSIZE, i);
+	ATF_CHECK_EQ(seq[i - 1], buf[BUFSIZE - 1]);
+	ATF_CHECK_EQ(1, ret);
+	ATF_CHECK_EQ(0, s.pos);
+
+	/*
+	 * At this point, the buffer is full but writefn() has not yet
+	 * been called.  The next fprintf() call will trigger a preemptive
+	 * fflush(), and writefn() will immediately return EAGAIN, causing
+	 * fprintf() to fail without having written anything (which is why
+	 * we don't increment i here).
+	 */
+	ret = fprintf(f, "%c", seq[i]);
+	ATF_CHECK_ERRNO(EAGAIN, ret < 0);
+	ATF_CHECK_EQ(s.len, s.pos);
+
+	/*
+	 * Now make our stream writeable.
+	 */
+	s.len = sizeof(s.buf);
+
+	/*
+	 * Flush the stream again.  The data we failed to write previously
+	 * should still be in the buffer and will now be written to the
+	 * stream.
+	 */
+	ATF_CHECK_EQ(0, fflush(f));
+	ATF_CHECK_EQ(seq[0], s.buf[0]);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+	ATF_TP_ADD_TC(tp, flushlbuf_partial);
+	ATF_TP_ADD_TC(tp, flushlbuf_full);
+
+	return (atf_no_error());
+}