[libc-commits] [libc] [libc] Fix EOF handling in fprintf. (PR #211982)
Alexey Samsonov via libc-commits
libc-commits at lists.llvm.org
Fri Jul 24 20:33:52 PDT 2026
https://github.com/vonosmas created https://github.com/llvm/llvm-project/pull/211982
`fscanf` family of functions should return EOF if:
- end of input is reached before either the first successful conversion or a matching failure occurs;
- a read error occurs.
This wasn't handled correctly before - in "system FILE" mode (in overlay build) `EOF` return value from `getc` was passed through to parser, violating `Reader` interface, which triggered infinite loops on "while-not-EOF" `fscanf` loops. In llvm-libc-FILE mode `fscanf` simply returned zero instead of EOF, because the code only checked error indicator on a stream.
This PR removes _any_ lookups of eof/error indicators on an input stream - instead we can simply rely on the `getc` (system or internal) function behavior, which returns `EOF` on unsuccessful reads.
Extend the test cases for `fprintf` to cover various return values in cases when `EOF` is reached (before or after conversions / matching errors).
>From 2aea581dbc4c0ca1d1eff6a4be7deae4546472d8 Mon Sep 17 00:00:00 2001
From: Alexey Samsonov <vonosmas at gmail.com>
Date: Sat, 25 Jul 2026 03:19:21 +0000
Subject: [PATCH] [libc] Fix EOF handling in fprintf.
---
libc/src/stdio/scanf_core/CMakeLists.txt | 1 -
libc/src/stdio/scanf_core/vfscanf_internal.h | 26 ++--
libc/test/src/stdio/CMakeLists.txt | 2 +
libc/test/src/stdio/fscanf_test.cpp | 153 ++++++++++++++++++-
4 files changed, 163 insertions(+), 19 deletions(-)
diff --git a/libc/src/stdio/scanf_core/CMakeLists.txt b/libc/src/stdio/scanf_core/CMakeLists.txt
index 35578f899c2eb..78d0354365102 100644
--- a/libc/src/stdio/scanf_core/CMakeLists.txt
+++ b/libc/src/stdio/scanf_core/CMakeLists.txt
@@ -14,7 +14,6 @@ if(LIBC_TARGET_OS_IS_GPU)
list(APPEND file_deps
libc.src.stdio.getc
libc.src.stdio.ungetc
- libc.src.stdio.ferror
)
elseif(LLVM_LIBC_FULL_BUILD)
list(APPEND file_deps
diff --git a/libc/src/stdio/scanf_core/vfscanf_internal.h b/libc/src/stdio/scanf_core/vfscanf_internal.h
index eabfbd46a9e0a..30b32c1dff8b5 100644
--- a/libc/src/stdio/scanf_core/vfscanf_internal.h
+++ b/libc/src/stdio/scanf_core/vfscanf_internal.h
@@ -17,7 +17,6 @@
#include "src/stdio/scanf_core/scanf_main.h"
#if defined(LIBC_TARGET_ARCH_IS_GPU)
-#include "src/stdio/ferror.h"
#include "src/stdio/getc.h"
#include "src/stdio/ungetc.h"
#endif
@@ -38,8 +37,6 @@ LIBC_INLINE void flockfile(::FILE *) { return; }
LIBC_INLINE void funlockfile(::FILE *) { return; }
-LIBC_INLINE int ferror_unlocked(::FILE *f) { return LIBC_NAMESPACE::ferror(f); }
-
LIBC_INLINE int getc(::FILE *f) { return LIBC_NAMESPACE::getc(f); }
LIBC_INLINE void ungetc(int c, ::FILE *f) { LIBC_NAMESPACE::ungetc(c, f); }
@@ -54,17 +51,13 @@ LIBC_INLINE void funlockfile(FILE *f) {
reinterpret_cast<LIBC_NAMESPACE::File *>(f)->unlock();
}
-LIBC_INLINE int ferror_unlocked(FILE *f) {
- return reinterpret_cast<LIBC_NAMESPACE::File *>(f)->error_unlocked();
-}
-
LIBC_INLINE int getc(FILE *f) {
unsigned char c;
auto result =
reinterpret_cast<LIBC_NAMESPACE::File *>(f)->read_unlocked(&c, 1);
size_t r = result.value;
if (result.has_error() || r != 1)
- return '\0';
+ return EOF;
return c;
}
@@ -81,8 +74,6 @@ LIBC_INLINE void flockfile(::FILE *) { return; }
LIBC_INLINE void funlockfile(::FILE *) { return; }
-LIBC_INLINE int ferror_unlocked(::FILE *f) { return ::ferror(f); }
-
LIBC_INLINE int getc(::FILE *f) { return ::getc(f); }
LIBC_INLINE void ungetc(int c, ::FILE *f) { ::ungetc(c, f); }
@@ -95,16 +86,25 @@ namespace scanf_core {
class StreamReader : public Reader<StreamReader> {
::FILE *stream;
+ bool eof = false;
public:
LIBC_INLINE StreamReader(::FILE *stream) : stream(stream) {}
LIBC_INLINE char getc() {
- return static_cast<char>(internal::getc(static_cast<FILE *>(stream)));
+ int c = internal::getc(stream);
+ if (c == EOF) {
+ eof = true;
+ return '\0';
+ }
+ return static_cast<char>(c);
}
LIBC_INLINE void ungetc(int c) {
- internal::ungetc(c, static_cast<FILE *>(stream));
+ // Don't ungetc the EOF to prevent clearing EOF indicator on stream.
+ if (!eof)
+ internal::ungetc(c, stream);
}
+ LIBC_INLINE bool reached_eof() const { return eof; }
};
LIBC_INLINE int vfscanf_internal(::FILE *__restrict stream,
@@ -113,7 +113,7 @@ LIBC_INLINE int vfscanf_internal(::FILE *__restrict stream,
internal::flockfile(stream);
scanf_core::StreamReader reader(stream);
int retval = scanf_core::scanf_main(&reader, format, args);
- if (retval == 0 && internal::ferror_unlocked(stream))
+ if (retval == 0 && reader.reached_eof())
retval = EOF;
internal::funlockfile(stream);
diff --git a/libc/test/src/stdio/CMakeLists.txt b/libc/test/src/stdio/CMakeLists.txt
index e0e5656f24db3..9f310c0cd66b2 100644
--- a/libc/test/src/stdio/CMakeLists.txt
+++ b/libc/test/src/stdio/CMakeLists.txt
@@ -309,6 +309,7 @@ if(LLVM_LIBC_FULL_BUILD)
# In fullbuild mode, fscanf's tests use the internal FILE for other functions.
list(APPEND fscanf_test_deps
libc.src.stdio.fclose
+ libc.src.stdio.feof
libc.src.stdio.ferror
libc.src.stdio.fopen
libc.src.stdio.fwrite
@@ -325,6 +326,7 @@ if(NOT LIBC_TARGET_ARCHITECTURE_IS_AMDGPU)
SRCS
fscanf_test.cpp
DEPENDS
+ libc.hdr.stdio_macros
libc.src.stdio.fscanf
${fscanf_test_deps}
libc.src.__support.CPP.string_view
diff --git a/libc/test/src/stdio/fscanf_test.cpp b/libc/test/src/stdio/fscanf_test.cpp
index 451ff94055ea5..96f0af53d7e50 100644
--- a/libc/test/src/stdio/fscanf_test.cpp
+++ b/libc/test/src/stdio/fscanf_test.cpp
@@ -6,27 +6,29 @@
//
//===----------------------------------------------------------------------===//
+#include "hdr/stdio_macros.h"
#include "src/__support/CPP/string_view.h"
+#include "src/stdio/fscanf.h"
+#include "test/UnitTest/Test.h"
#ifndef LIBC_COPT_STDIO_USE_SYSTEM_FILE
#include "src/stdio/fclose.h"
+#include "src/stdio/feof.h"
#include "src/stdio/ferror.h"
#include "src/stdio/fopen.h"
#include "src/stdio/fwrite.h"
#endif // LIBC_COPT_STDIO_USE_SYSTEM_FILE
-#include "src/stdio/fscanf.h"
-
-#include "test/UnitTest/Test.h"
-
namespace scanf_test {
#ifndef LIBC_COPT_STDIO_USE_SYSTEM_FILE
using LIBC_NAMESPACE::fclose;
+using LIBC_NAMESPACE::feof;
using LIBC_NAMESPACE::ferror;
using LIBC_NAMESPACE::fopen;
using LIBC_NAMESPACE::fwrite;
#else // defined(LIBC_COPT_STDIO_USE_SYSTEM_FILE)
using ::fclose;
+using ::feof;
using ::ferror;
using ::fopen;
using ::fwrite;
@@ -59,7 +61,8 @@ TEST(LlvmLibcFScanfTest, WriteToFile) {
read = LIBC_NAMESPACE::fscanf(file,
"Reading from a write-only file should fail.");
- EXPECT_LT(read, 0);
+ EXPECT_NE(scanf_test::ferror(file), 0);
+ EXPECT_EQ(read, EOF);
ASSERT_EQ(0, scanf_test::fclose(file));
@@ -87,3 +90,143 @@ TEST(LlvmLibcFScanfTest, WriteToFile) {
ASSERT_EQ(scanf_test::ferror(file), 0);
ASSERT_EQ(scanf_test::fclose(file), 0);
}
+
+TEST(LlvmLibcFScanfTest, ProcNetIfInet6Sample) {
+ const char *FILENAME = APPEND_LIBC_TEST("proc_net_if_inet6.txt");
+ auto FILE_PATH = libc_make_test_file_path(FILENAME);
+ ::FILE *file = scanf_test::fopen(FILE_PATH, "w");
+ ASSERT_FALSE(file == nullptr);
+
+ // Sample contents of /proc/net/if_inet6 on Linux.
+ constexpr char sample_contents[] =
+ "fe80000000000000adaf264669baa4c7 02 40 20 80 ens4\n"
+ "00000000000000000000000000000001 01 80 10 80 lo\n";
+ constexpr char entry_format[] =
+ "%4s%4s%4s%4s%4s%4s%4s%4s %08x %02x %02x %02x %20s\n";
+
+ ASSERT_EQ(sizeof(sample_contents) - 1,
+ scanf_test::fwrite(sample_contents, 1, sizeof(sample_contents) - 1,
+ file));
+ ASSERT_EQ(0, scanf_test::fclose(file));
+
+ file = scanf_test::fopen(FILE_PATH, "r");
+ ASSERT_FALSE(file == nullptr);
+
+ char addr6p[8][5];
+ int if_idx, prefix, scope, dad_status;
+ char devname[21];
+
+ // Validate first entry.
+ int ret = LIBC_NAMESPACE::fscanf(file, entry_format, addr6p[0], addr6p[1],
+ addr6p[2], addr6p[3], addr6p[4], addr6p[5],
+ addr6p[6], addr6p[7], &if_idx, &prefix,
+ &scope, &dad_status, devname);
+ ASSERT_EQ(ret, 13);
+ EXPECT_STREQ(addr6p[0], "fe80");
+ EXPECT_STREQ(addr6p[1], "0000");
+ EXPECT_STREQ(addr6p[2], "0000");
+ EXPECT_STREQ(addr6p[3], "0000");
+ EXPECT_STREQ(addr6p[4], "adaf");
+ EXPECT_STREQ(addr6p[5], "2646");
+ EXPECT_STREQ(addr6p[6], "69ba");
+ EXPECT_STREQ(addr6p[7], "a4c7");
+ EXPECT_EQ(if_idx, 2);
+ EXPECT_EQ(prefix, 64);
+ EXPECT_EQ(scope, 32);
+ EXPECT_EQ(dad_status, 128);
+ EXPECT_STREQ(devname, "ens4");
+
+ // Validate second entry.
+ ret = LIBC_NAMESPACE::fscanf(file, entry_format, addr6p[0], addr6p[1],
+ addr6p[2], addr6p[3], addr6p[4], addr6p[5],
+ addr6p[6], addr6p[7], &if_idx, &prefix, &scope,
+ &dad_status, devname);
+ ASSERT_EQ(ret, 13);
+ for (int i = 0; i < 7; i++)
+ EXPECT_STREQ(addr6p[i], "0000");
+ EXPECT_STREQ(addr6p[7], "0001");
+ EXPECT_EQ(if_idx, 1);
+ EXPECT_EQ(prefix, 128);
+ EXPECT_EQ(scope, 16);
+ EXPECT_EQ(dad_status, 128);
+ EXPECT_STREQ(devname, "lo");
+
+ // No more entries, return EOF.
+ ret = LIBC_NAMESPACE::fscanf(file, entry_format, addr6p[0], addr6p[1],
+ addr6p[2], addr6p[3], addr6p[4], addr6p[5],
+ addr6p[6], addr6p[7], &if_idx, &prefix, &scope,
+ &dad_status, devname);
+ EXPECT_EQ(ret, EOF);
+ EXPECT_NE(scanf_test::feof(file), 0);
+ EXPECT_EQ(scanf_test::ferror(file), 0);
+
+ ASSERT_EQ(scanf_test::fclose(file), 0);
+}
+
+TEST(LlvmLibcFScanfTest, EofPartialMatch) {
+ const char *FILENAME = APPEND_LIBC_TEST("eof_partial_match.txt");
+ auto FILE_PATH = libc_make_test_file_path(FILENAME);
+ ::FILE *file = scanf_test::fopen(FILE_PATH, "w");
+ ASSERT_FALSE(file == nullptr);
+
+ constexpr char contents[] = "1 2 3";
+ ASSERT_EQ(sizeof(contents) - 1,
+ scanf_test::fwrite(contents, 1, sizeof(contents) - 1, file));
+ ASSERT_EQ(0, scanf_test::fclose(file));
+
+ file = scanf_test::fopen(FILE_PATH, "r");
+ ASSERT_FALSE(file == nullptr);
+
+ int vals[4] = {0};
+ int ret = LIBC_NAMESPACE::fscanf(file, "%d %d %d %d", &vals[0], &vals[1],
+ &vals[2], &vals[3]);
+ // Returns 3 for number of matches despite EOF.
+ EXPECT_EQ(ret, 3);
+ EXPECT_EQ(vals[0], 1);
+ EXPECT_EQ(vals[1], 2);
+ EXPECT_EQ(vals[2], 3);
+
+ EXPECT_NE(scanf_test::feof(file), 0);
+ EXPECT_EQ(scanf_test::ferror(file), 0);
+
+ ASSERT_EQ(scanf_test::fclose(file), 0);
+}
+
+TEST(LlvmLibcFScanfTest, MatchingErrors) {
+ const char *FILENAME = APPEND_LIBC_TEST("matching_error_partial_match.txt");
+ auto FILE_PATH = libc_make_test_file_path(FILENAME);
+ ::FILE *file = scanf_test::fopen(FILE_PATH, "w");
+ ASSERT_FALSE(file == nullptr);
+
+ constexpr char contents[] = "one is 1 two is 2";
+ ASSERT_EQ(sizeof(contents) - 1,
+ scanf_test::fwrite(contents, 1, sizeof(contents) - 1, file));
+ ASSERT_EQ(0, scanf_test::fclose(file));
+
+ file = scanf_test::fopen(FILE_PATH, "r");
+ ASSERT_FALSE(file == nullptr);
+
+ int vals[2] = {0};
+ // Immediate matching error.
+ int ret =
+ LIBC_NAMESPACE::fscanf(file, "zzz is %d two is %d", &vals[0], &vals[1]);
+ EXPECT_EQ(ret, 0);
+ EXPECT_EQ(scanf_test::feof(file), 0);
+ EXPECT_EQ(scanf_test::ferror(file), 0);
+
+ // Only the first item is matched.
+ ret = LIBC_NAMESPACE::fscanf(file, "one is %d zzz is %d", &vals[0], &vals[1]);
+ EXPECT_EQ(ret, 1);
+ EXPECT_EQ(vals[0], 1);
+ EXPECT_EQ(scanf_test::feof(file), 0);
+ EXPECT_EQ(scanf_test::ferror(file), 0);
+
+ // Second item is matched before EOF.
+ ret = LIBC_NAMESPACE::fscanf(file, "two is %d zzz is %d", &vals[0], &vals[1]);
+ EXPECT_EQ(ret, 1);
+ EXPECT_EQ(vals[0], 2);
+ EXPECT_NE(scanf_test::feof(file), 0);
+ EXPECT_EQ(scanf_test::ferror(file), 0);
+
+ ASSERT_EQ(scanf_test::fclose(file), 0);
+}
More information about the libc-commits
mailing list