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
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include <errno.h>
#include <stdio.h>
+#include <stdio_ext.h>
#include <sys/syscall.h>
#include <unistd.h>
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);
'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',
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#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);