Commit 1261c82f1e for openssl.org
commit 1261c82f1e8a961294f9b119c258b1952bdcfbe3
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Fri Jul 17 19:20:38 2026 +0200
Add BIO bss_file test
Add a cmocka based unit test for the file BIO covering BIO_new_file,
BIO_new_fp and the file method callbacks, with stdio and openssl_fopen.
Additionally add a Windows-only Detours based side test for the
Windows specific branches.
Assisted-by: Claude:claude-fable-5
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Andrew Dinh <andrewd@openssl.org>
MergeDate: Sat Aug 8 15:26:34 2026
(Merged from https://github.com/openssl/openssl/pull/31991)
diff --git a/test/unit/build.info b/test/unit/build.info
index ac04787457..f9e6cbdef9 100644
--- a/test/unit/build.info
+++ b/test/unit/build.info
@@ -50,6 +50,13 @@ IF[{- $config{target} =~ /^(?:linux|BSD)/ -}]
DEPEND[crypto/bio/test_bss_fd]=../../libcrypto.a
WRAP[crypto/bio/test_bss_fd]=read write lseek close
+ PROGRAMS{noinst}=crypto/bio/test_bss_file
+ SOURCE[crypto/bio/test_bss_file]=crypto/bio/test_bss_file.c
+ INCLUDE[crypto/bio/test_bss_file]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_file]=../../libcrypto.a
+ WRAP[crypto/bio/test_bss_file]=openssl_fopen fread fwrite fseek ftell feof \
+ ferror fclose fflush fgets
+
PROGRAMS{noinst}=crypto/bio/test_bss_sock
SOURCE[crypto/bio/test_bss_sock]=crypto/bio/test_bss_sock.c
WRAP[crypto/bio/test_bss_sock]=read write BIO_closesocket
@@ -64,4 +71,20 @@ IF[{- $config{target} =~ /^VC-/ -}]
INCLUDE[crypto/bio/test_bss_dgram_win]=../../include ../../include/internal \
../../crypto/bio
DEPEND[crypto/bio/test_bss_dgram_win]=../../libcrypto
+
+ # On uplink builds the test must see the same BIO_FLAGS_UPLINK_INTERNAL
+ # value as libcrypto, and needs applink so UP_* calls dispatch into the
+ # test executable, where the detours intercept them.
+ IF[{- !$disabled{uplink} -}]
+ $INITSRC=../../ms/applink.c
+ $UPLINKDEF=OPENSSL_USE_APPLINK
+ ENDIF
+
+ PROGRAMS{noinst}=crypto/bio/test_bss_file_win
+ SOURCE[crypto/bio/test_bss_file_win]=crypto/bio/test_bss_file_win.c $INITSRC
+ DEFINE[crypto/bio/test_bss_file_win]=$UPLINKDEF
+ UNIT_TEST[crypto/bio/test_bss_file_win]=cmocka detours
+ INCLUDE[crypto/bio/test_bss_file_win]=../../include ../../include/internal \
+ ../../crypto/bio ../..
+ DEPEND[crypto/bio/test_bss_file_win]=../../libcrypto
ENDIF
diff --git a/test/unit/crypto/bio/test_bss_file.c b/test/unit/crypto/bio/test_bss_file.c
new file mode 100644
index 0000000000..88388aeb3f
--- /dev/null
+++ b/test/unit/crypto/bio/test_bss_file.c
@@ -0,0 +1,856 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#ifdef OPENSSL_NO_STDIO
+
+int main(void)
+{
+ return 0;
+}
+
+#else
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <string.h>
+#include <cmocka.h>
+
+#include "bio_local.h"
+
+#include <openssl/bio.h>
+#include <openssl/err.h>
+
+static char fake_file_a;
+static char fake_file_b;
+#define FAKE_FP ((FILE *)&fake_file_a)
+#define FAKE_FP2 ((FILE *)&fake_file_b)
+
+/* wraps */
+
+FILE *__wrap_openssl_fopen(const char *filename, const char *mode);
+size_t __wrap_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
+size_t __wrap_fwrite(const void *ptr, size_t size, size_t nmemb,
+ FILE *stream);
+int __wrap_fseek(FILE *stream, long offset, int whence);
+long __wrap_ftell(FILE *stream);
+int __wrap_feof(FILE *stream);
+int __wrap_ferror(FILE *stream);
+int __wrap_fclose(FILE *stream);
+int __wrap_fflush(FILE *stream);
+char *__wrap_fgets(char *s, int size, FILE *stream);
+
+FILE *__wrap_openssl_fopen(const char *filename, const char *mode)
+{
+ FILE *fp;
+
+ function_called();
+ check_expected(filename);
+ check_expected(mode);
+ fp = mock_ptr_type(FILE *);
+ if (fp == NULL)
+ errno = mock_type(int);
+ return fp;
+}
+
+size_t __wrap_fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ function_called();
+ check_expected_ptr(ptr);
+ check_expected(size);
+ check_expected(nmemb);
+ check_expected_ptr(stream);
+ return mock_type(size_t);
+}
+
+size_t __wrap_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ function_called();
+ check_expected_ptr(ptr);
+ check_expected(size);
+ check_expected(nmemb);
+ check_expected_ptr(stream);
+ return mock_type(size_t);
+}
+
+int __wrap_fseek(FILE *stream, long offset, int whence)
+{
+ function_called();
+ check_expected_ptr(stream);
+ check_expected(offset);
+ check_expected(whence);
+ return mock_type(int);
+}
+
+long __wrap_ftell(FILE *stream)
+{
+ function_called();
+ check_expected_ptr(stream);
+ return mock_type(long);
+}
+
+int __wrap_feof(FILE *stream)
+{
+ function_called();
+ check_expected_ptr(stream);
+ return mock_type(int);
+}
+
+int __wrap_ferror(FILE *stream)
+{
+ function_called();
+ check_expected_ptr(stream);
+ return mock_type(int);
+}
+
+int __wrap_fclose(FILE *stream)
+{
+ function_called();
+ check_expected_ptr(stream);
+ return mock_type(int);
+}
+
+int __wrap_fflush(FILE *stream)
+{
+ function_called();
+ check_expected_ptr(stream);
+ return mock_type(int);
+}
+
+char *__wrap_fgets(char *s, int size, FILE *stream)
+{
+ const char *data;
+
+ function_called();
+ check_expected_ptr(s);
+ check_expected(size);
+ check_expected_ptr(stream);
+ data = mock_ptr_type(const char *);
+ if (data == NULL)
+ return NULL;
+ assert_true(strlen(data) < (size_t)size);
+ strcpy(s, data);
+ return s;
+}
+
+/* expectations */
+
+/* errnoval is consumed by __wrap_openssl_fopen only when rc == NULL */
+static void expect_openssl_fopen(const char *filename, const char *mode,
+ FILE *rc, int errnoval)
+{
+ expect_function_call(__wrap_openssl_fopen);
+ expect_string(__wrap_openssl_fopen, filename, filename);
+ expect_string(__wrap_openssl_fopen, mode, mode);
+ will_return(__wrap_openssl_fopen, rc);
+ if (rc == NULL)
+ will_return(__wrap_openssl_fopen, errnoval);
+}
+
+static void expect_fread(void *ptr, size_t nmemb, FILE *stream, size_t rc)
+{
+ expect_function_call(__wrap_fread);
+ expect_value(__wrap_fread, ptr, ptr);
+ expect_value(__wrap_fread, size, 1);
+ expect_value(__wrap_fread, nmemb, nmemb);
+ expect_value(__wrap_fread, stream, stream);
+ will_return(__wrap_fread, rc);
+}
+
+static void expect_fwrite(const void *ptr, size_t nmemb, FILE *stream,
+ size_t rc)
+{
+ expect_function_call(__wrap_fwrite);
+ expect_value(__wrap_fwrite, ptr, ptr);
+ expect_value(__wrap_fwrite, size, 1);
+ expect_value(__wrap_fwrite, nmemb, nmemb);
+ expect_value(__wrap_fwrite, stream, stream);
+ will_return(__wrap_fwrite, rc);
+}
+
+static void expect_fseek(FILE *stream, long offset, int whence, int rc)
+{
+ expect_function_call(__wrap_fseek);
+ expect_value(__wrap_fseek, stream, stream);
+ expect_value(__wrap_fseek, offset, offset);
+ expect_value(__wrap_fseek, whence, whence);
+ will_return(__wrap_fseek, rc);
+}
+
+static void expect_ftell(FILE *stream, long rc)
+{
+ expect_function_call(__wrap_ftell);
+ expect_value(__wrap_ftell, stream, stream);
+ will_return(__wrap_ftell, rc);
+}
+
+static void expect_feof(FILE *stream, int rc)
+{
+ expect_function_call(__wrap_feof);
+ expect_value(__wrap_feof, stream, stream);
+ will_return(__wrap_feof, rc);
+}
+
+static void expect_ferror(FILE *stream, int rc)
+{
+ expect_function_call(__wrap_ferror);
+ expect_value(__wrap_ferror, stream, stream);
+ will_return(__wrap_ferror, rc);
+}
+
+static void expect_fclose(FILE *stream, int rc)
+{
+ expect_function_call(__wrap_fclose);
+ expect_value(__wrap_fclose, stream, stream);
+ will_return(__wrap_fclose, rc);
+}
+
+static void expect_fflush(FILE *stream, int rc)
+{
+ expect_function_call(__wrap_fflush);
+ expect_value(__wrap_fflush, stream, stream);
+ will_return(__wrap_fflush, rc);
+}
+
+/* data is copied into the caller's buffer by __wrap_fgets; NULL means EOF */
+static void expect_fgets(char *s, int size, FILE *stream, const char *data)
+{
+ expect_function_call(__wrap_fgets);
+ expect_value(__wrap_fgets, s, s);
+ expect_value(__wrap_fgets, size, size);
+ expect_value(__wrap_fgets, stream, stream);
+ will_return(__wrap_fgets, data);
+}
+
+/* setup / teardown */
+
+static int setup(void **state)
+{
+ BIO *bio = BIO_new(BIO_s_file());
+
+ assert_non_null(bio);
+ BIO_set_fp(bio, FAKE_FP, BIO_NOCLOSE);
+ *state = bio;
+ return 0;
+}
+
+static int teardown(void **state)
+{
+ if (*state != NULL)
+ BIO_free(*state);
+ return 0;
+}
+
+/* BIO_new_file */
+
+static void test_new_file_success(void **state)
+{
+ BIO *bio;
+ FILE *fp = NULL;
+
+ (void)state;
+ expect_openssl_fopen("test.txt", "r", FAKE_FP, 0);
+ bio = BIO_new_file("test.txt", "r");
+ assert_non_null(bio);
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP);
+ assert_int_equal(BIO_get_close(bio), BIO_CLOSE);
+
+ expect_fclose(FAKE_FP, 0);
+ BIO_free(bio);
+}
+
+static void test_new_file_binary_mode(void **state)
+{
+ BIO *bio;
+
+ (void)state;
+ expect_openssl_fopen("test.bin", "rb", FAKE_FP, 0);
+ bio = BIO_new_file("test.bin", "rb");
+ assert_non_null(bio);
+
+ expect_fclose(FAKE_FP, 0);
+ BIO_free(bio);
+}
+
+static void test_new_file_no_such_file(void **state)
+{
+ (void)state;
+ ERR_clear_error();
+ expect_openssl_fopen("missing.txt", "r", NULL, ENOENT);
+ assert_null(BIO_new_file("missing.txt", "r"));
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()),
+ BIO_R_NO_SUCH_FILE);
+}
+
+static void test_new_file_sys_error(void **state)
+{
+ (void)state;
+ ERR_clear_error();
+ expect_openssl_fopen("secret.txt", "r", NULL, EACCES);
+ assert_null(BIO_new_file("secret.txt", "r"));
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()), ERR_R_SYS_LIB);
+}
+
+static void test_new_file_null_filename(void **state)
+{
+ (void)state;
+ ERR_clear_error();
+ assert_null(BIO_new_file(NULL, "r"));
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()),
+ ERR_R_PASSED_NULL_PARAMETER);
+}
+
+/* BIO_new_fp */
+
+static void test_new_fp_noclose(void **state)
+{
+ BIO *bio;
+ FILE *fp = NULL;
+
+ (void)state;
+ bio = BIO_new_fp(FAKE_FP, BIO_NOCLOSE);
+ assert_non_null(bio);
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP);
+ assert_int_equal(BIO_get_close(bio), BIO_NOCLOSE);
+ BIO_free(bio);
+}
+
+static void test_new_fp_close(void **state)
+{
+ BIO *bio;
+
+ (void)state;
+ bio = BIO_new_fp(FAKE_FP, BIO_CLOSE);
+ assert_non_null(bio);
+ assert_int_equal(BIO_get_close(bio), BIO_CLOSE);
+
+ expect_fclose(FAKE_FP, 0);
+ BIO_free(bio);
+}
+
+/* file_read (via BIO_read) */
+
+static void test_file_read_success(void **state)
+{
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ expect_fread(buf, 8, FAKE_FP, 8);
+ assert_int_equal(BIO_read(bio, buf, 8), 8);
+}
+
+static void test_file_read_short(void **state)
+{
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ expect_fread(buf, 8, FAKE_FP, 3);
+ assert_int_equal(BIO_read(bio, buf, 8), 3);
+}
+
+static void test_file_read_eof(void **state)
+{
+ /*
+ * fread returns 0 without ferror: plain EOF, no error is raised.
+ * The two trailing feof calls come from outside bss_file.c: one from
+ * bread_conv and one from bio_read_intern, which both query
+ * BIO_CTRL_EOF when the method's read reports 0 bytes.
+ */
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ expect_fread(buf, 8, FAKE_FP, 0);
+ expect_ferror(FAKE_FP, 0);
+ expect_feof(FAKE_FP, 1);
+ expect_feof(FAKE_FP, 1);
+ assert_int_equal(BIO_read(bio, buf, 8), 0);
+}
+
+static void test_file_read_error(void **state)
+{
+ /* fread returns 0 with ferror set: -1 and ERR_R_SYS_LIB is raised */
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ ERR_clear_error();
+ expect_fread(buf, 8, FAKE_FP, 0);
+ expect_ferror(FAKE_FP, 1);
+ assert_int_equal(BIO_read(bio, buf, 8), -1);
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()), ERR_R_SYS_LIB);
+}
+
+static void test_file_read_zero_length(void **state)
+{
+ /* outl == 0 never reaches fread */
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ assert_true(BIO_read(bio, buf, 0) <= 0);
+}
+
+/* file_write (via BIO_write) */
+
+static void test_file_write_success(void **state)
+{
+ BIO *bio = *state;
+ const char buf[] = "hello";
+
+ expect_fwrite(buf, 5, FAKE_FP, 5);
+ assert_int_equal(BIO_write(bio, buf, 5), 5);
+}
+
+static void test_file_write_partial(void **state)
+{
+ BIO *bio = *state;
+ const char buf[] = "hello";
+
+ expect_fwrite(buf, 5, FAKE_FP, 3);
+ assert_int_equal(BIO_write(bio, buf, 5), 3);
+}
+
+static void test_file_write_fails(void **state)
+{
+ BIO *bio = *state;
+ const char buf[] = "hello";
+
+ expect_fwrite(buf, 5, FAKE_FP, 0);
+ assert_true(BIO_write(bio, buf, 5) <= 0);
+}
+
+/* file_ctrl (via BIO_ctrl) */
+
+static void test_file_ctrl_reset(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fseek(FAKE_FP, 0, 0, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_RESET, 0, NULL), 0);
+}
+
+static void test_file_ctrl_seek(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fseek(FAKE_FP, 512, 0, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_FILE_SEEK, 512, NULL), 0);
+}
+
+static void test_file_ctrl_seek_fails(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fseek(FAKE_FP, 512, 0, -1);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_FILE_SEEK, 512, NULL), -1);
+}
+
+static void test_file_ctrl_tell(void **state)
+{
+ BIO *bio = *state;
+
+ expect_ftell(FAKE_FP, 256);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_FILE_TELL, 0, NULL), 256);
+}
+
+static void test_file_ctrl_info(void **state)
+{
+ BIO *bio = *state;
+
+ /* BIO_CTRL_INFO shares the ftell branch with FILE_TELL */
+ expect_ftell(FAKE_FP, 128);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_INFO, 0, NULL), 128);
+}
+
+static void test_file_ctrl_eof_clear(void **state)
+{
+ BIO *bio = *state;
+
+ expect_feof(FAKE_FP, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 0);
+}
+
+static void test_file_ctrl_eof_set(void **state)
+{
+ /* any non-zero feof result is mapped to 1 by double negation */
+ BIO *bio = *state;
+
+ expect_feof(FAKE_FP, 7);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 1);
+}
+
+static void test_file_ctrl_set_fp_replaces(void **state)
+{
+ /* shutdown=BIO_NOCLOSE: the old fp is dropped without fclose */
+ BIO *bio = *state;
+ FILE *fp = NULL;
+
+ BIO_set_fp(bio, FAKE_FP2, BIO_NOCLOSE);
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP2);
+ assert_int_equal(bio->init, 1);
+}
+
+static void test_file_ctrl_set_fp_closes_old(void **state)
+{
+ /* shutdown=BIO_CLOSE: the old fp is fclosed before the replacement */
+ BIO *bio = *state;
+ FILE *fp = NULL;
+
+ BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_CLOSE, NULL);
+ expect_fclose(FAKE_FP, 0);
+ BIO_set_fp(bio, FAKE_FP2, BIO_NOCLOSE);
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP2);
+ assert_int_equal(BIO_get_close(bio), BIO_NOCLOSE);
+}
+
+static void test_file_ctrl_get_fp(void **state)
+{
+ BIO *bio = *state;
+ FILE *fp = NULL;
+
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP);
+}
+
+static void set_filename_test(BIO *bio, long flags, const char *mode)
+{
+ FILE *fp = NULL;
+
+ expect_openssl_fopen("file.txt", mode, FAKE_FP2, 0);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_C_SET_FILENAME, flags, (void *)"file.txt"), 1);
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP2);
+}
+
+static void test_file_ctrl_set_filename_read(void **state)
+{
+ set_filename_test(*state, BIO_FP_READ, "r");
+}
+
+static void test_file_ctrl_set_filename_write(void **state)
+{
+ set_filename_test(*state, BIO_FP_WRITE, "w");
+}
+
+static void test_file_ctrl_set_filename_read_write(void **state)
+{
+ set_filename_test(*state, BIO_FP_READ | BIO_FP_WRITE, "r+");
+}
+
+static void test_file_ctrl_set_filename_append(void **state)
+{
+ set_filename_test(*state, BIO_FP_APPEND, "a");
+}
+
+static void test_file_ctrl_set_filename_append_read(void **state)
+{
+ set_filename_test(*state, BIO_FP_APPEND | BIO_FP_READ, "a+");
+}
+
+static void test_file_ctrl_set_filename_close_flag(void **state)
+{
+ BIO *bio = *state;
+
+ expect_openssl_fopen("file.txt", "r", FAKE_FP2, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_SET_FILENAME,
+ BIO_CLOSE | BIO_FP_READ, (void *)"file.txt"),
+ 1);
+ assert_int_equal(BIO_get_close(bio), BIO_CLOSE);
+ /* Restore before teardown to avoid an unexpected fclose call. */
+ BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_NOCLOSE, NULL);
+}
+
+static void test_file_ctrl_set_filename_bad_mode(void **state)
+{
+ /* no BIO_FP_* mode flag at all: BIO_R_BAD_FOPEN_MODE, no fopen call */
+ BIO *bio = *state;
+
+ ERR_clear_error();
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_C_SET_FILENAME, 0, (void *)"file.txt"), 0);
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()),
+ BIO_R_BAD_FOPEN_MODE);
+}
+
+static void test_file_ctrl_set_filename_null(void **state)
+{
+ BIO *bio = *state;
+
+ ERR_clear_error();
+ assert_int_equal(BIO_ctrl(bio, BIO_C_SET_FILENAME, BIO_FP_READ, NULL), 0);
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()),
+ ERR_R_PASSED_NULL_PARAMETER);
+}
+
+static void test_file_ctrl_set_filename_fopen_fails(void **state)
+{
+ BIO *bio = *state;
+
+ ERR_clear_error();
+ expect_openssl_fopen("file.txt", "r", NULL, EACCES);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_C_SET_FILENAME, BIO_FP_READ, (void *)"file.txt"),
+ 0);
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()), ERR_R_SYS_LIB);
+}
+
+static void test_file_ctrl_get_close(void **state)
+{
+ BIO *bio = *state;
+
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_GET_CLOSE, 0, NULL),
+ BIO_NOCLOSE);
+ bio->shutdown = BIO_CLOSE;
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_GET_CLOSE, 0, NULL), BIO_CLOSE);
+ bio->shutdown = BIO_NOCLOSE;
+}
+
+static void test_file_ctrl_set_close(void **state)
+{
+ BIO *bio = *state;
+
+ BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_CLOSE, NULL);
+ assert_int_equal(bio->shutdown, BIO_CLOSE);
+ /* Restore before teardown to avoid an unexpected fclose call. */
+ BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_NOCLOSE, NULL);
+}
+
+static void test_file_ctrl_flush(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fflush(FAKE_FP, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_FLUSH, 0, NULL), 1);
+}
+
+static void test_file_ctrl_flush_fails(void **state)
+{
+ BIO *bio = *state;
+
+ ERR_clear_error();
+ expect_fflush(FAKE_FP, EOF);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_FLUSH, 0, NULL), 0);
+ assert_int_equal(ERR_GET_REASON(ERR_peek_last_error()), ERR_R_SYS_LIB);
+}
+
+static void test_file_ctrl_dup(void **state)
+{
+ assert_int_equal(BIO_ctrl(*state, BIO_CTRL_DUP, 0, NULL), 1);
+}
+
+static void test_file_ctrl_pending(void **state)
+{
+ assert_int_equal(BIO_ctrl(*state, BIO_CTRL_PENDING, 0, NULL), 0);
+}
+
+static void test_file_ctrl_wpending(void **state)
+{
+ assert_int_equal(BIO_ctrl(*state, BIO_CTRL_WPENDING, 0, NULL), 0);
+}
+
+static void test_file_ctrl_default(void **state)
+{
+ assert_int_equal(BIO_ctrl(*state, 9999, 0, NULL), 0);
+}
+
+/* file_gets (via BIO_gets) */
+
+static void test_file_gets_success(void **state)
+{
+ BIO *bio = *state;
+ char buf[16] = { 0 };
+
+ expect_fgets(buf, (int)sizeof(buf), FAKE_FP, "hi\n");
+ assert_int_equal(BIO_gets(bio, buf, (int)sizeof(buf)), 3);
+ assert_string_equal(buf, "hi\n");
+}
+
+static void test_file_gets_empty(void **state)
+{
+ /* fgets succeeds but stores an empty string: 0 is returned */
+ BIO *bio = *state;
+ char buf[16] = { 'x' };
+
+ expect_fgets(buf, (int)sizeof(buf), FAKE_FP, "");
+ assert_int_equal(BIO_gets(bio, buf, (int)sizeof(buf)), 0);
+ assert_int_equal(buf[0], '\0');
+}
+
+static void test_file_gets_eof(void **state)
+{
+ /* fgets returns NULL: 0 is returned and the buffer is cleared */
+ BIO *bio = *state;
+ char buf[16] = { 'x' };
+
+ expect_fgets(buf, (int)sizeof(buf), FAKE_FP, NULL);
+ assert_int_equal(BIO_gets(bio, buf, (int)sizeof(buf)), 0);
+ assert_int_equal(buf[0], '\0');
+}
+
+/* file_puts (via BIO_puts) */
+
+static void test_file_puts_success(void **state)
+{
+ BIO *bio = *state;
+ const char *str = "hello";
+
+ expect_fwrite(str, 5, FAKE_FP, 5);
+ assert_int_equal(BIO_puts(bio, str), 5);
+}
+
+static void test_file_puts_write_fails(void **state)
+{
+ BIO *bio = *state;
+ const char *str = "hello";
+
+ expect_fwrite(str, 5, FAKE_FP, 0);
+ assert_true(BIO_puts(bio, str) <= 0);
+}
+
+/* file_free (via BIO_free) */
+
+static void test_file_free_shutdown_closes(void **state)
+{
+ /* shutdown=1, init=1 and ptr set: fclose must be called */
+ BIO *bio = BIO_new(BIO_s_file());
+
+ (void)state;
+ assert_non_null(bio);
+ bio->ptr = FAKE_FP;
+ bio->init = 1;
+ bio->shutdown = BIO_CLOSE;
+
+ expect_fclose(FAKE_FP, 0);
+ BIO_free(bio);
+}
+
+static void test_file_free_no_shutdown(void **state)
+{
+ /* shutdown=0: fclose must NOT be called regardless of init */
+ BIO *bio = BIO_new(BIO_s_file());
+
+ (void)state;
+ assert_non_null(bio);
+ bio->ptr = FAKE_FP;
+ bio->init = 1;
+ bio->shutdown = BIO_NOCLOSE;
+
+ BIO_free(bio);
+}
+
+static void test_file_free_shutdown_no_init(void **state)
+{
+ /* shutdown=1 but init=0: fclose must NOT be called */
+ BIO *bio = BIO_new(BIO_s_file());
+
+ (void)state;
+ assert_non_null(bio);
+ bio->ptr = FAKE_FP;
+ bio->init = 0;
+ bio->shutdown = BIO_CLOSE;
+
+ BIO_free(bio);
+}
+
+static void test_file_free_shutdown_null_ptr(void **state)
+{
+ /* shutdown=1, init=1 but ptr=NULL: fclose must NOT be called */
+ BIO *bio = BIO_new(BIO_s_file());
+
+ (void)state;
+ assert_non_null(bio);
+ bio->ptr = NULL;
+ bio->init = 1;
+ bio->shutdown = BIO_CLOSE;
+
+ BIO_free(bio);
+}
+
+/* main */
+
+#define FILE_TEST(name) \
+ cmocka_unit_test_setup_teardown(name, setup, teardown)
+
+#define FILE_TEST_PLAIN(name) \
+ cmocka_unit_test(name)
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ /* BIO_new_file */
+ FILE_TEST_PLAIN(test_new_file_success),
+ FILE_TEST_PLAIN(test_new_file_binary_mode),
+ FILE_TEST_PLAIN(test_new_file_no_such_file),
+ FILE_TEST_PLAIN(test_new_file_sys_error),
+ FILE_TEST_PLAIN(test_new_file_null_filename),
+ /* BIO_new_fp */
+ FILE_TEST_PLAIN(test_new_fp_noclose),
+ FILE_TEST_PLAIN(test_new_fp_close),
+ /* file_read */
+ FILE_TEST(test_file_read_success),
+ FILE_TEST(test_file_read_short),
+ FILE_TEST(test_file_read_eof),
+ FILE_TEST(test_file_read_error),
+ FILE_TEST(test_file_read_zero_length),
+ /* file_write */
+ FILE_TEST(test_file_write_success),
+ FILE_TEST(test_file_write_partial),
+ FILE_TEST(test_file_write_fails),
+ /* file_ctrl */
+ FILE_TEST(test_file_ctrl_reset),
+ FILE_TEST(test_file_ctrl_seek),
+ FILE_TEST(test_file_ctrl_seek_fails),
+ FILE_TEST(test_file_ctrl_tell),
+ FILE_TEST(test_file_ctrl_info),
+ FILE_TEST(test_file_ctrl_eof_clear),
+ FILE_TEST(test_file_ctrl_eof_set),
+ FILE_TEST(test_file_ctrl_set_fp_replaces),
+ FILE_TEST(test_file_ctrl_set_fp_closes_old),
+ FILE_TEST(test_file_ctrl_get_fp),
+ FILE_TEST(test_file_ctrl_set_filename_read),
+ FILE_TEST(test_file_ctrl_set_filename_write),
+ FILE_TEST(test_file_ctrl_set_filename_read_write),
+ FILE_TEST(test_file_ctrl_set_filename_append),
+ FILE_TEST(test_file_ctrl_set_filename_append_read),
+ FILE_TEST(test_file_ctrl_set_filename_close_flag),
+ FILE_TEST(test_file_ctrl_set_filename_bad_mode),
+ FILE_TEST(test_file_ctrl_set_filename_null),
+ FILE_TEST(test_file_ctrl_set_filename_fopen_fails),
+ FILE_TEST(test_file_ctrl_get_close),
+ FILE_TEST(test_file_ctrl_set_close),
+ FILE_TEST(test_file_ctrl_flush),
+ FILE_TEST(test_file_ctrl_flush_fails),
+ FILE_TEST(test_file_ctrl_dup),
+ FILE_TEST(test_file_ctrl_pending),
+ FILE_TEST(test_file_ctrl_wpending),
+ FILE_TEST(test_file_ctrl_default),
+ /* file_gets */
+ FILE_TEST(test_file_gets_success),
+ FILE_TEST(test_file_gets_empty),
+ FILE_TEST(test_file_gets_eof),
+ /* file_puts */
+ FILE_TEST(test_file_puts_success),
+ FILE_TEST(test_file_puts_write_fails),
+ /* file_free */
+ FILE_TEST_PLAIN(test_file_free_shutdown_closes),
+ FILE_TEST_PLAIN(test_file_free_no_shutdown),
+ FILE_TEST_PLAIN(test_file_free_shutdown_no_init),
+ FILE_TEST_PLAIN(test_file_free_shutdown_null_ptr),
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
+
+#endif /* OPENSSL_NO_STDIO */
diff --git a/test/unit/crypto/bio/test_bss_file_win.c b/test/unit/crypto/bio/test_bss_file_win.c
new file mode 100644
index 0000000000..3d77512fb9
--- /dev/null
+++ b/test/unit/crypto/bio/test_bss_file_win.c
@@ -0,0 +1,753 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+/*
+ * Windows-only side test for bss_file.c Windows-specific paths.
+ * Uses Microsoft Detours to intercept CRT stdio calls at runtime.
+ * Does NOT replace the normal --wrap-based bss_file test; it only
+ * covers branches that differ under OPENSSL_SYS_WINDOWS: the feof
+ * EINVAL quirk, the GetFileType guard around ftell, the _setmode
+ * text/binary handling, the "b"/"t" fopen mode suffix, and (on
+ * uplink builds) the UP_* applink dispatch.
+ *
+ * The CRT interception relies on the test executable and libcrypto
+ * resolving stdio to the same CRT (the /MD default, where both use
+ * ucrtbase.dll); Detours patches the function bodies there, so calls
+ * from either module land in the mocks, and errno set by a mock is
+ * visible to libcrypto since the per-thread errno lives in the shared
+ * CRT. On uplink builds the applink table is populated with this
+ * executable's CRT functions (ms/applink.c is linked in), which the
+ * same detours intercept.
+ *
+ * NOTE: not compiled/verified by the author's toolchain. The first
+ * test (detour_probe) is a hard gate: if Detours does not intercept
+ * cross-module CRT calls, every other result is meaningless.
+ */
+
+#include "openssl/e_os2.h"
+
+#if defined(OPENSSL_NO_STDIO) || !defined(OPENSSL_SYS_WINDOWS)
+int main(void)
+{
+ return 0;
+}
+#else
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <windows.h>
+#include <io.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <wchar.h>
+#include <errno.h>
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <string.h>
+#include <cmocka.h>
+#include <detours/detours.h>
+
+#include "internal/sockets.h"
+#include "bio_local.h"
+
+#include <openssl/bio.h>
+#include <openssl/err.h>
+
+static char fake_file_a;
+static char fake_file_b;
+#define FAKE_FP ((FILE *)&fake_file_a)
+#define FAKE_FP2 ((FILE *)&fake_file_b)
+#define FAKE_FD 7
+#define FAKE_OSFHANDLE ((intptr_t)0x1234)
+
+/*
+ * Real function pointers. After DetourAttach commits, Detours rewrites
+ * these to trampolines pointing at the original code.
+ */
+static int(__cdecl *real_feof)(FILE *) = feof;
+static long(__cdecl *real_ftell)(FILE *) = ftell;
+static size_t(__cdecl *real_fread)(void *, size_t, size_t, FILE *) = fread;
+static size_t(__cdecl *real_fwrite)(const void *, size_t, size_t, FILE *)
+ = fwrite;
+static int(__cdecl *real_fclose)(FILE *) = fclose;
+static int(__cdecl *real_fileno)(FILE *) = _fileno;
+static int(__cdecl *real_setmode)(int, int) = _setmode;
+static intptr_t(__cdecl *real_get_osfhandle)(int) = _get_osfhandle;
+static FILE *(__cdecl *real_wfopen)(const wchar_t *, const wchar_t *)
+ = _wfopen;
+static DWORD(WINAPI *real_GetFileType)(HANDLE) = GetFileType;
+
+/*
+ * The detours are installed process-wide for the whole run, but the CRT
+ * (and cmocka itself) call some of these functions internally -- most
+ * importantly _fileno, which the stdio write path invokes on every
+ * printf/fwrite used to emit the TAP output. If a mock enforced cmocka
+ * expectations on those internal calls it would recurse (an unexpected
+ * call makes cmocka print an error, which calls _fileno again, ...) and
+ * blow the stack. So a mock only enforces expectations while a test has
+ * explicitly armed it via g_mocks_armed; otherwise it forwards to the
+ * real function through the Detours trampoline. Each mock also disarms
+ * around its own cmocka calls so that a failing expectation can report
+ * cleanly (its error output would otherwise re-enter the mock).
+ */
+static int g_mocks_armed;
+
+static void mocks_arm(void)
+{
+ g_mocks_armed = 1;
+}
+
+static void mocks_disarm(void)
+{
+ g_mocks_armed = 0;
+}
+
+/*
+ * detour_probe uses this to confirm the mock actually fired. It is the
+ * only place a mock is allowed to run outside a cmocka expectation, so
+ * mock_feof special-cases it.
+ */
+static int g_probe_active;
+static int g_probe_feof_hits;
+
+/* mocks */
+
+static int __cdecl mock_feof(FILE *stream)
+{
+ int rc;
+
+ /* Probe path: no expectations queued, just record and answer. */
+ if (g_probe_active) {
+ g_probe_feof_hits++;
+ return 0;
+ }
+ if (!g_mocks_armed)
+ return real_feof(stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(stream);
+ rc = mock_type(int);
+ /*
+ * bss_file.c inspects errno after feof to detect the invalid-stream
+ * case, so the mock always programs it (0 for the normal case).
+ */
+ errno = mock_type(int);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static long __cdecl mock_ftell(FILE *stream)
+{
+ long rc;
+
+ if (!g_mocks_armed)
+ return real_ftell(stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(stream);
+ rc = mock_type(long);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static size_t __cdecl mock_fread(void *ptr, size_t size, size_t nmemb,
+ FILE *stream)
+{
+ size_t rc;
+
+ if (!g_mocks_armed)
+ return real_fread(ptr, size, nmemb, stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(ptr);
+ check_expected(size);
+ check_expected(nmemb);
+ check_expected_ptr(stream);
+ rc = mock_type(size_t);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static size_t __cdecl mock_fwrite(const void *ptr, size_t size, size_t nmemb,
+ FILE *stream)
+{
+ size_t rc;
+
+ if (!g_mocks_armed)
+ return real_fwrite(ptr, size, nmemb, stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(ptr);
+ check_expected(size);
+ check_expected(nmemb);
+ check_expected_ptr(stream);
+ rc = mock_type(size_t);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static int __cdecl mock_fclose(FILE *stream)
+{
+ int rc;
+
+ if (!g_mocks_armed)
+ return real_fclose(stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(stream);
+ rc = mock_type(int);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static int __cdecl mock_fileno(FILE *stream)
+{
+ int rc;
+
+ if (!g_mocks_armed)
+ return real_fileno(stream);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(stream);
+ rc = mock_type(int);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static int __cdecl mock_setmode(int fd, int mode)
+{
+ int rc;
+
+ if (!g_mocks_armed)
+ return real_setmode(fd, mode);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected(fd);
+ check_expected(mode);
+ rc = mock_type(int);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static intptr_t __cdecl mock_get_osfhandle(int fd)
+{
+ intptr_t rc;
+
+ if (!g_mocks_armed)
+ return real_get_osfhandle(fd);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected(fd);
+ rc = mock_type(intptr_t);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static FILE *__cdecl mock_wfopen(const wchar_t *filename,
+ const wchar_t *mode)
+{
+ const wchar_t *exp_filename;
+ const wchar_t *exp_mode;
+ FILE *rc;
+
+ if (!g_mocks_armed)
+ return real_wfopen(filename, mode);
+
+ g_mocks_armed = 0;
+ function_called();
+ exp_filename = mock_ptr_type(const wchar_t *);
+ exp_mode = mock_ptr_type(const wchar_t *);
+ assert_int_equal(wcscmp(filename, exp_filename), 0);
+ assert_int_equal(wcscmp(mode, exp_mode), 0);
+ rc = mock_ptr_type(FILE *);
+ if (rc == NULL)
+ errno = mock_type(int);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+static DWORD WINAPI mock_GetFileType(HANDLE h)
+{
+ DWORD rc;
+
+ if (!g_mocks_armed)
+ return real_GetFileType(h);
+
+ g_mocks_armed = 0;
+ function_called();
+ check_expected_ptr(h);
+ rc = mock_type(DWORD);
+ g_mocks_armed = 1;
+ return rc;
+}
+
+/* expectations */
+
+/*
+ * errnoval is always consumed by mock_feof; pass 0 unless the test
+ * exercises the invalid-stream (EINVAL) quirk.
+ */
+static void expect_feof(FILE *stream, int rc, int errnoval)
+{
+ expect_function_call(mock_feof);
+ expect_value(mock_feof, stream, stream);
+ will_return(mock_feof, rc);
+ will_return(mock_feof, errnoval);
+}
+
+static void expect_ftell(FILE *stream, long rc)
+{
+ expect_function_call(mock_ftell);
+ expect_value(mock_ftell, stream, stream);
+ will_return(mock_ftell, rc);
+}
+
+static void expect_fread(void *ptr, size_t nmemb, FILE *stream, size_t rc)
+{
+ expect_function_call(mock_fread);
+ expect_value(mock_fread, ptr, ptr);
+ expect_value(mock_fread, size, 1);
+ expect_value(mock_fread, nmemb, nmemb);
+ expect_value(mock_fread, stream, stream);
+ will_return(mock_fread, rc);
+}
+
+static void expect_fwrite(const void *ptr, size_t nmemb, FILE *stream,
+ size_t rc)
+{
+ expect_function_call(mock_fwrite);
+ expect_value(mock_fwrite, ptr, ptr);
+ expect_value(mock_fwrite, size, 1);
+ expect_value(mock_fwrite, nmemb, nmemb);
+ expect_value(mock_fwrite, stream, stream);
+ will_return(mock_fwrite, rc);
+}
+
+static void expect_fclose(FILE *stream, int rc)
+{
+ expect_function_call(mock_fclose);
+ expect_value(mock_fclose, stream, stream);
+ will_return(mock_fclose, rc);
+}
+
+static void expect_fileno(FILE *stream, int rc)
+{
+ expect_function_call(mock_fileno);
+ expect_value(mock_fileno, stream, stream);
+ will_return(mock_fileno, rc);
+}
+
+static void expect_setmode(int fd, int mode, int rc)
+{
+ expect_function_call(mock_setmode);
+ expect_value(mock_setmode, fd, fd);
+ expect_value(mock_setmode, mode, mode);
+ will_return(mock_setmode, rc);
+}
+
+static void expect_get_osfhandle(int fd, intptr_t rc)
+{
+ expect_function_call(mock_get_osfhandle);
+ expect_value(mock_get_osfhandle, fd, fd);
+ will_return(mock_get_osfhandle, rc);
+}
+
+static void expect_wfopen(const wchar_t *filename, const wchar_t *mode,
+ FILE *rc, int errnoval)
+{
+ expect_function_call(mock_wfopen);
+ will_return(mock_wfopen, filename);
+ will_return(mock_wfopen, mode);
+ will_return(mock_wfopen, rc);
+ if (rc == NULL)
+ will_return(mock_wfopen, errnoval);
+}
+
+static void expect_GetFileType(intptr_t h, DWORD rc)
+{
+ expect_function_call(mock_GetFileType);
+ expect_value(mock_GetFileType, h, (HANDLE)h);
+ will_return(mock_GetFileType, rc);
+}
+
+/*
+ * Setting a FILE pointer always routes through the text/binary mode
+ * setup: _setmode on the non-uplink path, UP_fsetmod -> applink ->
+ * _setmode on the uplink path. Either way the same two CRT calls are
+ * observed.
+ */
+static void expect_set_fp_mode(FILE *fp, int fd, int mode)
+{
+ expect_fileno(fp, fd);
+ expect_setmode(fd, mode, 0);
+}
+
+/* detours */
+
+static int attach_detours(void)
+{
+ if (DetourTransactionBegin() != NO_ERROR)
+ return 0;
+ if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_feof, mock_feof) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_ftell, mock_ftell) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_fread, mock_fread) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_fwrite, mock_fwrite) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_fclose, mock_fclose) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_fileno, mock_fileno) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_setmode, mock_setmode) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_get_osfhandle, mock_get_osfhandle)
+ != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_wfopen, mock_wfopen) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_GetFileType, mock_GetFileType)
+ != NO_ERROR)
+ return 0;
+ return DetourTransactionCommit() == NO_ERROR;
+}
+
+static int detach_detours(void)
+{
+ if (DetourTransactionBegin() != NO_ERROR)
+ return 0;
+ if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR)
+ return 0;
+ DetourDetach((PVOID *)&real_feof, mock_feof);
+ DetourDetach((PVOID *)&real_ftell, mock_ftell);
+ DetourDetach((PVOID *)&real_fread, mock_fread);
+ DetourDetach((PVOID *)&real_fwrite, mock_fwrite);
+ DetourDetach((PVOID *)&real_fclose, mock_fclose);
+ DetourDetach((PVOID *)&real_fileno, mock_fileno);
+ DetourDetach((PVOID *)&real_setmode, mock_setmode);
+ DetourDetach((PVOID *)&real_get_osfhandle, mock_get_osfhandle);
+ DetourDetach((PVOID *)&real_wfopen, mock_wfopen);
+ DetourDetach((PVOID *)&real_GetFileType, mock_GetFileType);
+ return DetourTransactionCommit() == NO_ERROR;
+}
+
+/* setup / teardown */
+
+/*
+ * A BIO in the state BIO_new_file leaves it in: the UPLINK flag is
+ * cleared, so all Windows-specific non-uplink branches are reachable.
+ */
+static int setup_internal(void **state)
+{
+ BIO *bio = BIO_new(BIO_s_file());
+
+ assert_non_null(bio);
+ BIO_clear_flags(bio, BIO_FLAGS_UPLINK_INTERNAL);
+ mocks_arm();
+ expect_set_fp_mode(FAKE_FP, FAKE_FD, _O_BINARY);
+ BIO_set_fp(bio, FAKE_FP, BIO_NOCLOSE);
+ *state = bio;
+ return 0;
+}
+
+static int teardown(void **state)
+{
+ /*
+ * Disarm before BIO_free (and before cmocka prints the test result):
+ * once disarmed the mocks forward to the real CRT, so neither the
+ * NOCLOSE teardown nor the TAP output re-enters an expectation.
+ */
+ mocks_disarm();
+ if (*state != NULL)
+ BIO_free(*state);
+ return 0;
+}
+
+static int group_setup(void **state)
+{
+ (void)state;
+ assert_true(attach_detours());
+ return 0;
+}
+
+static int group_teardown(void **state)
+{
+ (void)state;
+ assert_true(detach_detours());
+ return 0;
+}
+
+/*
+ * GATE: prove Detours intercepts a CRT call. If this fails, fix the
+ * linkage (e.g. the test and libcrypto using different CRTs) before
+ * trusting any other test below.
+ */
+static void detour_probe(void **state)
+{
+ (void)state;
+ g_probe_feof_hits = 0;
+ g_probe_active = 1;
+ feof(stdin);
+ g_probe_active = 0;
+
+ assert_int_equal(g_probe_feof_hits, 1);
+}
+
+/*
+ * BIO_CTRL_EOF: feof returning 0 on an invalid stream sets errno to
+ * EINVAL, which bss_file.c maps to -EINVAL (Windows only).
+ */
+
+static void test_win_eof_invalid_stream(void **state)
+{
+ BIO *bio = *state;
+
+ expect_feof(FAKE_FP, 0, EINVAL);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), -EINVAL);
+}
+
+static void test_win_eof_not_eof(void **state)
+{
+ BIO *bio = *state;
+
+ expect_feof(FAKE_FP, 0, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 0);
+}
+
+static void test_win_eof_at_eof(void **state)
+{
+ /* any non-zero feof result is mapped to 1 by double negation */
+ BIO *bio = *state;
+
+ expect_feof(FAKE_FP, 7, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 1);
+}
+
+/*
+ * BIO_C_FILE_TELL: on Windows the non-uplink path refuses to ftell
+ * non-seekable files (GetFileType != FILE_TYPE_DISK), e.g. stdin.
+ */
+
+static void test_win_tell_non_disk(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fileno(FAKE_FP, FAKE_FD);
+ expect_get_osfhandle(FAKE_FD, FAKE_OSFHANDLE);
+ expect_GetFileType(FAKE_OSFHANDLE, FILE_TYPE_PIPE);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_FILE_TELL, 0, NULL), -1);
+}
+
+static void test_win_tell_disk(void **state)
+{
+ BIO *bio = *state;
+
+ expect_fileno(FAKE_FP, FAKE_FD);
+ expect_get_osfhandle(FAKE_FD, FAKE_OSFHANDLE);
+ expect_GetFileType(FAKE_OSFHANDLE, FILE_TYPE_DISK);
+ expect_ftell(FAKE_FP, 12345);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_FILE_TELL, 0, NULL), 12345);
+}
+
+/* BIO_C_SET_FILE_PTR: text/binary mode is applied with _setmode */
+
+static void test_win_set_fp_text_mode(void **state)
+{
+ BIO *bio = *state;
+
+ expect_set_fp_mode(FAKE_FP2, FAKE_FD, _O_TEXT);
+ BIO_set_fp(bio, FAKE_FP2, BIO_NOCLOSE | BIO_FP_TEXT);
+}
+
+static void test_win_set_fp_binary_mode(void **state)
+{
+ BIO *bio = *state;
+
+ expect_set_fp_mode(FAKE_FP2, FAKE_FD, _O_BINARY);
+ BIO_set_fp(bio, FAKE_FP2, BIO_NOCLOSE);
+}
+
+/*
+ * BIO_C_SET_FILENAME: on Windows an explicit "b" or "t" is appended to
+ * the fopen mode. The filename and mode are checked in mock_wfopen,
+ * where openssl_fopen's UTF-8 conversion of plain ASCII input lands.
+ */
+
+static void test_win_set_filename_appends_b(void **state)
+{
+ BIO *bio = *state;
+
+ expect_wfopen(L"file.txt", L"rb", FAKE_FP2, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_SET_FILENAME, BIO_FP_READ,
+ (void *)"file.txt"),
+ 1);
+}
+
+static void test_win_set_filename_appends_t(void **state)
+{
+ BIO *bio = *state;
+
+ expect_wfopen(L"file.txt", L"rt", FAKE_FP2, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_C_SET_FILENAME,
+ BIO_FP_READ | BIO_FP_TEXT, (void *)"file.txt"),
+ 1);
+}
+
+#if BIO_FLAGS_UPLINK_INTERNAL != 0
+
+/*
+ * Uplink-only behaviour (requires an applink-enabled build; this test
+ * links ms/applink.c, so UP_* calls resolve to this executable's CRT).
+ */
+
+static int setup_uplink(void **state)
+{
+ BIO *bio;
+
+ mocks_arm();
+ /* BIO_new_fp keeps the UPLINK flag, so UP_fsetmod is dispatched */
+ expect_set_fp_mode(FAKE_FP, FAKE_FD, _O_BINARY);
+ bio = BIO_new_fp(FAKE_FP, BIO_NOCLOSE);
+ assert_non_null(bio);
+ assert_true(BIO_test_flags(bio, BIO_FLAGS_UPLINK_INTERNAL));
+ *state = bio;
+ return 0;
+}
+
+static void test_win_uplink_read(void **state)
+{
+ BIO *bio = *state;
+ char buf[8] = { 0 };
+
+ expect_fread(buf, 8, FAKE_FP, 8);
+ assert_int_equal(BIO_read(bio, buf, 8), 8);
+}
+
+static void test_win_uplink_write(void **state)
+{
+ BIO *bio = *state;
+ const char buf[] = "hello";
+
+ expect_fwrite(buf, 5, FAKE_FP, 5);
+ assert_int_equal(BIO_write(bio, buf, 5), 5);
+}
+
+static void test_win_uplink_free_closes(void **state)
+{
+ BIO *bio = *state;
+
+ BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_CLOSE, NULL);
+ expect_fclose(FAKE_FP, 0);
+ BIO_free(bio);
+ *state = NULL;
+}
+
+static void test_win_uplink_get_fp_visible(void **state)
+{
+ /* an application-owned FILE (UPLINK set) is returned to the app */
+ BIO *bio = *state;
+ FILE *fp = NULL;
+
+ assert_int_equal(BIO_get_fp(bio, &fp), 1);
+ assert_ptr_equal(fp, FAKE_FP);
+}
+
+static void test_win_get_fp_internal_hidden(void **state)
+{
+ /*
+ * A FILE opened inside libcrypto (UPLINK cleared) belongs to the
+ * library CRT and must not be handed back to the application.
+ */
+ BIO *bio = *state;
+ FILE *fp = FAKE_FP;
+
+ assert_int_equal(BIO_get_fp(bio, &fp), 0);
+ assert_null(fp);
+}
+
+#if defined(_MSC_VER) && _MSC_VER >= 1900
+
+static void test_win_set_fp_stdio_clears_uplink(void **state)
+{
+ /*
+ * Safety net: passing one of the standard streams to BIO_set_fp
+ * clears the UPLINK flag, after which the mode is set with a plain
+ * _setmode call.
+ */
+ BIO *bio = BIO_new(BIO_s_file());
+
+ (void)state;
+ assert_non_null(bio);
+ assert_true(BIO_test_flags(bio, BIO_FLAGS_UPLINK_INTERNAL));
+ mocks_arm();
+ expect_set_fp_mode(stdout, 1, _O_BINARY);
+ BIO_set_fp(bio, stdout, BIO_NOCLOSE);
+ mocks_disarm();
+ assert_false(BIO_test_flags(bio, BIO_FLAGS_UPLINK_INTERNAL));
+ BIO_free(bio);
+}
+
+#endif /* _MSC_VER >= 1900 */
+
+#endif /* BIO_FLAGS_UPLINK_INTERNAL != 0 */
+
+/* main */
+
+#define FILE_WIN(name) \
+ cmocka_unit_test_setup_teardown(name, setup_internal, teardown)
+
+#if BIO_FLAGS_UPLINK_INTERNAL != 0
+#define FILE_WIN_UPLINK(name) \
+ cmocka_unit_test_setup_teardown(name, setup_uplink, teardown)
+#endif
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ /* no fixture: the gate must not touch the CRT with a fake FILE */
+ cmocka_unit_test(detour_probe),
+ FILE_WIN(test_win_eof_invalid_stream),
+ FILE_WIN(test_win_eof_not_eof),
+ FILE_WIN(test_win_eof_at_eof),
+ FILE_WIN(test_win_tell_non_disk),
+ FILE_WIN(test_win_tell_disk),
+ FILE_WIN(test_win_set_fp_text_mode),
+ FILE_WIN(test_win_set_fp_binary_mode),
+ FILE_WIN(test_win_set_filename_appends_b),
+ FILE_WIN(test_win_set_filename_appends_t),
+#if BIO_FLAGS_UPLINK_INTERNAL != 0
+ FILE_WIN_UPLINK(test_win_uplink_read),
+ FILE_WIN_UPLINK(test_win_uplink_write),
+ FILE_WIN_UPLINK(test_win_uplink_free_closes),
+ FILE_WIN_UPLINK(test_win_uplink_get_fp_visible),
+ FILE_WIN(test_win_get_fp_internal_hidden),
+#if defined(_MSC_VER) && _MSC_VER >= 1900
+ cmocka_unit_test(test_win_set_fp_stdio_clears_uplink),
+#endif
+#endif
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+ return cmocka_run_group_tests(tests, group_setup, group_teardown);
+}
+
+#endif