From 384e88a238390073ba69d8f9b9837c65bf01648d Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 13 Nov 2025 13:40:19 +0900 Subject: [PATCH] musl: stdio: check if stream is writable earlier in fputs() and friends --- src/include/musl/stdio.h | 18 +++++++++++++++ src/libc/musl/stdio.c | 36 ++++++++++++++++++++++++++++++ src/test/meson.build | 1 + src/test/test-stdio.c | 48 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 src/test/test-stdio.c diff --git a/src/include/musl/stdio.h b/src/include/musl/stdio.h index d677201f457..2ff95e952a4 100644 --- a/src/include/musl/stdio.h +++ b/src/include/musl/stdio.h @@ -11,3 +11,21 @@ int missing_renameat2(int __oldfd, const char *__old, int __newfd, const char *__new, unsigned __flags); # define renameat2 missing_renameat2 #endif + +/* When a stream is opened read-only under glibc, fputs() and friends fail with EBADF. However, they + * succeed under musl. We rely on the glibc behavior in the code base. The following _check_writable() + * functions first check if the passed stream is writable, and refuse to write with EBADF if not. */ + +int putc_check_writable(int c, FILE *stream); +int putc_unlocked_check_writable(int c, FILE *stream); +int fputc_check_writable(int c, FILE *stream); +int fputc_unlocked_check_writable(int c, FILE *stream); +int fputs_check_writable(const char *s, FILE *stream); +int fputs_unlocked_check_writable(const char *s, FILE *stream); + +#define putc putc_check_writable +#define putc_unlocked putc_unlocked_check_writable +#define fputc fputc_check_writable +#define fputc_unlocked fputc_unlocked_check_writable +#define fputs fputs_check_writable +#define fputs_unlocked fputs_unlocked_check_writable diff --git a/src/libc/musl/stdio.c b/src/libc/musl/stdio.c index 102a22cd5cd..6d02aef5f65 100644 --- a/src/libc/musl/stdio.c +++ b/src/libc/musl/stdio.c @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include +#include #include #include @@ -9,3 +11,37 @@ int missing_renameat2(int __oldfd, const char *__old, int __newfd, const char *_ return syscall(__NR_renameat2, __oldfd, __old, __newfd, __new, __flags); } #endif + +#define DEFINE_PUT(func) \ + int func##_check_writable(int c, FILE *stream) { \ + if (!__fwritable(stream)) { \ + errno = EBADF; \ + return EOF; \ + } \ + \ + return func(c, stream); \ + } + +#define DEFINE_FPUTS(func) \ + int func##_check_writable(const char *s, FILE *stream) { \ + if (!__fwritable(stream)) { \ + errno = EBADF; \ + return EOF; \ + } \ + \ + return func(s, stream); \ + } + +#undef putc +#undef putc_unlocked +#undef fputc +#undef fputc_unlocked +#undef fputs +#undef fputs_unlocked + +DEFINE_PUT(putc); +DEFINE_PUT(putc_unlocked); +DEFINE_PUT(fputc); +DEFINE_PUT(fputc_unlocked); +DEFINE_FPUTS(fputs); +DEFINE_FPUTS(fputs_unlocked); diff --git a/src/test/meson.build b/src/test/meson.build index b46197e4b73..9f89ce8e58a 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -191,6 +191,7 @@ simple_tests += files( 'test-specifier.c', 'test-stat-util.c', 'test-static-destruct.c', + 'test-stdio.c', 'test-strbuf.c', 'test-string-util.c', 'test-strip-tab-ansi.c', diff --git a/src/test/test-stdio.c b/src/test/test-stdio.c new file mode 100644 index 00000000000..8dd19289609 --- /dev/null +++ b/src/test/test-stdio.c @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "tests.h" +#include "tmpfile-util.h" + +TEST(read_only) { + _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-stdio-read-only-XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_close_ int fd = -EBADF; + int r; + + ASSERT_OK(fd = mkostemp_safe(fn)); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = putc('a', f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = putc_unlocked('a', f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = fputc('a', f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = fputc_unlocked('a', f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = fputs("a", f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); + + ASSERT_NOT_NULL(f = fopen(fn, "r")); + ASSERT_ERROR_ERRNO(r = fputs_unlocked("a", f), EBADF); + ASSERT_EQ(r, EOF); + f = safe_fclose(f); +} + +DEFINE_TEST_MAIN(LOG_INFO); -- 2.47.3