Commit 857afd02b2 for openssl.org
commit 857afd02b282314687bc1290d7d12a313d3b9f85
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Thu Jun 11 19:05:43 2026 +0200
test: add Windows unit tests setup and initial dgram test
This adds an initial setup for unit testing on Windows that allows
mocking of system functions using Detour library. This works only for
library functions and not object function like wrap so it is a bit
limited but it is still useful for BIO mocking.
An initial BIO bss_dgram test is added covering the Windows specific
parts.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Igor Ustinov <igus@openssl.foundation>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Wed Jul 15 16:00:35 2026
(Merged from https://github.com/openssl/openssl/pull/31915)
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index dd513c658b..6d8ebe39b7 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -190,6 +190,59 @@ jobs:
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
nmake test VERBOSE_FAILURE=yes HARNESS_JOBS=4
+ unit-tests:
+ runs-on: windows-2022
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ - name: checkout fuzz/corpora submodule
+ run: git submodule update --init --depth 1 fuzz/corpora
+ - name: install jom
+ if: github.repository == 'openssl/openssl'
+ run: |
+ mkdir C:\jom
+ Invoke-WebRequest -Uri "https://openssl-library.org/ci-deps/jom-1.1.7.exe" -OutFile C:\jom\jom.exe
+ $expected = (Get-Content "$env:GITHUB_WORKSPACE\.github\ci-deps.json" -Raw | ConvertFrom-Json).'jom-1.1.7.exe'
+ $actual = (Get-FileHash C:\jom\jom.exe -Algorithm SHA256).Hash
+ if ($actual -ne $expected) { throw "SHA256 mismatch for jom.exe (expected $expected, got $actual)" }
+ "C:\jom" | Out-File -FilePath "$env:GITHUB_PATH" -Append
+ - name: install jom (forks)
+ if: github.repository != 'openssl/openssl'
+ run: |
+ mkdir C:\jom
+ Invoke-WebRequest -Uri "https://download.qt.io/official_releases/jom/jom_1_1_7.zip" -OutFile C:\jom\jom.zip
+ Expand-Archive -Path C:\jom\jom.zip -DestinationPath C:\jom
+ "C:\jom" | Out-File -FilePath "$env:GITHUB_PATH" -Append
+ - name: install cmocka and detours via vcpkg
+ shell: pwsh
+ run: |
+ & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install cmocka:x64-windows-static-md detours:x64-windows-static-md
+ "VCPKG_INST=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows-static-md" | Out-File -FilePath $env:GITHUB_ENV -Append
+ - name: prepare the build directory
+ run: mkdir _build
+ - name: config
+ working-directory: _build
+ shell: cmd
+ run: |
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
+ perl ..\Configure VC-WIN64A --banner=Configured --strict-warnings no-makedepend no-asm enable-unit-tests ^
+ --with-cmocka-include=%VCPKG_INST%\include --with-cmocka-lib=%VCPKG_INST%\lib ^
+ --with-detours-include=%VCPKG_INST%\include --with-detours-lib=%VCPKG_INST%\lib
+ perl configdata.pm --dump
+ - name: build
+ working-directory: _build
+ shell: cmd
+ run: |
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
+ jom /j4 /S
+ - name: test
+ working-directory: _build
+ shell: cmd
+ run: |
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
+ jom test VERBOSE=1 TESTS=test_unit
+
minimal:
runs-on: windows-2022
steps:
diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl
index f8e44e73f7..0d4ec0f983 100644
--- a/Configurations/unix-Makefile.tmpl
+++ b/Configurations/unix-Makefile.tmpl
@@ -505,6 +505,7 @@ BIN_LDFLAGS={- join(' ', $target{bin_lflags} || (),
BIN_EX_LIBS=$(CNF_EX_LIBS) $(EX_LIBS)
CMOCKA_LIBS={- $config{cmocka_libs} // '' -}
+DETOURS_LIBS={- $config{detours_libs} // '' -}
# CPPFLAGS_Q is used for one thing only: to build up buildinf.h
CPPFLAGS_Q={- $cppflags1 =~ s|([\\"])|\\$1|g;
@@ -2024,14 +2025,14 @@ EOF
}
}
my $wrapflags = '';
- my $cmocka = '';
if (defined $unified_info{wraps}->{$args{bin}}) {
- $wrapflags = join(' ',
- map { "-Wl,--wrap=$_" }
- @{$unified_info{wraps}->{$args{bin}}});
- $wrapflags = ' ' . $wrapflags;
- $cmocka = ' $(CMOCKA_LIBS)';
+ $wrapflags = ' ' . join(' ',
+ map { "-Wl,--wrap=$_" }
+ @{$unified_info{wraps}->{$args{bin}}});
}
+ my $utlibs = $unified_info{unit_test_libs}->{$args{bin}};
+ $utlibs = $utlibs ne '' ? ' ' . $utlibs : '' if defined $utlibs;
+ $utlibs //= '';
my $linkflags = join("", map { $_." " } @linkdirs);
my $linklibs = join("", map { $_." " } @linklibs);
my $cmd = '$(CC)';
@@ -2052,7 +2053,7 @@ $bin: $deps
\$\${LDCMD:-$cmd} $cmdflags $linkflags\$(BIN_LDFLAGS)$wrapflags \\
-o $bin \\
$objs \\
- $linklibs\$(BIN_EX_LIBS)$cmocka
+ $linklibs\$(BIN_EX_LIBS)$utlibs
EOF
}
sub in2script {
diff --git a/Configurations/windows-makefile.tmpl b/Configurations/windows-makefile.tmpl
index 16fed4670d..a7f2b6652b 100644
--- a/Configurations/windows-makefile.tmpl
+++ b/Configurations/windows-makefile.tmpl
@@ -380,6 +380,9 @@ BIN_LDFLAGS={- join(' ', $target{bin_lflags} || (),
'$(CNF_LDFLAGS)', '$(LDFLAGS)') -}
BIN_EX_LIBS=$(CNF_EX_LIBS) $(EX_LIBS)
+CMOCKA_LIBS={- $config{cmocka_libs} // '' -}
+DETOURS_LIBS={- $config{detours_libs} // '' -}
+
# CPPFLAGS_Q is used for one thing only: to build up buildinf.h
CPPFLAGS_Q={- $cppflags1 =~ s|([\\"])|\\$1|g;
$cppflags2 =~ s|([\\"])|\\$1|g;
@@ -1001,11 +1004,14 @@ EOF
my $ress = join($target{ld_resp_delim}, @ress);
my $linklibs = join("", map { "$_$target{ld_resp_delim}" } @deps);
my $deps = join(" ", @objs, @ress, @deps);
+ my $utlibs = $unified_info{unit_test_libs}->{$args{bin}};
+ $utlibs = (defined $utlibs && $utlibs ne '')
+ ? "$utlibs$target{ld_resp_delim}" : '';
return <<"EOF";
$bin: $deps
IF EXIST $bin.manifest DEL /F /Q $bin.manifest
\$(LD) \$(LDFLAGS) \$(BIN_LDFLAGS) @<<
-$objs$target{ld_resp_delim}\$(LDOUTFLAG)$bin$target{ldpostoutflag}$target{ld_resp_delim}$linklibs\$(BIN_EX_LIBS)$target{ldresflag}$target{ldresflag}$ress
+$objs$target{ld_resp_delim}\$(LDOUTFLAG)$bin$target{ldpostoutflag}$target{ld_resp_delim}$utlibs$linklibs\$(BIN_EX_LIBS)$target{ldresflag}$target{ldresflag}$ress
<<
IF EXIST $bin.manifest \\
\$(MT) \$(MTFLAGS) \$(MTINFLAG)$bin.manifest \$(MTOUTFLAG)$bin
diff --git a/Configure b/Configure
index 10a218954e..5ee8597f2e 100755
--- a/Configure
+++ b/Configure
@@ -1125,6 +1125,14 @@ while (@argvcopy)
{
$withargs{cmocka_include}=$1;
}
+ elsif (/^--with-detours-lib=(.*)$/)
+ {
+ $withargs{detours_lib}=$1;
+ }
+ elsif (/^--with-detours-include=(.*)$/)
+ {
+ $withargs{detours_include}=$1;
+ }
elsif (/^--with-rand-seed=(.*)$/)
{
foreach my $x (split(m|,|, $1))
@@ -1927,15 +1935,26 @@ unless ($disabled{winstore}) {
push @{$config{openssl_other_defines}}, "OPENSSL_NO_KTLS" if ($disabled{ktls});
+# Keywords accepted in a build.info UNIT_TEST[] link set.
+my @unit_test_keywords = qw(cmocka detours);
+
unless ($disabled{"unit-tests"}) {
if ($target =~ /^linux/ || $target =~ /^BSD/) {
$config{cmocka_includes} =
$withargs{cmocka_include} ? [$withargs{cmocka_include}] : [];
$config{cmocka_libs} = $withargs{cmocka_lib}
- ? "-L$withargs{cmocka_lib} -lcmocka"
- : "-lcmocka";
+ ? "-L$withargs{cmocka_lib} -lcmocka" : "-lcmocka";
+ } elsif ($target =~ /^VC-/) {
+ $config{cmocka_includes} =
+ $withargs{cmocka_include} ? [$withargs{cmocka_include}] : [];
+ $config{cmocka_libs} = $withargs{cmocka_lib}
+ ? "/LIBPATH:$withargs{cmocka_lib} cmocka.lib" : "cmocka.lib";
+ $config{detours_includes} =
+ $withargs{detours_include} ? [$withargs{detours_include}] : [];
+ $config{detours_libs} = $withargs{detours_lib}
+ ? "/LIBPATH:$withargs{detours_lib} detours.lib" : "detours.lib";
} else {
- disable('no-wrap-support', 'unit-tests');
+ disable('no-unit-test-support', 'unit-tests');
}
}
@@ -2163,6 +2182,7 @@ if ($builder eq "unified") {
my %includes = ();
my %defines = ();
my %depends = ();
+ my %unit_tests = ();
my %wraps = ();
my %generate = ();
my %imagedocs = ();
@@ -2419,11 +2439,16 @@ if ($builder eq "unified") {
\$attributes{depends}, $+{ATTRIBS},
tokenize($expand_variables->($+{VALUE})))
if !@skip || $skip[$#skip] > 0; },
+ qr/^\s* UNIT_TEST ${index_re} \s* = \s* ${value_re} \s* $/x
+ => sub { $push_to->(\%unit_tests, $expand_variables->($+{INDEX}),
+ undef, undef,
+ tokenize($expand_variables->($+{VALUE})))
+ if !@skip || $skip[$#skip] > 0; },
qr/^\s* WRAP ${index_re} \s* = \s* ${value_re} \s* $/x
=> sub { $push_to->(\%wraps, $expand_variables->($+{INDEX}),
undef, undef,
tokenize($expand_variables->($+{VALUE})))
- if !@skip || $skip[$#skip] > 0; },
+ if !@skip || $skip[$#skip] > 0; },
qr/^\s* GENERATE ${index_re} ${attribs_re} \s* = \s* ${value_re} \s* $/x
=> sub { $push_to->(\%generate, $expand_variables->($+{INDEX}),
\$attributes{generate}, $+{ATTRIBS},
@@ -2717,6 +2742,20 @@ if ($builder eq "unified") {
}
}
+ foreach my $dest (keys %unit_tests) {
+ my $ddest = cleanfile($buildd, $dest, $blddir);
+ foreach my $kw (@{$unit_tests{$dest}}) {
+ die "***** Unknown keyword '$kw' in UNIT_TEST[$dest] at $sourced/$f\n"
+ unless grep { $_ eq $kw } @unit_test_keywords;
+ }
+ $unified_info{unit_tests}->{$ddest} =
+ [ @{$unit_tests{$dest}} ];
+ }
+ # WRAP implies cmocka unless an explicit UNIT_TEST set was given
+ foreach my $dest (keys %{$unified_info{wraps} // {}}) {
+ $unified_info{unit_tests}->{$dest} //= [ "cmocka" ];
+ }
+
foreach my $section (keys %imagedocs) {
foreach (@{$imagedocs{$section}}) {
my $imagedocs = cleanfile($buildd, $_, $blddir);
@@ -3049,11 +3088,25 @@ EOF
}
}
-# Add cmocka include path to all targets that use WRAP
-if (!$disabled{"unit-tests"} && @{$config{cmocka_includes} // []}) {
- foreach my $dest (keys %{$unified_info{wraps} // {}}) {
- push @{$unified_info{includes}->{$dest}},
- @{$config{cmocka_includes}};
+# Attach cmocka (and, on Windows, Detours) include paths to unit tests,
+# based on each test's UNIT_TEST[] link set.
+if (!$disabled{"unit-tests"}) {
+ while (my ($dest, $libs) = each %{$unified_info{unit_tests} // {}}) {
+ my %want = map { $_ => 1 } @$libs;
+ push @{$unified_info{includes}->{$dest}}, @{$config{cmocka_includes}}
+ if $want{cmocka} && @{$config{cmocka_includes} // []};
+ push @{$unified_info{includes}->{$dest}}, @{$config{detours_includes}}
+ if $want{detours} && @{$config{detours_includes} // []};
+ }
+}
+
+if (!$disabled{"unit-tests"}) {
+ foreach my $dest (sort keys %{$unified_info{unit_tests} // {}}) {
+ my %want = map { $_ => 1 } @{$unified_info{unit_tests}->{$dest}};
+ my @resolved;
+ push @resolved, '$(CMOCKA_LIBS)' if $want{cmocka} && $config{cmocka_libs};
+ push @resolved, '$(DETOURS_LIBS)' if $want{detours} && $config{detours_libs};
+ $unified_info{unit_test_libs}->{$dest} = join(' ', @resolved);
}
}
diff --git a/test/recipes/02-test_unit.t b/test/recipes/02-test_unit.t
index 0c43c28a36..d88c3dcf31 100644
--- a/test/recipes/02-test_unit.t
+++ b/test/recipes/02-test_unit.t
@@ -18,14 +18,32 @@ use OpenSSL::Test::Utils;
setup("test_unit");
my $unit_dir = bldtop_dir('test', 'unit');
+my $exeext = '';
+
+if ($^O eq 'MSWin32') {
+ $exeext = '.exe';
+ # The shared libraries (libcrypto/libssl DLLs) are only placed at the
+ # build top and copied into apps/, test/ and fuzz/. The unit test
+ # executables live in nested directories under test/unit/, so add the
+ # build top to PATH to let the loader find the DLLs.
+ $ENV{PATH} = bldtop_dir() . ';' . ($ENV{PATH} // '');
+}
my @tests = ();
if (-d $unit_dir) {
find({
wanted => sub {
- return unless -f $_ && -x $_;
- return unless $_ =~ m|/test_[^/]*$|;
- return if $_ =~ m|\.\w+$|;
+ return unless -f $_;
+ my $base = $_;
+ if ($exeext ne '') {
+ # require + strip .exe
+ return unless $base =~ s/\Q$exeext\E$//;
+ } else {
+ return unless -x $_;
+ }
+ return unless $base =~ m|/test_[^/]*$|;
+ # reject .pdb/.obj/etc
+ return if $base =~ m|\.\w+$|;
push @tests, $_;
},
no_chdir => 1,
diff --git a/test/unit/build.info b/test/unit/build.info
index bc0465dc13..ac04787457 100644
--- a/test/unit/build.info
+++ b/test/unit/build.info
@@ -1,58 +1,67 @@
-# Make sure all tests have WRAP otherwise -lcmocka is not added.
-
-PROGRAMS{noinst}=crypto/bio/test_bio_addr
-SOURCE[crypto/bio/test_bio_addr]=crypto/bio/test_bio_addr.c
-INCLUDE[crypto/bio/test_bio_addr]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bio_addr]=../../libcrypto.a
-WRAP[crypto/bio/test_bio_addr]=BIO_sock_init getnameinfo freeaddrinfo
-
-PROGRAMS{noinst}=crypto/bio/test_bio_sock
-SOURCE[crypto/bio/test_bio_sock]=crypto/bio/test_bio_sock.c
-INCLUDE[crypto/bio/test_bio_sock]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bio_sock]=../../libcrypto.a
-WRAP[crypto/bio/test_bio_sock]=getsockopt setsockopt getsockname ioctl poll \
- gethostbyname BIO_lookup BIO_socket BIO_listen BIO_closesocket \
- BIO_ADDRINFO_free BIO_accept_ex BIO_sock_should_retry \
- BIO_ADDR_hostname_string BIO_ADDR_service_string
-
-PROGRAMS{noinst}=crypto/bio/test_bio_sock2
-SOURCE[crypto/bio/test_bio_sock2]=crypto/bio/test_bio_sock2.c
-INCLUDE[crypto/bio/test_bio_sock2]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bio_sock2]=../../libcrypto.a
-WRAP[crypto/bio/test_bio_sock2]=socket connect bind listen accept close \
- getsockopt setsockopt BIO_socket_nbio BIO_sock_should_retry
-
-PROGRAMS{noinst}=crypto/bio/test_bss_acpt
-SOURCE[crypto/bio/test_bss_acpt]=crypto/bio/test_bss_acpt.c
-WRAP[crypto/bio/test_bss_acpt]=BIO_lookup BIO_socket BIO_listen \
- BIO_accept_ex BIO_sock_info BIO_sock_should_retry BIO_closesocket \
- BIO_ADDR_hostname_string BIO_ADDR_service_string
-INCLUDE[crypto/bio/test_bss_acpt]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bss_acpt]=../../libcrypto.a
-
-PROGRAMS{noinst}=crypto/bio/test_bss_conn
-SOURCE[crypto/bio/test_bss_conn]=crypto/bio/test_bss_conn.c
-WRAP[crypto/bio/test_bss_conn]=BIO_lookup BIO_socket BIO_connect \
- BIO_sock_should_retry BIO_closesocket BIO_socket_wait BIO_sock_error \
- read write
-INCLUDE[crypto/bio/test_bss_conn]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bss_conn]=../../libcrypto.a
-
-PROGRAMS{noinst}=crypto/bio/test_bss_dgram
-SOURCE[crypto/bio/test_bss_dgram]=crypto/bio/test_bss_dgram.c
-WRAP[crypto/bio/test_bss_dgram]=recvfrom sendto write getsockname \
- getpeername BIO_closesocket BIO_socket_nbio
-INCLUDE[crypto/bio/test_bss_dgram]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bss_dgram]=../../libcrypto.a
-
-PROGRAMS{noinst}=crypto/bio/test_bss_fd
-SOURCE[crypto/bio/test_bss_fd]=crypto/bio/test_bss_fd.c
-INCLUDE[crypto/bio/test_bss_fd]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bss_fd]=../../libcrypto.a
-WRAP[crypto/bio/test_bss_fd]=read write lseek close
-
-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
-INCLUDE[crypto/bio/test_bss_sock]=../../include ../../crypto/bio
-DEPEND[crypto/bio/test_bss_sock]=../../libcrypto.a
+IF[{- $config{target} =~ /^(?:linux|BSD)/ -}]
+ PROGRAMS{noinst}=crypto/bio/test_bio_addr
+ SOURCE[crypto/bio/test_bio_addr]=crypto/bio/test_bio_addr.c
+ INCLUDE[crypto/bio/test_bio_addr]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bio_addr]=../../libcrypto.a
+ WRAP[crypto/bio/test_bio_addr]=BIO_sock_init getnameinfo freeaddrinfo
+
+ PROGRAMS{noinst}=crypto/bio/test_bio_sock
+ SOURCE[crypto/bio/test_bio_sock]=crypto/bio/test_bio_sock.c
+ INCLUDE[crypto/bio/test_bio_sock]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bio_sock]=../../libcrypto.a
+ WRAP[crypto/bio/test_bio_sock]=getsockopt setsockopt getsockname ioctl poll \
+ gethostbyname BIO_lookup BIO_socket BIO_listen BIO_closesocket \
+ BIO_ADDRINFO_free BIO_accept_ex BIO_sock_should_retry \
+ BIO_ADDR_hostname_string BIO_ADDR_service_string
+
+ PROGRAMS{noinst}=crypto/bio/test_bio_sock2
+ SOURCE[crypto/bio/test_bio_sock2]=crypto/bio/test_bio_sock2.c
+ INCLUDE[crypto/bio/test_bio_sock2]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bio_sock2]=../../libcrypto.a
+ WRAP[crypto/bio/test_bio_sock2]=socket connect bind listen accept close \
+ getsockopt setsockopt BIO_socket_nbio BIO_sock_should_retry
+
+ PROGRAMS{noinst}=crypto/bio/test_bss_acpt
+ SOURCE[crypto/bio/test_bss_acpt]=crypto/bio/test_bss_acpt.c
+ WRAP[crypto/bio/test_bss_acpt]=BIO_lookup BIO_socket BIO_listen \
+ BIO_accept_ex BIO_sock_info BIO_sock_should_retry BIO_closesocket \
+ BIO_ADDR_hostname_string BIO_ADDR_service_string
+ INCLUDE[crypto/bio/test_bss_acpt]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_acpt]=../../libcrypto.a
+
+ PROGRAMS{noinst}=crypto/bio/test_bss_conn
+ SOURCE[crypto/bio/test_bss_conn]=crypto/bio/test_bss_conn.c
+ WRAP[crypto/bio/test_bss_conn]=BIO_lookup BIO_socket BIO_connect \
+ BIO_sock_should_retry BIO_closesocket BIO_socket_wait BIO_sock_error \
+ read write
+ INCLUDE[crypto/bio/test_bss_conn]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_conn]=../../libcrypto.a
+
+ PROGRAMS{noinst}=crypto/bio/test_bss_dgram
+ SOURCE[crypto/bio/test_bss_dgram]=crypto/bio/test_bss_dgram.c
+ WRAP[crypto/bio/test_bss_dgram]=recvfrom sendto write getsockname \
+ getpeername BIO_closesocket BIO_socket_nbio
+ INCLUDE[crypto/bio/test_bss_dgram]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_dgram]=../../libcrypto.a
+
+ PROGRAMS{noinst}=crypto/bio/test_bss_fd
+ SOURCE[crypto/bio/test_bss_fd]=crypto/bio/test_bss_fd.c
+ INCLUDE[crypto/bio/test_bss_fd]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_fd]=../../libcrypto.a
+ WRAP[crypto/bio/test_bss_fd]=read write lseek close
+
+ 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
+ INCLUDE[crypto/bio/test_bss_sock]=../../include ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_sock]=../../libcrypto.a
+ENDIF
+
+IF[{- $config{target} =~ /^VC-/ -}]
+ PROGRAMS{noinst}=crypto/bio/test_bss_dgram_win
+ SOURCE[crypto/bio/test_bss_dgram_win]=crypto/bio/test_bss_dgram_win.c
+ UNIT_TEST[crypto/bio/test_bss_dgram_win]=cmocka detours
+ INCLUDE[crypto/bio/test_bss_dgram_win]=../../include ../../include/internal \
+ ../../crypto/bio
+ DEPEND[crypto/bio/test_bss_dgram_win]=../../libcrypto
+ENDIF
diff --git a/test/unit/crypto/bio/test_bss_dgram_win.c b/test/unit/crypto/bio/test_bss_dgram_win.c
new file mode 100644
index 0000000000..080d91c4e2
--- /dev/null
+++ b/test/unit/crypto/bio/test_bss_dgram_win.c
@@ -0,0 +1,478 @@
+/*
+ * test/bss_dgram_win_test.c
+ *
+ * Windows-only side test for bss_dgram.c Windows-specific paths.
+ * Uses Microsoft Detours to intercept Winsock calls at runtime.
+ * Does NOT replace the normal --wrap-based bss_dgram test; it only
+ * covers branches that differ under OPENSSL_SYS_WINDOWS.
+ *
+ * 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 Winsock calls, every other result is meaningless.
+ */
+
+#include "openssl/e_os2.h"
+
+#if defined(OPENSSL_NO_SOCK) || defined(OPENSSL_NO_DGRAM) \
+ || !defined(OPENSSL_SYS_WINDOWS)
+int main(void) { return 0; }
+#else
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <windows.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>
+
+#define FAKE_SOCKET ((SOCKET)42)
+
+/*
+ * Real function pointers. After DetourAttach commits, Detours rewrites
+ * these to trampolines pointing at the original code.
+ */
+static int(WSAAPI *real_getsockname)(SOCKET, struct sockaddr *, int *)
+ = getsockname;
+static int(WSAAPI *real_getpeername)(SOCKET, struct sockaddr *, int *)
+ = getpeername;
+static int(WSAAPI *real_setsockopt)(SOCKET, int, int, const char *, int)
+ = setsockopt;
+static int(WSAAPI *real_getsockopt)(SOCKET, int, int, char *, int *)
+ = getsockopt;
+static int(WSAAPI *real_recvfrom)(SOCKET, char *, int, int,
+ struct sockaddr *, int *)
+ = recvfrom;
+
+static struct sockaddr_in g_sin;
+
+/*
+ * 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
+ * the mocks below special-case it.
+ */
+static int g_probe_active;
+static int g_probe_getsockopt_hits;
+
+/* mocks */
+
+static int WSAAPI mock_getsockname(SOCKET s, struct sockaddr *name,
+ int *namelen)
+{
+ int rc;
+
+ function_called();
+ check_expected_uint(s);
+
+ rc = mock_type(int);
+ if (rc == 0) {
+ const struct sockaddr *sa = mock_ptr_type(const struct sockaddr *);
+ int sl = mock_type(int);
+
+ assert_non_null(name);
+ assert_non_null(namelen);
+ assert_true(*namelen >= sl);
+ memcpy(name, sa, (size_t)sl);
+ *namelen = sl;
+ } else {
+ WSASetLastError(mock_type(int));
+ }
+ return rc;
+}
+
+static int WSAAPI mock_getpeername(SOCKET s, struct sockaddr *name,
+ int *namelen)
+{
+ int rc;
+
+ function_called();
+ check_expected_uint(s);
+
+ rc = mock_type(int);
+ if (rc == 0) {
+ assert_non_null(name);
+ assert_non_null(namelen);
+ assert_true(*namelen >= (int)sizeof(g_sin));
+ memcpy(name, &g_sin, sizeof(g_sin));
+ *namelen = (int)sizeof(g_sin);
+ } else {
+ WSASetLastError(mock_type(int));
+ }
+ return rc;
+}
+
+static int WSAAPI mock_setsockopt(SOCKET s, int level, int optname,
+ const char *optval, int optlen)
+{
+ int expected_int;
+ int rc;
+
+ function_called();
+ check_expected_uint(s);
+ check_expected_int(level);
+ check_expected_int(optname);
+ check_expected_int(optlen);
+
+ /* Every Windows path routed here passes a single int. */
+ assert_int_equal(optlen, (int)sizeof(int));
+ expected_int = mock_type(int);
+ assert_int_equal(*(const int *)optval, expected_int);
+
+ rc = mock_type(int);
+ if (rc == SOCKET_ERROR)
+ WSASetLastError(mock_type(int));
+ return rc;
+}
+
+static int WSAAPI mock_getsockopt(SOCKET s, int level, int optname,
+ char *optval, int *optlen)
+{
+ int out_value;
+ int rc;
+
+ /* Probe path: no expectations queued, just record and answer. */
+ if (g_probe_active) {
+ g_probe_getsockopt_hits++;
+ if (optval != NULL && optlen != NULL && *optlen >= (int)sizeof(int)) {
+ *(int *)optval = 0;
+ *optlen = (int)sizeof(int);
+ }
+ return 0;
+ }
+
+ function_called();
+ check_expected_uint(s);
+ check_expected_int(level);
+ check_expected_int(optname);
+
+ assert_non_null(optval);
+ assert_non_null(optlen);
+ assert_int_equal(*optlen, (int)sizeof(int));
+
+ out_value = mock_type(int);
+ rc = mock_type(int);
+ if (rc == 0) {
+ *(int *)optval = out_value;
+ *optlen = (int)sizeof(int);
+ } else {
+ WSASetLastError(mock_type(int));
+ }
+ return rc;
+}
+
+static int WSAAPI mock_recvfrom(SOCKET s, char *buf, int len, int flags,
+ struct sockaddr *from, int *fromlen)
+{
+ int rc;
+
+ (void)from;
+ (void)fromlen;
+
+ function_called();
+ check_expected_uint(s);
+ check_expected_ptr(buf);
+ check_expected_int(len);
+ check_expected_int(flags);
+
+ rc = mock_type(int);
+ if (rc == SOCKET_ERROR)
+ WSASetLastError(mock_type(int));
+ return rc;
+}
+
+/* expectations */
+
+static void expect_getsockname_ok(void)
+{
+ expect_function_call(mock_getsockname);
+ expect_uint_value(mock_getsockname, s, FAKE_SOCKET);
+ will_return_int(mock_getsockname, 0);
+ will_return_ptr(mock_getsockname, (const struct sockaddr *)&g_sin);
+ will_return_int(mock_getsockname, (int)sizeof(g_sin));
+}
+
+static void expect_getpeername_fail(void)
+{
+ expect_function_call(mock_getpeername);
+ expect_uint_value(mock_getpeername, s, FAKE_SOCKET);
+ will_return_int(mock_getpeername, SOCKET_ERROR);
+ will_return_int(mock_getpeername, WSAENOTCONN);
+}
+
+static void expect_setsockopt_int(int level, int optname, int value, int rc)
+{
+ expect_function_call(mock_setsockopt);
+ expect_uint_value(mock_setsockopt, s, FAKE_SOCKET);
+ expect_int_value(mock_setsockopt, level, level);
+ expect_int_value(mock_setsockopt, optname, optname);
+ expect_int_value(mock_setsockopt, optlen, (int)sizeof(int));
+ will_return_int(mock_setsockopt, value);
+ will_return_int(mock_setsockopt, rc);
+ if (rc == SOCKET_ERROR)
+ will_return(mock_setsockopt, WSAEINVAL);
+}
+
+static void expect_getsockopt_int(int level, int optname, int value, int rc)
+{
+ expect_function_call(mock_getsockopt);
+ expect_uint_value(mock_getsockopt, s, FAKE_SOCKET);
+ expect_int_value(mock_getsockopt, level, level);
+ expect_int_value(mock_getsockopt, optname, optname);
+ will_return_int(mock_getsockopt, value);
+ will_return_int(mock_getsockopt, rc);
+ if (rc == SOCKET_ERROR)
+ will_return_int(mock_getsockopt, WSAEINVAL);
+}
+
+static void expect_recvfrom_error(char *buf, int len, int flags, int wsaerr)
+{
+ expect_function_call(mock_recvfrom);
+ expect_uint_value(mock_recvfrom, s, FAKE_SOCKET);
+ expect_value(mock_recvfrom, buf, buf);
+ expect_int_value(mock_recvfrom, len, len);
+ expect_int_value(mock_recvfrom, flags, flags);
+ will_return_int(mock_recvfrom, SOCKET_ERROR);
+ will_return_int(mock_recvfrom, wsaerr);
+}
+
+/* detours */
+
+static int attach_detours(void)
+{
+ if (DetourTransactionBegin() != NO_ERROR)
+ return 0;
+ if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_getsockname, mock_getsockname) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_getpeername, mock_getpeername) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_setsockopt, mock_setsockopt) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_getsockopt, mock_getsockopt) != NO_ERROR)
+ return 0;
+ if (DetourAttach((PVOID *)&real_recvfrom, mock_recvfrom) != 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_getsockname, mock_getsockname);
+ DetourDetach((PVOID *)&real_getpeername, mock_getpeername);
+ DetourDetach((PVOID *)&real_setsockopt, mock_setsockopt);
+ DetourDetach((PVOID *)&real_getsockopt, mock_getsockopt);
+ DetourDetach((PVOID *)&real_recvfrom, mock_recvfrom);
+ return DetourTransactionCommit() == NO_ERROR;
+}
+
+/* setup / teardown */
+
+static int setup_io(void **state)
+{
+ BIO *bio = BIO_new(BIO_s_datagram());
+
+ assert_non_null(bio);
+ expect_getsockname_ok();
+ expect_getpeername_fail();
+ BIO_set_fd(bio, (int)FAKE_SOCKET, BIO_NOCLOSE);
+ *state = bio;
+ return 0;
+}
+
+static int teardown_io(void **state)
+{
+ if (*state != NULL) {
+ BIO *bio = *state;
+
+ bio->num = (int)INVALID_SOCKET;
+ bio->shutdown = BIO_NOCLOSE;
+ BIO_free(bio);
+ }
+ return 0;
+}
+
+static int group_setup(void **state)
+{
+ WSADATA wsa;
+
+ (void)state;
+ memset(&g_sin, 0, sizeof(g_sin));
+ g_sin.sin_family = AF_INET;
+ g_sin.sin_port = htons(443);
+ g_sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ assert_int_equal(WSAStartup(MAKEWORD(2, 2), &wsa), 0);
+ assert_true(attach_detours());
+ return 0;
+}
+
+static int group_teardown(void **state)
+{
+ (void)state;
+ assert_true(detach_detours());
+ WSACleanup();
+ return 0;
+}
+
+/*
+ * GATE: prove Detours intercepts a Winsock call made through the same
+ * import machinery the production library uses. If this fails, fix the
+ * linkage (e.g. DetourFindFunction on ws2_32.dll) before trusting any
+ * other test below.
+ */
+static void detour_probe(void **state)
+{
+ int val = -1;
+ int len = (int)sizeof(val);
+ int rc;
+
+ (void)state;
+ g_probe_getsockopt_hits = 0;
+ g_probe_active = 1;
+ rc = getsockopt(FAKE_SOCKET, SOL_SOCKET, SO_TYPE, (char *)&val, &len);
+ g_probe_active = 0;
+
+ assert_int_equal(rc, 0);
+ assert_int_equal(g_probe_getsockopt_hits, 1);
+}
+
+/* SO_RCVTIMEO / SO_SNDTIMEO use int milliseconds on Windows. */
+
+static void test_win_set_recv_timeout_uses_milliseconds(void **state)
+{
+ BIO *bio = *state;
+ struct timeval tv = { 1, 500000 };
+
+ expect_setsockopt_int(SOL_SOCKET, SO_RCVTIMEO, 1500, 0);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_RECV_TIMEOUT, 0, &tv), 0);
+}
+
+static void test_win_get_recv_timeout_converts_milliseconds(void **state)
+{
+ BIO *bio = *state;
+ struct timeval tv;
+
+ memset(&tv, 0, sizeof(tv));
+ expect_getsockopt_int(SOL_SOCKET, SO_RCVTIMEO, 2500, 0);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_RECV_TIMEOUT, 0, &tv),
+ (int)sizeof(tv));
+ assert_int_equal(tv.tv_sec, 2);
+ assert_int_equal(tv.tv_usec, 500000);
+}
+
+static void test_win_set_send_timeout_uses_milliseconds(void **state)
+{
+ BIO *bio = *state;
+ struct timeval tv = { 3, 250000 };
+
+ expect_setsockopt_int(SOL_SOCKET, SO_SNDTIMEO, 3250, 0);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_SEND_TIMEOUT, 0, &tv), 0);
+}
+
+static void test_win_get_send_timeout_converts_milliseconds(void **state)
+{
+ BIO *bio = *state;
+ struct timeval tv;
+
+ memset(&tv, 0, sizeof(tv));
+ expect_getsockopt_int(SOL_SOCKET, SO_SNDTIMEO, 4250, 0);
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_SEND_TIMEOUT, 0, &tv),
+ (int)sizeof(tv));
+ assert_int_equal(tv.tv_sec, 4);
+ assert_int_equal(tv.tv_usec, 250000);
+}
+
+/*
+ * GET_RECV_TIMER_EXP checks data->_errno == WSAETIMEDOUT on Windows
+ * (EAGAIN elsewhere), then consumes/clears it. WSAETIMEDOUT is treated
+ * as fatal by BIO_dgram_non_fatal_error, so we set _errno directly
+ * rather than driving it through a recvfrom retry that never sets it.
+ */
+static void test_win_recv_timer_exp_consumes_errno(void **state)
+{
+ BIO *bio = *state;
+ bio_dgram_data *data = (bio_dgram_data *)bio->ptr;
+
+ data->_errno = WSAETIMEDOUT;
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP, 0, NULL), 1);
+ /* second read reports 0: the ctrl cleared _errno */
+ assert_int_equal(
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP, 0, NULL), 0);
+}
+
+/*
+ * A fatal recvfrom error (WSAECONNRESET) must NOT set a retry flag.
+ * This exercises the real Windows recvfrom signature through Detours.
+ */
+static void test_win_recvfrom_fatal_no_retry(void **state)
+{
+ BIO *bio = *state;
+ char buf[16];
+
+ memset(buf, 0, sizeof(buf));
+ expect_recvfrom_error(buf, (int)sizeof(buf), 0, WSAECONNRESET);
+ assert_true(BIO_read(bio, buf, (int)sizeof(buf)) <= 0);
+ assert_false(BIO_should_retry(bio));
+}
+
+#if defined(IP_DONTFRAGMENT)
+/*
+ * IPv4 don't-fragment falls to IP_DONTFRAGMENT on Windows. This branch
+ * is reached only when IP_DONTFRAG is NOT defined (it is the #elif).
+ */
+static void test_win_set_dont_frag_ipv4(void **state)
+{
+ BIO *bio = *state;
+ struct sockaddr_in peer;
+
+ memset(&peer, 0, sizeof(peer));
+ peer.sin_family = AF_INET;
+ peer.sin_port = htons(4433);
+ peer.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_PEER, 0, &peer);
+
+ expect_setsockopt_int(IPPROTO_IP, IP_DONTFRAGMENT, 1, 0);
+ assert_int_equal(BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_DONT_FRAG, 1, NULL), 0);
+}
+#endif
+
+#define DG_WIN(name) \
+ cmocka_unit_test_setup_teardown(name, setup_io, teardown_io)
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ DG_WIN(detour_probe),
+ DG_WIN(test_win_set_recv_timeout_uses_milliseconds),
+ DG_WIN(test_win_get_recv_timeout_converts_milliseconds),
+ DG_WIN(test_win_set_send_timeout_uses_milliseconds),
+ DG_WIN(test_win_get_send_timeout_converts_milliseconds),
+ DG_WIN(test_win_recv_timer_exp_consumes_errno),
+ DG_WIN(test_win_recvfrom_fatal_no_retry),
+#if defined(IP_DONTFRAGMENT)
+ DG_WIN(test_win_set_dont_frag_ipv4),
+#endif
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+ return cmocka_run_group_tests(tests, group_setup, group_teardown);
+}
+
+#endif