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