From c48f32d915c5f8d4e41b5f90f716e0820419c289 Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Wed, 20 May 2026 16:53:12 -0700 Subject: [PATCH 01/19] Add MinGW cross-compilation support for FIPS builds --- crypto/CMakeLists.txt | 5 +- crypto/fipsmodule/CMakeLists.txt | 22 ++++ util/fipstools/inject_hash/inject_hash.go | 121 +++++++++++++++++++++- 3 files changed, 145 insertions(+), 3 deletions(-) diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index ade407902fb..54bad19f6ef 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -673,12 +673,15 @@ if(FIPS_SHARED) if (APPLE) set(INJECT_HASH_APPLE_FLAG "-apple") endif() + if (MINGW) + set(INJECT_HASH_MINGW_FLAG "-mingw") + endif() add_custom_command( TARGET crypto POST_BUILD COMMAND ${GO_EXECUTABLE} run ${AWSLC_SOURCE_DIR}/util/fipstools/inject_hash/inject_hash.go - -o $ -in-object $ ${INJECT_HASH_APPLE_FLAG} + -o $ -in-object $ ${INJECT_HASH_APPLE_FLAG} ${INJECT_HASH_MINGW_FLAG} WORKING_DIRECTORY ${AWSLC_SOURCE_DIR} ) diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index cee9476d891..980bcc0acab 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -735,6 +735,28 @@ elseif(FIPS_SHARED) DEPENDS fips_msvc_start.obj fips_msvc_end.obj bcm_library WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) + elseif(MINGW) + # The PE linker does not support linker scripts. Use the same start/end + # marker object approach as macOS by compiling the marker file twice + # (start and end), then partial-link the three pieces in order so + # BORINGSSL_bcm_text_start/end bracket all BCM code. + set(BCM_NAME bcm.o) + add_custom_command( + OUTPUT fips_gnu_start.o + COMMAND ${CMAKE_C_COMPILER} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_gnu_start.o + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + ) + add_custom_command( + OUTPUT fips_gnu_end.o + COMMAND ${CMAKE_C_COMPILER} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_gnu_end.o + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c + ) + add_custom_command( + OUTPUT ${BCM_NAME} + COMMAND ${CMAKE_LINKER} -r fips_gnu_start.o --whole-archive $ --no-whole-archive fips_gnu_end.o -o ${BCM_NAME} + DEPENDS fips_gnu_start.o fips_gnu_end.o bcm_library + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) else() set(BCM_NAME bcm.o) # fips_shared.lds does not have 'clang' prefix because we want to keep merging any changes from upstream. diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index dd351af266c..1dabd806f9b 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -12,6 +12,7 @@ import ( "crypto/sha256" "debug/elf" "debug/macho" + "debug/pe" "encoding/binary" "errors" "flag" @@ -349,7 +350,120 @@ func doWindows(objectBytes []byte, mapPath string) ([]byte, []byte, error) { return moduleText, moduleROData, nil } -func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, mapFile string) error { +func doMingw(objectBytes []byte) ([]byte, []byte, error) { + object, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return nil, nil, errors.New("failed to parse object: " + err.Error()) + } + + // Find .text and .rdata sections. Windows uses .rdata for read-only data. + var textSection, rodataSection *pe.Section + var textSectionIndex, rodataSectionIndex int + + for i, section := range object.Sections { + switch section.Name { + case ".text": + textSection = section + textSectionIndex = i + 1 + case ".rdata": + rodataSection = section + rodataSectionIndex = i + 1 + } + } + + if textSection == nil { + return nil, nil, errors.New("failed to find .text section in object") + } + + var textStart, textEnd, rodataStart, rodataEnd *uint64 + + symbols := object.Symbols + if symbols == nil { + return nil, nil, errors.New("failed to parse symbols: " + err.Error()) + } + + for _, symbol := range symbols { + switch int(symbol.SectionNumber) { + case textSectionIndex: + case rodataSectionIndex: + // rodataSectionIndex is 0 if no .rdata section was found, + // which would match undefined symbols (COFF section number 0) — skip those. + if rodataSection == nil { + continue + } + default: + continue + } + + // In COFF, symbol.Value is the offset from the start of the section, + // not an RVA. + offset := uint64(symbol.Value) + + switch symbol.Name { + case "BORINGSSL_bcm_text_start": + if textStart != nil { + return nil, nil, errors.New("duplicate start symbol found") + } + textStart = &offset + case "BORINGSSL_bcm_text_end": + if textEnd != nil { + return nil, nil, errors.New("duplicate end symbol found") + } + textEnd = &offset + case "BORINGSSL_bcm_rodata_start": + if rodataStart != nil { + return nil, nil, errors.New("duplicate rodata start symbol found") + } + rodataStart = &offset + case "BORINGSSL_bcm_rodata_end": + if rodataEnd != nil { + return nil, nil, errors.New("duplicate rodata end symbol found") + } + rodataEnd = &offset + } + } + + if textStart == nil || textEnd == nil { + return nil, nil, errors.New("could not find .text module boundaries in object") + } + + if (rodataStart == nil) != (rodataSection == nil) { + return nil, nil, errors.New("rodata start marker inconsistent with rodata section presence") + } + + if (rodataStart != nil) != (rodataEnd != nil) { + return nil, nil, errors.New("rodata marker presence inconsistent") + } + + if max := uint64(textSection.Size); *textStart > max || *textStart > *textEnd || *textEnd > max { + return nil, nil, fmt.Errorf("invalid module .text boundaries: start: %x, end: %x, max: %x", *textStart, *textEnd, max) + } + + if rodataStart != nil { + if max := uint64(rodataSection.Size); *rodataStart > max || *rodataStart > *rodataEnd || *rodataEnd > max { + return nil, nil, fmt.Errorf("invalid module .rdata boundaries: start: %x, end: %x, max: %x", *rodataStart, *rodataEnd, max) + } + } + + text, err := textSection.Data() + if err != nil { + return nil, nil, errors.New("failed to read .text data: " + err.Error()) + } + moduleText := text[*textStart:*textEnd] + + var moduleROData []byte + if rodataStart != nil { + rodata, err := rodataSection.Data() + if err != nil { + return nil, nil, errors.New("failed to read .rdata data: " + err.Error()) + } + moduleROData = rodata[*rodataStart:*rodataEnd] + } + + return moduleText, moduleROData, nil +} + +func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, mapFile string, mingw bool) error { var objectBytes []byte var isStatic bool var perm os.FileMode @@ -422,6 +536,8 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma moduleText, moduleROData, err = doWindows(objectBytes, mapFile) } else if appleOS { moduleText, moduleROData, err = doAppleOS(objectBytes) + } else if mingw == true { + moduleText, moduleROData, err = doMingw(objectBytes) } else { moduleText, moduleROData, err = doLinux(objectBytes, isStatic) } @@ -471,10 +587,11 @@ func main() { appleOS := flag.Bool("apple", false, "Whether the FIPS module is built for macOS/iOS or not.") windowsOS := flag.Bool("windows", false, "Whether the FIPS module is built for Windows or not.") mapFile := flag.String("map", "", "Path to linker .map file (required for Windows)") + mingw := flag.Bool("mingw", false, "Whether the FIPS module is built for a Windows MinGW toolchain or not.") flag.Parse() - if err := do(*outPath, *oInput, *arInput, *appleOS, *windowsOS, *mapFile); err != nil { + if err := do(*outPath, *oInput, *arInput, *appleOS, *windowsOS, *mapFile, *mingw); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } From 33689c2869f29f1479e4a603d08482524a20a459 Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Wed, 20 May 2026 16:58:12 -0700 Subject: [PATCH 02/19] Move MinGW thread static linking to top-level CMakeLists.txt --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2305bdd7f8a..34c74bddf94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1369,6 +1369,12 @@ endif() # via Web Workers and SharedArrayBuffer, configured through compiler flags. if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) + if(MINGW) + # Statically link libwinpthread so the FIPS DLL has no runtime dependency + # on libwinpthread-1.dll, which is not present on Windows without MinGW. + set_property(TARGET Threads::Threads PROPERTY + INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") + endif() set(AWSLC_LINK_THREADS TRUE) # CMAKE_THREAD_LIBS_INIT contains the actual linker flags (e.g., -lpthread) # set by find_package(Threads). Use this for pkgconfig instead of the From e22aaf800c09103ae7ae16c941cc4612d78ce5e9 Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Thu, 21 May 2026 08:59:10 -0700 Subject: [PATCH 03/19] Fix jitterentropy MinGW shared build: add BORINGSSL_IMPLEMENTATION --- third_party/jitterentropy/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/third_party/jitterentropy/CMakeLists.txt b/third_party/jitterentropy/CMakeLists.txt index ce4825fb98a..be361f02156 100644 --- a/third_party/jitterentropy/CMakeLists.txt +++ b/third_party/jitterentropy/CMakeLists.txt @@ -20,7 +20,11 @@ if(WIN32) if(MSVC) set(JITTER_COMPILE_FLAGS "/Od /W4 /DYNAMICBASE /DAWSLC") else() - set(JITTER_COMPILE_FLAGS "-DAWSLC -fwrapv --param ssp-buffer-size=4 -fvisibility=hidden -Wcast-align -Wmissing-field-initializers -Wshadow -Wswitch-enum -Wextra -Wall -pedantic -O0 -fwrapv -Wconversion") + # BORINGSSL_IMPLEMENTATION signals that jitterentropy is being compiled + # as part of the library itself, not as an external consumer. Without it, + # on MinGW shared builds, OPENSSL_EXPORT resolves to __declspec(dllimport) + # causing undefined __imp_ references at link time. + set(JITTER_COMPILE_FLAGS "-DAWSLC -DBORINGSSL_IMPLEMENTATION -fwrapv --param ssp-buffer-size=4 -fvisibility=hidden -Wcast-align -Wmissing-field-initializers -Wshadow -Wswitch-enum -Wextra -Wall -pedantic -O0 -fwrapv -Wconversion") endif() else() set(CMAKE_POSITION_INDEPENDENT_CODE true) From 3def106e92c88e1b9134fe89eb5ac259845751a7 Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Thu, 21 May 2026 14:30:00 -0700 Subject: [PATCH 04/19] Fix inject_hash.go: error message and rodata section check --- util/fipstools/inject_hash/inject_hash.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index 1dabd806f9b..d8421ffedec 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -379,7 +379,7 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { symbols := object.Symbols if symbols == nil { - return nil, nil, errors.New("failed to parse symbols: " + err.Error()) + return nil, nil, errors.New("no symbol table found in object") } for _, symbol := range symbols { @@ -427,8 +427,8 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { return nil, nil, errors.New("could not find .text module boundaries in object") } - if (rodataStart == nil) != (rodataSection == nil) { - return nil, nil, errors.New("rodata start marker inconsistent with rodata section presence") + if rodataStart != nil && rodataSection == nil { + return nil, nil, errors.New("rodata start marker found but no .rdata section present") } if (rodataStart != nil) != (rodataEnd != nil) { From a380e9d9822c328d2fad8d114c81e3aef75f90ee Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Thu, 21 May 2026 15:39:56 -0700 Subject: [PATCH 05/19] Clarify jitterentropy BORINGSSL_IMPLEMENTATION comment --- third_party/jitterentropy/CMakeLists.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/third_party/jitterentropy/CMakeLists.txt b/third_party/jitterentropy/CMakeLists.txt index be361f02156..7d5326b184e 100644 --- a/third_party/jitterentropy/CMakeLists.txt +++ b/third_party/jitterentropy/CMakeLists.txt @@ -20,10 +20,9 @@ if(WIN32) if(MSVC) set(JITTER_COMPILE_FLAGS "/Od /W4 /DYNAMICBASE /DAWSLC") else() - # BORINGSSL_IMPLEMENTATION signals that jitterentropy is being compiled - # as part of the library itself, not as an external consumer. Without it, - # on MinGW shared builds, OPENSSL_EXPORT resolves to __declspec(dllimport) - # causing undefined __imp_ references at link time. + # Without BORINGSSL_IMPLEMENTATION, on MinGW shared builds, + # symbols are treated as imported rather than exported, causing + # undefined references at link time. set(JITTER_COMPILE_FLAGS "-DAWSLC -DBORINGSSL_IMPLEMENTATION -fwrapv --param ssp-buffer-size=4 -fvisibility=hidden -Wcast-align -Wmissing-field-initializers -Wshadow -Wswitch-enum -Wextra -Wall -pedantic -O0 -fwrapv -Wconversion") endif() else() From c94856ca67b0be04e2037b5116474fa5b64d40a1 Mon Sep 17 00:00:00 2001 From: Will-Low <26700668+Will-Low@users.noreply.github.com> Date: Thu, 21 May 2026 15:52:45 -0700 Subject: [PATCH 06/19] Remove comment from libwinpthread static link --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 34c74bddf94..7b533cb34dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1370,8 +1370,6 @@ endif() if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) if(MINGW) - # Statically link libwinpthread so the FIPS DLL has no runtime dependency - # on libwinpthread-1.dll, which is not present on Windows without MinGW. set_property(TARGET Threads::Threads PROPERTY INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") endif() From 0c8dc440c2e9e8136a4a3272d2d4cbe51f9732e7 Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Tue, 9 Jun 2026 16:44:51 -0400 Subject: [PATCH 07/19] Address PR feedback for MinGW FIPS support - inject_hash.go: clarify COFF section filter (avoid fallthrough-looking switch) and make the rodata/.rdata consistency check bidirectional, matching doLinux; use idiomatic boolean test for the mingw flag. - crypto/fipsmodule/CMakeLists.txt: rename FIPS marker objects to fips_mingw_{start,end}.o and forward CMAKE_C_FLAGS to the marker compile commands for cross-compilation. - CMakeLists.txt: gate static winpthread linking on FIPS, and disable -ffunction-sections/-fdata-sections for MinGW FIPS_SHARED builds. --- CMakeLists.txt | 13 +++++++++---- crypto/fipsmodule/CMakeLists.txt | 17 +++++++++++------ util/fipstools/inject_hash/inject_hash.go | 20 ++++++++------------ 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b533cb34dd..414ee448f49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1063,9 +1063,11 @@ if(FIPS) add_definitions(-DBORINGSSL_FIPS) endif() -# The Android CMake files set -ffunction-sections and -fdata-sections, which -# is incompatible with FIPS_SHARED. -if(FIPS_SHARED AND ANDROID) +# -ffunction-sections / -fdata-sections are incompatible with FIPS_SHARED. +# The Android CMake files set these flags. Although the MinGW build does not +# currently enable them, defensively disabling them here to keep the FIPS +# integrity check robust if added in the future. +if(FIPS_SHARED AND (ANDROID OR MINGW)) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-function-sections -fno-data-sections") set(CMAKE_CXX_FLAGS @@ -1369,7 +1371,10 @@ endif() # via Web Workers and SharedArrayBuffer, configured through compiler flags. if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) - if(MINGW) + if(MINGW AND FIPS) + # The FIPS shared module on MinGW must contain its own copy of the + # winpthread implementation so that the integrity-checked DLL does not + # depend on an external libwinpthread DLL at runtime. set_property(TARGET Threads::Threads PROPERTY INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") endif() diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index 980bcc0acab..686b2f84ebb 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -741,20 +741,25 @@ elseif(FIPS_SHARED) # (start and end), then partial-link the three pieces in order so # BORINGSSL_bcm_text_start/end bracket all BCM code. set(BCM_NAME bcm.o) + # These custom commands invoke the compiler directly (outside CMake's normal + # target compilation), so we must forward CMAKE_C_FLAGS manually. This is + # necessary for cross-compilation where the flags contain --target and + # system include paths (e.g. clang targeting MinGW). + separate_arguments(FIPS_MARKER_C_FLAGS NATIVE_COMMAND "${CMAKE_C_FLAGS}") add_custom_command( - OUTPUT fips_gnu_start.o - COMMAND ${CMAKE_C_COMPILER} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_gnu_start.o + OUTPUT fips_mingw_start.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) add_custom_command( - OUTPUT fips_gnu_end.o - COMMAND ${CMAKE_C_COMPILER} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_gnu_end.o + OUTPUT fips_mingw_end.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_mingw_end.o DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) add_custom_command( OUTPUT ${BCM_NAME} - COMMAND ${CMAKE_LINKER} -r fips_gnu_start.o --whole-archive $ --no-whole-archive fips_gnu_end.o -o ${BCM_NAME} - DEPENDS fips_gnu_start.o fips_gnu_end.o bcm_library + COMMAND ${CMAKE_LINKER} -r fips_mingw_start.o --whole-archive $ --no-whole-archive fips_mingw_end.o -o ${BCM_NAME} + DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) else() diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index d8421ffedec..e2742ad6c84 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -383,15 +383,11 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { } for _, symbol := range symbols { - switch int(symbol.SectionNumber) { - case textSectionIndex: - case rodataSectionIndex: - // rodataSectionIndex is 0 if no .rdata section was found, - // which would match undefined symbols (COFF section number 0) — skip those. - if rodataSection == nil { - continue - } - default: + // Only consider symbols defined in .text or, if present, .rdata. COFF + // section numbers are 1-based; undefined symbols carry section number 0, + // which must not be mistaken for a real section when .rdata is absent. + sn := int(symbol.SectionNumber) + if sn != textSectionIndex && (rodataSection == nil || sn != rodataSectionIndex) { continue } @@ -427,8 +423,8 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { return nil, nil, errors.New("could not find .text module boundaries in object") } - if rodataStart != nil && rodataSection == nil { - return nil, nil, errors.New("rodata start marker found but no .rdata section present") + if (rodataStart == nil) != (rodataSection == nil) { + return nil, nil, errors.New("rodata start marker inconsistent with .rdata section presence") } if (rodataStart != nil) != (rodataEnd != nil) { @@ -536,7 +532,7 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma moduleText, moduleROData, err = doWindows(objectBytes, mapFile) } else if appleOS { moduleText, moduleROData, err = doAppleOS(objectBytes) - } else if mingw == true { + } else if mingw { moduleText, moduleROData, err = doMingw(objectBytes) } else { moduleText, moduleROData, err = doLinux(objectBytes, isStatic) From a56008001d18415d8ad7ef21c1e95fb61295a14e Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Wed, 10 Jun 2026 08:59:45 -0400 Subject: [PATCH 08/19] Add cross-MinGW FIPS shared CI job Adds a cross-mingw-fips job (Ubuntu -> x86_64-w64-mingw32) that builds a FIPS shared library and runs the FIPS integrity self-test under Wine, exercising the PE/COFF inject_hash path and the MinGW CMake plumbing. - tests/ci/run_cross_mingw_fips_tests.sh: new script modeled on run_cross_mingw_tests.sh; builds with -DFIPS=1 -DBUILD_SHARED_LIBS=1, sets WINEPATH for the crypto/ssl and mingw runtime DLLs, and runs bssl isfips, test_fips, crypto_test, and ssl_test. - .github/workflows/windows-alt.yml: add cross-mingw-fips job reusing the cross-mingw toolchain/Wine install block. --- .github/workflows/windows-alt.yml | 26 +++++++ tests/ci/run_cross_mingw_fips_tests.sh | 103 +++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100755 tests/ci/run_cross_mingw_fips_tests.sh diff --git a/.github/workflows/windows-alt.yml b/.github/workflows/windows-alt.yml index bfdac615fb3..4c67ff26496 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -207,6 +207,32 @@ jobs: - name: x86_64-w64-mingw32 Build/Test run: ./tests/ci/run_cross_mingw_tests.sh x86_64 w64-mingw32 "-DCMAKE_BUILD_TYPE=Release" + cross-mingw-fips: + if: github.repository_owner == 'aws' + runs-on: ubuntu-22.04 + steps: + - name: Install Tools + run: | + set -ex + sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none + sudo apt-get install --assume-yes --no-install-recommends software-properties-common + sudo add-apt-repository --yes ppa:longsleep/golang-backports + sudo dpkg --add-architecture i386 + sudo mkdir -pm755 /etc/apt/keyrings + sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key + sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources + sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none + sudo apt-get install --assume-yes --no-install-recommends build-essential cmake golang-go nasm clang wget mingw-w64 + sudo apt-get install --assume-yes --install-recommends winehq-stable wine-binfmt + sudo update-binfmts --display + sudo update-binfmts --disable + sudo update-binfmts --enable wine + sudo update-binfmts --display + sudo rm -rf /tmp/* + - uses: actions/checkout@v6 + - name: x86_64-w64-mingw32 FIPS shared Build/Test + run: + ./tests/ci/run_cross_mingw_fips_tests.sh x86_64 w64-mingw32 "-DCMAKE_BUILD_TYPE=Release" cross-clang-cl-ninja: # Cross-compile from Linux to x86_64-pc-windows-msvc with Ninja + clang-cl, # mirroring the default aws-lc-rs setup. This catches command-line quoting diff --git a/tests/ci/run_cross_mingw_fips_tests.sh b/tests/ci/run_cross_mingw_fips_tests.sh new file mode 100755 index 00000000000..12d189e4c01 --- /dev/null +++ b/tests/ci/run_cross_mingw_fips_tests.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +set -ex + +# Cross-compiles a FIPS *shared* (DLL) build of AWS-LC for Windows using the +# mingw-w64 toolchain, then exercises the FIPS integrity self-test under Wine. +# This is the cross-compilation analogue of the native clang-cl FIPS shared +# coverage, and validates the PE/COFF hash-injection path in inject_hash.go as +# well as the MinGW-specific CMake plumbing (bcm marker objects, statically +# linked winpthread). + +TARGET_CPU="${1}" +TARGET_PLATFORM="${2}" +BUILD_OPTIONS=("${@:3:5}") +GCC_VERSION="10" +THREAD_MODEL="posix" + +if [ "${#BUILD_OPTIONS[@]}" -lt 1 ]; then + echo "Must pass build parameters" + exit 1 +fi + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SCRIPT_DIR="$(readlink -f "${SCRIPT_DIR}")" + +source "${SCRIPT_DIR}/common_posix_setup.sh" +source "${SCRIPT_DIR}/gtest_util.sh" + +# Assumes script is executed from the root of aws-lc directory +SCRATCH_FOLDER="${SYS_ROOT}/SCRATCH_${TARGET_CPU}_FIPS" + +if [ -e "${SCRATCH_FOLDER}" ]; then + # Some directories in the archive lack write permission, preventing deletion of files + chmod +w -R "${SCRATCH_FOLDER}" + rm -rf "${SCRATCH_FOLDER:?}" +fi +mkdir -p "${SCRATCH_FOLDER}" + +pushd "${SCRATCH_FOLDER}" + +# Note: unlike the static cross-mingw build, we do NOT set +# CMAKE_EXE_LINKER_FLAGS=--static here. A FIPS shared build links the test +# executables against the crypto DLL, so the executables (and the DLL) must be +# able to locate their runtime dependencies at load time. winpthread is pulled +# statically into the crypto DLL itself (see the MINGW AND FIPS branch in the +# top-level CMakeLists.txt); the remaining mingw runtime DLLs are made +# discoverable through WINEPATH below. +cat < ${TARGET_CPU}-${TARGET_PLATFORM}.cmake +# Specify the target system +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR ${TARGET_CPU}) + +# Specify the cross-compiler +set(CMAKE_C_COMPILER /usr/bin/${TARGET_CPU}-${TARGET_PLATFORM}-gcc-${THREAD_MODEL}) +set(CMAKE_CXX_COMPILER /usr/bin/${TARGET_CPU}-${TARGET_PLATFORM}-g++-${THREAD_MODEL}) + +# Specify the sysroot for the target system +set(CMAKE_SYSROOT /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/) +set(CMAKE_SYSTEM_INCLUDE_PATH /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/include) + +set(CMAKE_FIND_ROOT_PATH /usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}/) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_GENERATOR Ninja) +EOF + +cat ${TARGET_CPU}-${TARGET_PLATFORM}.cmake + +# Make the cross-compiled crypto/ssl DLLs and the mingw runtime DLLs +# (libstdc++, libgcc_s, libwinpthread) discoverable by Wine when it loads the +# test executables. WINEPATH is a colon-separated list of Unix paths. +export WINEPATH="${BUILD_ROOT}/crypto:${BUILD_ROOT}/ssl:/usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}:/usr/${TARGET_CPU}-${TARGET_PLATFORM}/lib" +# Keep Wine quiet and non-interactive in CI. +export WINEDEBUG="-all" + +echo "Testing AWS-LC FIPS shared library for ${TARGET_CPU}." + +for BO in "${BUILD_OPTIONS[@]}"; do + run_build \ + -DCMAKE_TOOLCHAIN_FILE="${SCRATCH_FOLDER}/${TARGET_CPU}-${TARGET_PLATFORM}.cmake" \ + -DFIPS=1 \ + -DBUILD_SHARED_LIBS=1 \ + ${BO} + + # The module's FIPS status can be verified by 'tool/bssl isfips'. This must + # report FIPS mode is enabled for a successful shared FIPS build. + module_status=$(${BUILD_ROOT}/tool/bssl.exe isfips) + [[ "${module_status}" == "1" ]] || { echo >&2 "FIPS Mode validation failed."; exit 1; } + + # test_fips exercises the power-on self-test, including the runtime integrity + # check that recomputes the module hash. This is the primary validation that + # inject_hash.go wrote a correct hash into the PE/COFF DLL. + ${BUILD_ROOT}/util/fipstools/test_fips.exe + + # Confirm the module loads and basic functionality works through the DLL. + shard_gtest "${BUILD_ROOT}/crypto/crypto_test.exe --gtest_also_run_disabled_tests" + shard_gtest ${BUILD_ROOT}/ssl/ssl_test.exe +done +popd From 1f3d795b2310af776f8d7f56c9bf9fb778112ddb Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Thu, 18 Jun 2026 12:32:32 -0400 Subject: [PATCH 09/19] Fix MinGW FIPS shared build compilation under -Werror Two pre-existing latent issues break the FIPS shared build the moment it is compiled with the MinGW GCC toolchain: - bcm.c gated its MSVC section pragmas (#pragma code_seg/data_seg/const_seg/ bss_seg/section) on OPENSSL_WINDOWS, which is also defined for MinGW. These pragmas are MSVC-only, so MinGW GCC trips -Werror=unknown-pragmas. Gate them on _MSC_VER instead, matching fips_shared_library_marker.c; MinGW places the module via marker objects partial-linked with 'ld -r', so the pragmas are unnecessary there. - The MinGW marker objects are compiled by forwarding the full CMAKE_C_FLAGS, which includes -Werror -Wmissing-prototypes. The marker file intentionally defines BORINGSSL_bcm_text_start/end without prototypes, so compile with -w (the MSVC marker path already does this). Surfaced by the cross-mingw-fips CI job. --- crypto/fipsmodule/CMakeLists.txt | 4 ++-- crypto/fipsmodule/bcm.c | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index 686b2f84ebb..fdc0fe65fb7 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -748,12 +748,12 @@ elseif(FIPS_SHARED) separate_arguments(FIPS_MARKER_C_FLAGS NATIVE_COMMAND "${CMAKE_C_FLAGS}") add_custom_command( OUTPUT fips_mingw_start.o - COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) add_custom_command( OUTPUT fips_mingw_end.o - COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_mingw_end.o + COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_mingw_end.o DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) add_custom_command( diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index 00a58c284be..e038a09b5c0 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -15,8 +15,10 @@ // On Windows place the bcm code in a specific section that uses Grouped Sections // to control the order. $b section will place bcm in between the start/end markers -// which are in $a and $z. -#if defined(BORINGSSL_FIPS) && defined(OPENSSL_WINDOWS) +// which are in $a and $z. These #pragma directives are MSVC-specific, so this is +// gated on _MSC_VER (matches fips_shared_library_marker.c); the MinGW FIPS build +// instead brackets the module via marker objects partial-linked with `ld -r`. +#if defined(BORINGSSL_FIPS) && defined(_MSC_VER) #pragma code_seg(".fipstx$b") #pragma data_seg(".fipsda$b") #pragma const_seg(".fipsco$b") From e719f56f9d22b3f3ebd259bb292056ca19730bcf Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Thu, 18 Jun 2026 12:32:58 -0400 Subject: [PATCH 10/19] Keep MinGW FIPS module code within the integrity markers and enforce it At -O2+ GCC partitions functions into hot/cold paths, emitting cold code into separate .text.unlikely/.text.{exit,startup} sections. On ELF the FIPS linker script (gcc_fips_shared.lds) gathers these between the BORINGSSL_bcm_text_start/ end markers, but the PE/COFF MinGW path partial-links with a plain 'ld -r' (PE linkers do not support linker scripts), so any split-out section lands outside the integrity-checked region in the final DLL. The self-test would still pass, since injection and the runtime check measure the same window, so the gap is silent. - Disable the reordering/partitioning for FIPS_SHARED MinGW GCC builds so all module code stays in one contiguous .text bracketed by the markers. - Add a build-time guard (check_fips_mingw_sections.cmake) that inspects the partial-linked module object and fails the build if any split-out .text/.rdata section remains. This is the MinGW analogue of the /DISCARD/ net in the ELF linker scripts and turns the otherwise-silent coverage gap into a hard error. --- CMakeLists.txt | 12 +++ crypto/fipsmodule/CMakeLists.txt | 24 +++++- .../check_fips_mingw_sections.cmake | 74 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 crypto/fipsmodule/check_fips_mingw_sections.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 414ee448f49..a9a10e464c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1074,6 +1074,18 @@ if(FIPS_SHARED AND (ANDROID OR MINGW)) "${CMAKE_CXX_FLAGS} -fno-function-sections -fno-data-sections") endif() +# GCC (-O2+) splits cold code into separate .text.unlikely/.text.* sections. +# On ELF, gcc_fips_shared.lds gathers these between the bcm text markers, but +# the MinGW path partial-links with a plain `ld -r` (PE has no linker script +# support), so any split-out section would land outside the integrity-checked +# region. Keep all module code in one contiguous .text bracketed by the markers. +if(FIPS_SHARED AND MINGW AND GCC) + set(CMAKE_C_FLAGS + "${CMAKE_C_FLAGS} -fno-reorder-functions -fno-reorder-blocks-and-partition") + set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} -fno-reorder-functions -fno-reorder-blocks-and-partition") +endif() + if(OPENSSL_SMALL) add_definitions(-DOPENSSL_SMALL) # OPENSSL_SMALL implies disabling AVX-512 optimizations on x86_64 for size. diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index fdc0fe65fb7..0ee8b6a789a 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -746,6 +746,25 @@ elseif(FIPS_SHARED) # necessary for cross-compilation where the flags contain --target and # system include paths (e.g. clang targeting MinGW). separate_arguments(FIPS_MARKER_C_FLAGS NATIVE_COMMAND "${CMAKE_C_FLAGS}") + + # Resolve an objdump for the target toolchain. It is used below to verify + # that the assembled module object keeps all code/rodata in the single + # .text/.rdata sections bracketed by the integrity markers. + set(FIPS_MINGW_OBJDUMP "${CMAKE_OBJDUMP}") + if(NOT FIPS_MINGW_OBJDUMP) + # Derive from the linker (e.g. -ld -> -objdump). + get_filename_component(FIPS_LD_DIR "${CMAKE_LINKER}" DIRECTORY) + get_filename_component(FIPS_LD_NAME "${CMAKE_LINKER}" NAME) + string(REGEX REPLACE "ld(\\.exe)?$" "objdump" FIPS_OD_NAME "${FIPS_LD_NAME}") + find_program(FIPS_MINGW_OBJDUMP NAMES "${FIPS_OD_NAME}" objdump llvm-objdump + HINTS "${FIPS_LD_DIR}") + endif() + if(NOT FIPS_MINGW_OBJDUMP) + message(FATAL_ERROR + "FIPS MinGW build: could not find objdump to verify the module " + "integrity section layout. Set CMAKE_OBJDUMP to the target objdump.") + endif() + add_custom_command( OUTPUT fips_mingw_start.o COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o @@ -759,7 +778,10 @@ elseif(FIPS_SHARED) add_custom_command( OUTPUT ${BCM_NAME} COMMAND ${CMAKE_LINKER} -r fips_mingw_start.o --whole-archive $ --no-whole-archive fips_mingw_end.o -o ${BCM_NAME} - DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library + # Fail the build if GCC split any module code/rodata into sections that + # would fall outside the integrity markers in the final DLL. + COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${FIPS_MINGW_OBJDUMP} -DBCM_OBJECT=${BCM_NAME} -P ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake + DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) else() diff --git a/crypto/fipsmodule/check_fips_mingw_sections.cmake b/crypto/fipsmodule/check_fips_mingw_sections.cmake new file mode 100644 index 00000000000..b9dda6466a9 --- /dev/null +++ b/crypto/fipsmodule/check_fips_mingw_sections.cmake @@ -0,0 +1,74 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +# Verifies that the partial-linked FIPS module object (bcm.o) does not contain +# any code or read-only data in sections that would fall outside the +# [BORINGSSL_bcm_text_start, BORINGSSL_bcm_text_end] (and rodata) integrity +# markers in the final DLL. +# +# Background: at -O2+ GCC partitions functions into hot/cold paths and moves +# cold code into .text.unlikely (and constructors/destructors into +# .text.startup/.text.exit). On ELF the FIPS linker script (gcc_fips_shared.lds) +# gathers these back between the markers and /DISCARD/s stray sections. The +# PE/COFF MinGW path has no such net: it partial-links with a plain `ld -r` and +# relies on GCC not emitting split sections (see -fno-reorder-functions / +# -fno-reorder-blocks-and-partition in the top-level CMakeLists.txt). If a split +# section ever reappears, the module would still pass its self-test (injection +# and the runtime check measure the same window) but would silently leave that +# code/data outside the integrity check. This guard turns that silent gap into a +# hard build failure. +# +# Required arguments (passed via -D): +# OBJDUMP - path to a binutils/llvm objdump for the target +# BCM_OBJECT - path to the partial-linked module object to inspect + +if(NOT OBJDUMP) + message(FATAL_ERROR "FIPS MinGW section check: OBJDUMP not provided") +endif() +if(NOT BCM_OBJECT) + message(FATAL_ERROR "FIPS MinGW section check: BCM_OBJECT not provided") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -h "${BCM_OBJECT}" + OUTPUT_VARIABLE objdump_output + ERROR_VARIABLE objdump_error + RESULT_VARIABLE objdump_result +) +if(NOT objdump_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW section check: failed to run '${OBJDUMP} -h ${BCM_OBJECT}'\n${objdump_error}") +endif() + +# Extract section names (the second whitespace-delimited column of `objdump -h`) +# and flag any that indicate split-out module code or data. Dollar-suffixed +# grouped sections such as .rdata$zzz are benign compiler metadata that the +# linker collates into .rdata, so only the dot-form splits are forbidden. +string(REPLACE "\n" ";" objdump_lines "${objdump_output}") +set(forbidden_sections "") +foreach(line IN LISTS objdump_lines) + string(REGEX REPLACE "^[ \t]+" "" line "${line}") + string(REGEX REPLACE "[ \t]+" ";" fields "${line}") + list(LENGTH fields field_count) + if(field_count GREATER 1) + list(GET fields 1 section_name) + if(section_name MATCHES "^\\.text\\." OR section_name MATCHES "^\\.rdata\\.") + list(APPEND forbidden_sections "${section_name}") + endif() + endif() +endforeach() + +if(forbidden_sections) + list(REMOVE_DUPLICATES forbidden_sections) + string(REPLACE ";" ", " forbidden_pretty "${forbidden_sections}") + message(FATAL_ERROR + "FIPS integrity check would be incomplete: '${BCM_OBJECT}' contains " + "split-out section(s) outside the integrity markers: ${forbidden_pretty}.\n" + "All module code must live in a single contiguous .text (and rodata in " + ".rdata) bracketed by BORINGSSL_bcm_text_start/end. Ensure the module is " + "built with -fno-reorder-functions -fno-reorder-blocks-and-partition " + "(and -fno-function-sections -fno-data-sections); see the FIPS_SHARED " + "MinGW blocks in the top-level CMakeLists.txt.") +endif() + +message(STATUS "FIPS MinGW section check passed for ${BCM_OBJECT}") From 8dd6bcd2d7da12ce0def4fe2d45638dedc7cc61e Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 14:38:03 +0000 Subject: [PATCH 11/19] Make the MinGW FIPS shared integrity check correct under ASLR The FIPS integrity hash is injected over the crypto DLL's preferred-base on-disk bytes, but a PE DLL's hashed .text/.rdata windows contain DIR64 base relocations (the .refptr pointer table). Under ASLR the loader rewrites those pointers to the actual load address, so a naive in-memory hash no longer matches the injected value. To reconcile this, the runtime integrity check walks the PE base relocation table and, for every relocation inside the hashed windows, un-applies the ASLR load delta before hashing, recovering the preferred-base bytes that inject_hash measured. Computing that load delta requires the link-time preferred image base. It cannot be read from the in-memory PE header at runtime because the Windows loader overwrites OptionalHeader.ImageBase with the actual load address once the module is relocated. Instead, inject_hash.go records the preferred base in BORINGSSL_bcm_preferred_base, a plain integer (so it carries no relocation of its own) living in .fipshash outside the hashed module boundary. The runtime reads it to compute load_delta = actual_base - preferred_base. The expected-hash storage (BORINGSSL_bcm_text_hash) is likewise kept in its own .fipshash section so it is never part of the integrity input, which would otherwise create an unsatisfiable circular dependency. --- crypto/CMakeLists.txt | 8 + crypto/fipsmodule/CMakeLists.txt | 25 ++- crypto/fipsmodule/bcm.c | 155 +++++++++++++++++- .../check_fips_mingw_sections.cmake | 40 +++-- .../fipsmodule/fips_shared_library_marker.c | 7 + crypto/fipsmodule/fips_shared_support.c | 42 ++++- .../fipsmodule/merge_fips_mingw_rodata.cmake | 97 +++++++++++ util/fipstools/inject_hash/inject_hash.go | 40 +++++ 8 files changed, 383 insertions(+), 31 deletions(-) create mode 100644 crypto/fipsmodule/merge_fips_mingw_rodata.cmake diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 54bad19f6ef..a7d425f8246 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -616,6 +616,14 @@ function(build_libcrypto) # MinGW ignores that pragma so we need to add the link explicitly. if(MINGW) target_link_libraries(${arg_NAME} PUBLIC bcrypt) + if(FIPS AND BUILD_SHARED_LIBS) + # MinGW's default auto-export handling does not reliably include all + # symbols consumers need from libcrypto.dll in FIPS shared builds, where + # the module is linked through bcm.o and jitterentropy is built into + # crypto. Force a complete import library so libssl.dll, bssl.exe, and + # test binaries can resolve those symbols. + target_link_options(${arg_NAME} PRIVATE "-Wl,--export-all-symbols") + endif() endif() endif() diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index 0ee8b6a789a..595051958f0 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -750,11 +750,12 @@ elseif(FIPS_SHARED) # Resolve an objdump for the target toolchain. It is used below to verify # that the assembled module object keeps all code/rodata in the single # .text/.rdata sections bracketed by the integrity markers. + get_filename_component(FIPS_LD_DIR "${CMAKE_LINKER}" DIRECTORY) + get_filename_component(FIPS_LD_NAME "${CMAKE_LINKER}" NAME) + set(FIPS_MINGW_OBJDUMP "${CMAKE_OBJDUMP}") if(NOT FIPS_MINGW_OBJDUMP) # Derive from the linker (e.g. -ld -> -objdump). - get_filename_component(FIPS_LD_DIR "${CMAKE_LINKER}" DIRECTORY) - get_filename_component(FIPS_LD_NAME "${CMAKE_LINKER}" NAME) string(REGEX REPLACE "ld(\\.exe)?$" "objdump" FIPS_OD_NAME "${FIPS_LD_NAME}") find_program(FIPS_MINGW_OBJDUMP NAMES "${FIPS_OD_NAME}" objdump llvm-objdump HINTS "${FIPS_LD_DIR}") @@ -765,6 +766,19 @@ elseif(FIPS_SHARED) "integrity section layout. Set CMAKE_OBJDUMP to the target objdump.") endif() + set(FIPS_MINGW_OBJCOPY "${CMAKE_OBJCOPY}") + if(NOT FIPS_MINGW_OBJCOPY) + # Derive from the linker (e.g. -ld -> -objcopy). + string(REGEX REPLACE "ld(\\.exe)?$" "objcopy" FIPS_OC_NAME "${FIPS_LD_NAME}") + find_program(FIPS_MINGW_OBJCOPY NAMES "${FIPS_OC_NAME}" objcopy llvm-objcopy + HINTS "${FIPS_LD_DIR}") + endif() + if(NOT FIPS_MINGW_OBJCOPY) + message(FATAL_ERROR + "FIPS MinGW build: could not find objcopy to merge the module " + "read-only data sections. Set CMAKE_OBJCOPY to the target objcopy.") + endif() + add_custom_command( OUTPUT fips_mingw_start.o COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_START -o fips_mingw_start.o @@ -775,13 +789,16 @@ elseif(FIPS_SHARED) COMMAND ${CMAKE_C_COMPILER} ${FIPS_MARKER_C_FLAGS} -w -c ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c -DAWSLC_FIPS_SHARED_END -o fips_mingw_end.o DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared_library_marker.c ) + set(BCM_UNMERGED_NAME bcm.unmerged.o) add_custom_command( OUTPUT ${BCM_NAME} - COMMAND ${CMAKE_LINKER} -r fips_mingw_start.o --whole-archive $ --no-whole-archive fips_mingw_end.o -o ${BCM_NAME} + COMMAND ${CMAKE_LINKER} -r fips_mingw_start.o --whole-archive $ --no-whole-archive fips_mingw_end.o -o ${BCM_UNMERGED_NAME} + COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${FIPS_MINGW_OBJDUMP} -DOBJCOPY=${FIPS_MINGW_OBJCOPY} -DLINKER=${CMAKE_LINKER} -DINPUT_OBJECT=${BCM_UNMERGED_NAME} -DOUTPUT_OBJECT=${BCM_NAME} -P ${CMAKE_CURRENT_SOURCE_DIR}/merge_fips_mingw_rodata.cmake # Fail the build if GCC split any module code/rodata into sections that # would fall outside the integrity markers in the final DLL. COMMAND ${CMAKE_COMMAND} -DOBJDUMP=${FIPS_MINGW_OBJDUMP} -DBCM_OBJECT=${BCM_NAME} -P ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake - DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake + DEPENDS fips_mingw_start.o fips_mingw_end.o bcm_library ${CMAKE_CURRENT_SOURCE_DIR}/check_fips_mingw_sections.cmake ${CMAKE_CURRENT_SOURCE_DIR}/merge_fips_mingw_rodata.cmake + BYPRODUCTS ${BCM_UNMERGED_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) else() diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index e038a09b5c0..9aaea4ab5bd 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -8,6 +8,9 @@ #include #include +#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +#include +#endif #if defined(BORINGSSL_FIPS) && !defined(OPENSSL_WINDOWS) #include #include @@ -181,6 +184,133 @@ extern const uint8_t BORINGSSL_bcm_rodata_start[]; extern const uint8_t BORINGSSL_bcm_rodata_end[]; #endif +#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +// Defined in fips_shared_support.c and filled in by inject_hash.go with the +// link-time preferred PE image base. See the comment there for why the runtime +// cannot read this from the in-memory PE header. +extern const uint64_t BORINGSSL_bcm_preferred_base; +#endif + +#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +#if !defined(OPENSSL_64_BIT) +#error "FIPS shared MinGW builds are only supported on 64-bit (x86_64) targets." +#endif +// MinGW is the only supported FIPS target whose hashed module region contains +// base relocations: GCC emits .refptr indirection cells (and genuine imports +// such as the CRT's free) into .rdata, inside the integrity boundary. Every +// other toolchain keeps the hashed region relocation-free by construction +// (ELF/Mach-O segregate relocated read-only pointers into separate sections; +// MSVC does not emit .refptr cells), so they hash the region directly via the +// plain definition below. Here we recompute the hash as it would appear at the +// preferred image base by un-applying the ASLR load delta to each relocation. +static int hmac_update_module_region(HMAC_CTX *hmac_ctx, const uint8_t *start, + size_t len) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(start, &mbi, sizeof(mbi)) == 0) { + return 0; + } + + const uint8_t *const image_base = (const uint8_t *)mbi.AllocationBase; + const IMAGE_DOS_HEADER *const dos_header = + (const IMAGE_DOS_HEADER *)image_base; + if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { + return 0; + } + + const IMAGE_NT_HEADERS *const nt_headers = + (const IMAGE_NT_HEADERS *)(image_base + dos_header->e_lfanew); + if (nt_headers->Signature != IMAGE_NT_SIGNATURE || + nt_headers->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + return 0; + } + + // The Windows loader overwrites the in-memory OptionalHeader.ImageBase with + // the actual load address, so use the link-time base recorded by + // inject_hash.go to recover the ASLR load delta. + const uintptr_t preferred_base = (uintptr_t)BORINGSSL_bcm_preferred_base; + const uintptr_t load_delta = (uintptr_t)image_base - preferred_base; + const IMAGE_DATA_DIRECTORY *const reloc_dir = + &nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + if (load_delta == 0 || reloc_dir->VirtualAddress == 0 || + reloc_dir->Size == 0) { + return HMAC_Update(hmac_ctx, start, len); + } + + const uintptr_t region_start = (uintptr_t)start; + const uintptr_t region_end = region_start + len; + if (region_end < region_start) { + return 0; + } + + const uint8_t *cursor = start; + const uint8_t *const end = start + len; + const uint8_t *reloc_block_ptr = image_base + reloc_dir->VirtualAddress; + const uint8_t *const reloc_end = reloc_block_ptr + reloc_dir->Size; + while (reloc_block_ptr + sizeof(IMAGE_BASE_RELOCATION) <= reloc_end) { + const IMAGE_BASE_RELOCATION *const reloc_block = + (const IMAGE_BASE_RELOCATION *)reloc_block_ptr; + if (reloc_block->SizeOfBlock < sizeof(IMAGE_BASE_RELOCATION) || + reloc_block_ptr + reloc_block->SizeOfBlock > reloc_end) { + return 0; + } + + const WORD *const entries = + (const WORD *)(reloc_block_ptr + sizeof(IMAGE_BASE_RELOCATION)); + const size_t entry_count = + (reloc_block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / + sizeof(WORD); + for (size_t i = 0; i < entry_count; i++) { + const WORD entry = entries[i]; + const WORD type = entry >> 12; + const WORD offset = entry & 0x0fff; + + // IMAGE_REL_BASED_ABSOLUTE entries are block padding, not relocations. + if (type == IMAGE_REL_BASED_ABSOLUTE) { + continue; + } + const uintptr_t reloc_addr = + (uintptr_t)image_base + reloc_block->VirtualAddress + offset; + if (reloc_addr < region_start || reloc_addr >= region_end) { + continue; + } + // Inside the hashed region only 64-bit absolute relocations are expected. + // Anything else (or one that would run past the region end) cannot be + // reconciled, so fail closed rather than hash load-dependent bytes. + if (type != IMAGE_REL_BASED_DIR64 || + (size_t)(region_end - reloc_addr) < sizeof(uint64_t)) { + return 0; + } + + // PE base relocations are emitted in ascending address order; the cursor + // check enforces that and bounds the gap length below. + const uint8_t *const reloc_ptr = (const uint8_t *)reloc_addr; + if (reloc_ptr < cursor || + !HMAC_Update(hmac_ctx, cursor, (size_t)(reloc_ptr - cursor))) { + return 0; + } + + uint8_t adjusted[sizeof(uint64_t)]; + const uint64_t value = + CRYPTO_load_u64_le(reloc_ptr) - (uint64_t)load_delta; + CRYPTO_store_u64_le(adjusted, value); + if (!HMAC_Update(hmac_ctx, adjusted, sizeof(uint64_t))) { + return 0; + } + cursor = reloc_ptr + sizeof(uint64_t); + } + + reloc_block_ptr += reloc_block->SizeOfBlock; + } + + return HMAC_Update(hmac_ctx, cursor, (size_t)(end - cursor)); +} +#else +static int hmac_update_module_region(HMAC_CTX *hmac_ctx, const uint8_t *start, + size_t len) { + return HMAC_Update(hmac_ctx, start, len); +} +#endif + #define STRING_POINTER_LENGTH 18 #define MAX_FUNCTION_NAME 32 #define ASSERT_WITHIN_MSG "FIPS module doesn't span expected symbol (%s). Expected %p <= %p < %p\n" @@ -350,18 +480,29 @@ int BORINGSSL_integrity_test(void) { BORINGSSL_maybe_set_module_text_permissions(PROT_READ | PROT_EXEC); #endif #if defined(BORINGSSL_SHARED_LIBRARY) - uint64_t length = end - start; + size_t region_len = (size_t)(end - start); + uint64_t length = region_len; uint8_t buffer[sizeof(length)]; CRYPTO_store_u64_le(buffer, length); - HMAC_Update(&hmac_ctx, buffer, sizeof(length)); - HMAC_Update(&hmac_ctx, start, length); + if (!HMAC_Update(&hmac_ctx, buffer, sizeof(length)) || + !hmac_update_module_region(&hmac_ctx, start, region_len)) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } - length = rodata_end - rodata_start; + region_len = (size_t)(rodata_end - rodata_start); + length = region_len; CRYPTO_store_u64_le(buffer, length); - HMAC_Update(&hmac_ctx, buffer, sizeof(length)); - HMAC_Update(&hmac_ctx, rodata_start, length); + if (!HMAC_Update(&hmac_ctx, buffer, sizeof(length)) || + !hmac_update_module_region(&hmac_ctx, rodata_start, region_len)) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } #else - HMAC_Update(&hmac_ctx, start, end - start); + if (!hmac_update_module_region(&hmac_ctx, start, (size_t)(end - start))) { + fprintf(stderr, "HMAC failed.\n"); + return 0; + } #endif #if !defined(OPENSSL_WINDOWS) BORINGSSL_maybe_set_module_text_permissions(PROT_EXEC); diff --git a/crypto/fipsmodule/check_fips_mingw_sections.cmake b/crypto/fipsmodule/check_fips_mingw_sections.cmake index b9dda6466a9..c36934bcd8d 100644 --- a/crypto/fipsmodule/check_fips_mingw_sections.cmake +++ b/crypto/fipsmodule/check_fips_mingw_sections.cmake @@ -42,33 +42,51 @@ endif() # Extract section names (the second whitespace-delimited column of `objdump -h`) # and flag any that indicate split-out module code or data. Dollar-suffixed -# grouped sections such as .rdata$zzz are benign compiler metadata that the -# linker collates into .rdata, so only the dot-form splits are forbidden. +# grouped .rdata sections contain MinGW refptr cells, so the build must merge +# them into .rdata before this check runs. string(REPLACE "\n" ";" objdump_lines "${objdump_output}") set(forbidden_sections "") +set(forbidden_section_metadata "") +set(current_section_name "") foreach(line IN LISTS objdump_lines) string(REGEX REPLACE "^[ \t]+" "" line "${line}") - string(REGEX REPLACE "[ \t]+" ";" fields "${line}") - list(LENGTH fields field_count) - if(field_count GREATER 1) - list(GET fields 1 section_name) - if(section_name MATCHES "^\\.text\\." OR section_name MATCHES "^\\.rdata\\.") + if(line MATCHES "^[0-9]+[ \t]+([^ \t]+)") + set(section_name "${CMAKE_MATCH_1}") + set(current_section_name "${section_name}") + if(section_name MATCHES "^\\.text[\\.$]" OR section_name MATCHES "^\\.rdata[\\.$]") list(APPEND forbidden_sections "${section_name}") endif() + elseif(current_section_name STREQUAL ".rdata" AND line MATCHES "LINK_ONCE|COMDAT") + list(APPEND forbidden_section_metadata ".rdata: ${line}") endif() endforeach() -if(forbidden_sections) +if(forbidden_sections OR forbidden_section_metadata) list(REMOVE_DUPLICATES forbidden_sections) + list(REMOVE_DUPLICATES forbidden_section_metadata) string(REPLACE ";" ", " forbidden_pretty "${forbidden_sections}") + string(REPLACE ";" ", " forbidden_metadata_pretty "${forbidden_section_metadata}") + set(failure_details "") + if(forbidden_sections) + string(APPEND failure_details + "split-out section(s) outside the integrity markers: ${forbidden_pretty}.") + endif() + if(forbidden_section_metadata) + if(failure_details) + string(APPEND failure_details " ") + endif() + string(APPEND failure_details + "merged .rdata still has grouped-section metadata: ${forbidden_metadata_pretty}.") + endif() message(FATAL_ERROR "FIPS integrity check would be incomplete: '${BCM_OBJECT}' contains " - "split-out section(s) outside the integrity markers: ${forbidden_pretty}.\n" + "${failure_details}\n" "All module code must live in a single contiguous .text (and rodata in " ".rdata) bracketed by BORINGSSL_bcm_text_start/end. Ensure the module is " "built with -fno-reorder-functions -fno-reorder-blocks-and-partition " - "(and -fno-function-sections -fno-data-sections); see the FIPS_SHARED " - "MinGW blocks in the top-level CMakeLists.txt.") + "(and -fno-function-sections -fno-data-sections), and ensure grouped " + ".rdata$ sections are merged by merge_fips_mingw_rodata.cmake; see the " + "FIPS_SHARED MinGW blocks in the top-level CMakeLists.txt.") endif() message(STATUS "FIPS MinGW section check passed for ${BCM_OBJECT}") diff --git a/crypto/fipsmodule/fips_shared_library_marker.c b/crypto/fipsmodule/fips_shared_library_marker.c index 3c8df98e76e..95c401e216a 100644 --- a/crypto/fipsmodule/fips_shared_library_marker.c +++ b/crypto/fipsmodule/fips_shared_library_marker.c @@ -13,6 +13,12 @@ #include #include +#if defined(__MINGW32__) +#define AWSLC_MINGW_RODATA_END __attribute__((section(".rdata$zzzz"), used)) +#else +#define AWSLC_MINGW_RODATA_END +#endif + #if defined(AWSLC_FIPS_SHARED_START) #if defined(_MSC_VER) #pragma code_seg(".fipstx$a") @@ -56,6 +62,7 @@ const uint8_t *BORINGSSL_bcm_text_end(void){ #if defined(_MSC_VER) __declspec(allocate(".fipsco$z")) #endif +AWSLC_MINGW_RODATA_END const uint8_t BORINGSSL_bcm_rodata_end[16] = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; diff --git a/crypto/fipsmodule/fips_shared_support.c b/crypto/fipsmodule/fips_shared_support.c index 65bd0ea90f2..d4d6cc4afd3 100644 --- a/crypto/fipsmodule/fips_shared_support.c +++ b/crypto/fipsmodule/fips_shared_support.c @@ -2,10 +2,9 @@ // SPDX-License-Identifier: ISC // Ensure that no ambient #pragma const_seg is active. BORINGSSL_bcm_text_hash -// MUST be placed in the default .rdata section, outside the FIPS module rodata -// boundary (.fipsco). If it were placed inside the FIPS rodata boundary, the -// integrity check would hash the expected value itself, creating a circular -// dependency that can never be satisfied. +// MUST be placed outside the FIPS module rodata boundary. If it were placed +// inside the FIPS rodata boundary, the integrity check would hash the expected +// value itself, creating a circular dependency that can never be satisfied. #if defined(_MSC_VER) #pragma const_seg() #endif @@ -13,15 +12,40 @@ #if defined(BORINGSSL_FIPS) && defined(BORINGSSL_SHARED_LIBRARY) #include -// BORINGSSL_bcm_text_hash is is default hash value for the FIPS integrity check -// that must be replaced with the real value during the build process. This -// value need only be distinct, i.e. so that we can safely search-and-replace it -// in an object file. -const uint8_t BORINGSSL_bcm_text_hash[32] = { +#if defined(__MINGW32__) +// MinGW brackets the module rodata with symbols in .rdata. Keep the expected +// hash in a separate read-only PE section so it cannot be included in the +// integrity input. +#define AWSLC_FIPS_HASH_SECTION __attribute__((section(".fipshash"), used)) +#else +#define AWSLC_FIPS_HASH_SECTION +#endif + +// BORINGSSL_bcm_text_hash is the default hash value for the FIPS integrity +// check that must be replaced with the real value during the build process. +// This value need only be distinct, i.e. so that we can safely +// search-and-replace it in an object file. +const uint8_t BORINGSSL_bcm_text_hash[32] AWSLC_FIPS_HASH_SECTION = { 0xae, 0x2c, 0xea, 0x2a, 0xbd, 0xa6, 0xf3, 0xec, 0x97, 0x7f, 0x9b, 0xf6, 0x94, 0x9a, 0xfc, 0x83, 0x68, 0x27, 0xcb, 0xa0, 0xa0, 0x9f, 0x6b, 0x6f, 0xde, 0x52, 0xcd, 0xe2, 0xcd, 0xff, 0x31, 0x80, }; + +#if defined(__MINGW32__) +// BORINGSSL_bcm_preferred_base records the link-time (preferred) PE image base +// of the crypto DLL. When a module is relocated for ASLR, the Windows loader +// rewrites OptionalHeader.ImageBase in the in-memory PE header to the actual +// load address, so the runtime integrity check cannot recover the load delta +// from the header. inject_hash.go writes the real preferred base here during the +// build. Like BORINGSSL_bcm_text_hash it lives outside the hashed module +// boundary, and because it is an integer (not a pointer) it carries no base +// relocation of its own, so its value is identical on disk and in memory. +// +// The initializer is a recognizable sentinel: if it survives to runtime, base +// injection did not run and the integrity check fails closed. +const uint64_t BORINGSSL_bcm_preferred_base AWSLC_FIPS_HASH_SECTION = + UINT64_C(0xBADC0FFEE0DDF00D); +#endif #else // C requires a translation unit to contain at least one declaration. Since // BORINGSSL_FIPS or BORINGSSL_SHARED_LIBRARY is not defined, this file is diff --git a/crypto/fipsmodule/merge_fips_mingw_rodata.cmake b/crypto/fipsmodule/merge_fips_mingw_rodata.cmake new file mode 100644 index 00000000000..1d91406b2a0 --- /dev/null +++ b/crypto/fipsmodule/merge_fips_mingw_rodata.cmake @@ -0,0 +1,97 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR ISC + +# MinGW emits external address cells in grouped .rdata$* sections. They are part +# of the FIPS module's read-only data, so merge them into .rdata before the final +# DLL link. The end marker is compiled into .rdata$zzzz, so it remains after the +# merged grouped sections. + +foreach(required_var OBJDUMP OBJCOPY LINKER INPUT_OBJECT OUTPUT_OBJECT) + if(NOT DEFINED ${required_var} OR "${${required_var}}" STREQUAL "") + message(FATAL_ERROR + "FIPS MinGW rodata merge: ${required_var} not provided") + endif() +endforeach() + +execute_process( + COMMAND "${OBJDUMP}" -h "${INPUT_OBJECT}" + OUTPUT_VARIABLE objdump_output + ERROR_VARIABLE objdump_error + RESULT_VARIABLE objdump_result +) +if(NOT objdump_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to run '${OBJDUMP} -h ${INPUT_OBJECT}'\n${objdump_error}") +endif() + +string(REPLACE "\n" ";" objdump_lines "${objdump_output}") +set(objcopy_args "") +foreach(line IN LISTS objdump_lines) + string(REGEX REPLACE "^[ \t]+" "" line "${line}") + string(REGEX REPLACE "[ \t]+" ";" fields "${line}") + list(LENGTH fields field_count) + if(field_count GREATER 1) + list(GET fields 1 section_name) + if(section_name MATCHES "^\\.rdata\\$") + list(APPEND objcopy_args "--rename-section" "${section_name}=.rdata") + endif() + endif() +endforeach() + +if(NOT objcopy_args) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy "${INPUT_OBJECT}" "${OUTPUT_OBJECT}" + ERROR_VARIABLE copy_error + RESULT_VARIABLE copy_result + ) + if(NOT copy_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to copy '${INPUT_OBJECT}' to '${OUTPUT_OBJECT}'\n${copy_error}") + endif() + return() +endif() + +set(renamed_object "${OUTPUT_OBJECT}.rdata_renamed.tmp") +set(cleaned_object "${OUTPUT_OBJECT}.rdata_cleaned.tmp") +file(REMOVE "${renamed_object}" "${cleaned_object}" "${OUTPUT_OBJECT}") + +execute_process( + COMMAND "${OBJCOPY}" ${objcopy_args} --wildcard --localize-symbol=.refptr.* "${INPUT_OBJECT}" "${renamed_object}" + ERROR_VARIABLE objcopy_error + RESULT_VARIABLE objcopy_result +) +if(NOT objcopy_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to rename grouped .rdata sections\n${objcopy_error}") +endif() + +# Re-link the renamed object to coalesce the duplicate .rdata sections into one +# contiguous section. Warnings from intermediate COMDAT section names are +# expected after the rename and are intentionally not surfaced on success. +execute_process( + COMMAND "${LINKER}" -r "${renamed_object}" -o "${OUTPUT_OBJECT}" + ERROR_VARIABLE linker_error + RESULT_VARIABLE linker_result +) +file(REMOVE "${renamed_object}") +if(NOT linker_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to merge grouped .rdata sections\n${linker_error}") +endif() + +# The rename can leave the merged .rdata section marked LINK_ONCE/COMDAT. Clear +# that metadata so the final DLL link emits base-relocation entries for absolute +# refptr cells inside the FIPS rodata window. +execute_process( + COMMAND "${OBJCOPY}" + --set-section-flags ".rdata=alloc,load,readonly,data,contents" + "${OUTPUT_OBJECT}" "${cleaned_object}" + ERROR_VARIABLE objcopy_flags_error + RESULT_VARIABLE objcopy_flags_result +) +if(NOT objcopy_flags_result EQUAL 0) + message(FATAL_ERROR + "FIPS MinGW rodata merge: failed to clear merged .rdata flags\n${objcopy_flags_error}") +endif() +file(REMOVE "${OUTPUT_OBJECT}") +file(RENAME "${cleaned_object}" "${OUTPUT_OBJECT}") diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index e2742ad6c84..9fc9b803924 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -456,6 +456,46 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { moduleROData = rodata[*rodataStart:*rodataEnd] } + // Record the link-time preferred PE image base in BORINGSSL_bcm_preferred_base + // so the runtime integrity check can compute the ASLR load delta. The Windows + // loader rewrites OptionalHeader.ImageBase in the in-memory image once the DLL + // is relocated, so the runtime cannot read the preferred base from the header. + var imageBase uint64 + switch oh := object.OptionalHeader.(type) { + case *pe.OptionalHeader64: + imageBase = oh.ImageBase + case *pe.OptionalHeader32: + imageBase = uint64(oh.ImageBase) + default: + return nil, nil, errors.New("unsupported or missing PE optional header") + } + + var baseSym *pe.Symbol + for _, symbol := range symbols { + if symbol.Name == "BORINGSSL_bcm_preferred_base" { + if baseSym != nil { + return nil, nil, errors.New("duplicate preferred base symbol found") + } + baseSym = symbol + } + } + if baseSym == nil { + return nil, nil, errors.New("could not find BORINGSSL_bcm_preferred_base symbol") + } + sn := int(baseSym.SectionNumber) + if sn < 1 || sn > len(object.Sections) { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base has invalid section number") + } + baseSection := object.Sections[sn-1] + if baseSection.Offset == 0 { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base section has no file data") + } + baseFileOffset := int64(baseSection.Offset) + int64(baseSym.Value) + if baseFileOffset < 0 || baseFileOffset+8 > int64(len(objectBytes)) { + return nil, nil, errors.New("BORINGSSL_bcm_preferred_base lies outside the object file") + } + binary.LittleEndian.PutUint64(objectBytes[baseFileOffset:], imageBase) + return moduleText, moduleROData, nil } From b3f0893b55bb3be93e4f22f666e325358bc66c13 Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 14:38:12 +0000 Subject: [PATCH 12/19] Add a FIPS integrity negative test to the cross-mingw-fips CI job The positive test_fips run alone cannot distinguish a working integrity check from a no-op, since injection and the runtime check measure the same window. Bring the MinGW job to parity with the native MSVC FIPS job by corrupting the module and asserting the check fails. - break-hash.go: add a -mingw mode that locates the module via the PE/COFF symbol table (no .map file, mirroring inject_hash.go's -mingw path) and flips a byte inside [BORINGSSL_bcm_text_start, BORINGSSL_bcm_text_end]. - run_cross_mingw_fips_tests.sh: after the positive test, corrupt the crypto DLL, confirm test_fips now fails, then restore the pristine DLL before the gtest runs. --- tests/ci/run_cross_mingw_fips_tests.sh | 52 +++++++++++++-- util/fipstools/break-hash.go | 87 ++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/tests/ci/run_cross_mingw_fips_tests.sh b/tests/ci/run_cross_mingw_fips_tests.sh index 12d189e4c01..43a28486ecc 100755 --- a/tests/ci/run_cross_mingw_fips_tests.sh +++ b/tests/ci/run_cross_mingw_fips_tests.sh @@ -70,10 +70,20 @@ EOF cat ${TARGET_CPU}-${TARGET_PLATFORM}.cmake -# Make the cross-compiled crypto/ssl DLLs and the mingw runtime DLLs -# (libstdc++, libgcc_s, libwinpthread) discoverable by Wine when it loads the -# test executables. WINEPATH is a colon-separated list of Unix paths. -export WINEPATH="${BUILD_ROOT}/crypto:${BUILD_ROOT}/ssl:/usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}:/usr/${TARGET_CPU}-${TARGET_PLATFORM}/lib" +function wine_z_path { + local unix_path="${1%/}" + printf 'Z:%s' "${unix_path//\//\\}" +} + +# Make the cross-compiled crypto/ssl DLLs, generated test DLLs, and the mingw +# runtime DLLs (libstdc++, libgcc_s, libwinpthread) discoverable by Wine when it +# loads the test executables. WINEPATH is a semicolon-separated list of +# Windows-style paths. +export WINEPATH="$(wine_z_path "${BUILD_ROOT}")" +WINEPATH+=";$(wine_z_path "${BUILD_ROOT}/crypto")" +WINEPATH+=";$(wine_z_path "${BUILD_ROOT}/ssl")" +WINEPATH+=";$(wine_z_path "/usr/lib/gcc/${TARGET_CPU}-${TARGET_PLATFORM}/${GCC_VERSION}-${THREAD_MODEL}")" +WINEPATH+=";$(wine_z_path "/usr/${TARGET_CPU}-${TARGET_PLATFORM}/lib")" # Keep Wine quiet and non-interactive in CI. export WINEDEBUG="-all" @@ -91,11 +101,41 @@ for BO in "${BUILD_OPTIONS[@]}"; do module_status=$(${BUILD_ROOT}/tool/bssl.exe isfips) [[ "${module_status}" == "1" ]] || { echo >&2 "FIPS Mode validation failed."; exit 1; } - # test_fips exercises the power-on self-test, including the runtime integrity - # check that recomputes the module hash. This is the primary validation that + # Positive test: test_fips exercises the power-on self-test, including the + # runtime integrity check that recomputes the module hash. This validates that # inject_hash.go wrote a correct hash into the PE/COFF DLL. ${BUILD_ROOT}/util/fipstools/test_fips.exe + # Negative test: corrupt the FIPS module inside the crypto DLL and confirm the + # runtime integrity check detects it (test_fips must now FAIL). This proves the + # integrity check actually runs and has teeth on the MinGW PE/COFF DLL, rather + # than merely being self-consistent with inject_hash.go (both measure the same + # window, so a passing positive test alone cannot distinguish a working check + # from a no-op one). break-hash.go -mingw locates the module via the PE/COFF + # symbol table (no .map file) and flips a byte inside + # [BORINGSSL_bcm_text_start, BORINGSSL_bcm_text_end]. + crypto_dll="${BUILD_ROOT}/crypto/libcrypto.dll" + if [ ! -f "${crypto_dll}" ]; then + echo >&2 "Expected crypto DLL not found at ${crypto_dll}"; exit 1 + fi + cp "${crypto_dll}" "${crypto_dll}.bak" + (cd "${SRC_ROOT}" && go run util/fipstools/break-hash.go -mingw "${crypto_dll}" "${crypto_dll}.corrupted") + cp "${crypto_dll}.corrupted" "${crypto_dll}" + # test_fips is expected to fail here; disable errexit so we can capture its + # exit code instead of aborting the script. + set +e + ${BUILD_ROOT}/util/fipstools/test_fips.exe + fips_negative_rc=$? + set -e + # Restore the pristine DLL before asserting, so a failure does not leave a + # corrupted DLL behind for the gtest runs below or for local re-runs. + cp "${crypto_dll}.bak" "${crypto_dll}" + rm -f "${crypto_dll}.bak" "${crypto_dll}.corrupted" + if [ "${fips_negative_rc}" -eq 0 ]; then + echo >&2 "FIPS integrity negative test failed: test_fips should have failed with a corrupted crypto DLL." + exit 1 + fi + # Confirm the module loads and basic functionality works through the DLL. shard_gtest "${BUILD_ROOT}/crypto/crypto_test.exe --gtest_also_run_disabled_tests" shard_gtest ${BUILD_ROOT}/ssl/ssl_test.exe diff --git a/util/fipstools/break-hash.go b/util/fipstools/break-hash.go index 6d12e0a52e3..37202a777d2 100644 --- a/util/fipstools/break-hash.go +++ b/util/fipstools/break-hash.go @@ -12,6 +12,7 @@ import ( "crypto/hmac" "crypto/sha512" "debug/elf" + "debug/pe" "encoding/hex" "errors" "flag" @@ -140,7 +141,80 @@ func doPE(objectBytes []byte, mapPath string) (int, []byte, error) { return int(startOffset), moduleText, nil } -func do(outPath, inPath, mapPath string) error { +func doMingw(objectBytes []byte) (int, []byte, error) { + // The MinGW FIPS DLL is not built with a linker .map file, so (unlike + // doPE) we locate the module from the PE/COFF symbol table directly, the + // same way inject_hash.go does for MinGW. COFF symbol values are offsets + // from the start of their section, which translates directly to an on-disk + // location via the section's raw-data file offset. + object, err := pe.NewFile(bytes.NewReader(objectBytes)) + if err != nil { + return 0, nil, errors.New("failed to parse object: " + err.Error()) + } + + var textSection *pe.Section + var textSectionIndex int + for i, section := range object.Sections { + if section.Name == ".text" { + textSection = section + // COFF section numbers are 1-based. + textSectionIndex = i + 1 + break + } + } + if textSection == nil { + return 0, nil, errors.New("failed to find .text section in object") + } + + symbols := object.Symbols + if symbols == nil { + return 0, nil, errors.New("no symbol table found in object") + } + + var startSeen, endSeen bool + var start, end uint64 + for _, symbol := range symbols { + if int(symbol.SectionNumber) != textSectionIndex { + continue + } + switch symbol.Name { + case "BORINGSSL_bcm_text_start": + if startSeen { + return 0, nil, errors.New("duplicate start symbol found") + } + startSeen = true + start = uint64(symbol.Value) + case "BORINGSSL_bcm_text_end": + if endSeen { + return 0, nil, errors.New("duplicate end symbol found") + } + endSeen = true + end = uint64(symbol.Value) + } + } + + if !startSeen || !endSeen { + return 0, nil, errors.New("could not find module in object") + } + if start >= end || end > uint64(textSection.Size) { + return 0, nil, fmt.Errorf("invalid module boundaries: start=0x%x end=0x%x textsize=0x%x", start, end, textSection.Size) + } + + // Section.Offset is the file offset of the section's raw data + // (PointerToRawData), so the module's on-disk start is that plus the + // section-relative symbol offset. + fileOffset := uint64(textSection.Offset) + start + if fileOffset+(end-start) > uint64(len(objectBytes)) { + return 0, nil, errors.New("module extends past end of file") + } + + moduleText := make([]byte, end-start) + copy(moduleText, objectBytes[fileOffset:fileOffset+(end-start)]) + + return int(fileOffset), moduleText, nil +} + +func do(outPath, inPath, mapPath string, mingw bool) error { objectBytes, err := os.ReadFile(inPath) if err != nil { return err @@ -149,7 +223,9 @@ func do(outPath, inPath, mapPath string) error { var fileOffset int var moduleText []byte - if mapPath != "" { + if mingw { + fileOffset, moduleText, err = doMingw(objectBytes) + } else if mapPath != "" { fileOffset, moduleText, err = doPE(objectBytes, mapPath) } else { fileOffset, moduleText, err = doELF(objectBytes) @@ -182,9 +258,10 @@ func do(outPath, inPath, mapPath string) error { } func main() { - mapPath := flag.String("map", "", "Path to linker .map file (required for Windows PE/DLL)") + mapPath := flag.String("map", "", "Path to linker .map file (required for Windows MSVC PE/DLL)") + mingw := flag.Bool("mingw", false, "Whether the FIPS module is a Windows MinGW DLL (uses the PE/COFF symbol table instead of a .map file)") flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [-map mapfile] \n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [-map mapfile] [-mingw] \n", os.Args[0]) flag.PrintDefaults() } flag.Parse() @@ -195,7 +272,7 @@ func main() { os.Exit(1) } - if err := do(args[1], args[0], *mapPath); err != nil { + if err := do(args[1], args[0], *mapPath, *mingw); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } From 785379d3e775faeb78ca286a22d0f30fa8fadadd Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 15:32:51 +0000 Subject: [PATCH 13/19] Use Win32 threads on MinGW so FIPS static locks are zero-initialized MinGW-GCC was the only Windows toolchain still routed to winpthreads (crypto/internal.h). winpthreads defines PTHREAD_RWLOCK_INITIALIZER as (pthread_rwlock_t)-1, so CRYPTO_STATIC_MUTEX_INIT is all-ones rather than all zeros. The FIPS power-on self-test build requires these static initializers to be zero (BSS-placeable), so ThreadTest.InitZeros failed once the cross-mingw-fips job started running crypto_test in FIPS mode under MinGW. Route MinGW-GCC to the native Win32/SRWLOCK threading backend, matching MSVC and clang-MinGW (the latter was moved in #2381). SRWLOCK_INIT is {0}, satisfying the zero-init requirement. Zeroing the winpthreads initializer is not a valid alternative: (void*)-1 is its lazy-init sentinel. thread_win.c registers a thread-local-destructor callback in .CRT$XLC via the MSVC-only const_seg/data_seg pragmas, which MinGW-GCC rejects under -Werror=unknown-pragmas. Add a GCC branch that places the callback with __attribute__((section(".CRT$XLC"), used)) instead. Verified under Wine that ThreadTest.InitZeros and the TLS-destructor test (ThreadTest.ThreadLocal) both pass. --- crypto/internal.h | 4 +--- crypto/thread_win.c | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crypto/internal.h b/crypto/internal.h index 1f7ad67816a..2de8195be82 100644 --- a/crypto/internal.h +++ b/crypto/internal.h @@ -34,9 +34,7 @@ #endif #endif -#if defined(OPENSSL_THREADS) && \ - (!defined(OPENSSL_WINDOWS) || \ - (defined(__MINGW32__) && !defined(__clang__))) +#if defined(OPENSSL_THREADS) && !defined(OPENSSL_WINDOWS) #include #define OPENSSL_PTHREADS #endif diff --git a/crypto/thread_win.c b/crypto/thread_win.c index 10082fbc05f..b3c64230d5f 100644 --- a/crypto/thread_win.c +++ b/crypto/thread_win.c @@ -183,6 +183,7 @@ __pragma(comment( #ifdef _WIN64 // .CRT section is merged with .rdata on x64 so it must be constant data. +#if defined(_MSC_VER) || defined(__clang__) #pragma const_seg(".CRT$XLC") // When defining a const variable, it must have external linkage to be sure the // linker doesn't discard it. @@ -190,13 +191,27 @@ extern const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl; const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl = thread_local_destructor; // Reset the default section. #pragma const_seg() +#else +// GCC (e.g. MinGW-GCC) does not support the const_seg/data_seg pragmas, so use +// the section attribute to place the callback in the .CRT$XLC callback array. +// 'used' keeps the optimizer from discarding it (the linker /INCLUDE pragmas +// above are likewise MSVC-only). +extern const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl; +const PIMAGE_TLS_CALLBACK p_thread_callback_boringssl + __attribute__((section(".CRT$XLC"), used)) = thread_local_destructor; +#endif // _MSC_VER || __clang__ #else +#if defined(_MSC_VER) || defined(__clang__) #pragma data_seg(".CRT$XLC") PIMAGE_TLS_CALLBACK p_thread_callback_boringssl = thread_local_destructor; // Reset the default section. #pragma data_seg() +#else +PIMAGE_TLS_CALLBACK p_thread_callback_boringssl + __attribute__((section(".CRT$XLC"), used)) = thread_local_destructor; +#endif // _MSC_VER || __clang__ #endif // _WIN64 From 698d614b2b482c694a01bbcb89939cd7eb02f1a3 Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 18:07:32 +0000 Subject: [PATCH 14/19] Clarify MinGW FIPS shared comments and fold duplicate guard - Expand the Threads::Threads comment to note that statically linking winpthread by mutating the imported target's interface affects every threaded artifact in the build (libssl.dll, bssl.exe, tests), not just the integrity-checked crypto DLL, and is build-local only. - Fold two adjacent identical #if (__MINGW32__ && BORINGSSL_SHARED_LIBRARY) guards in bcm.c into a single block. No functional change. --- CMakeLists.txt | 8 ++++++++ crypto/fipsmodule/bcm.c | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a9a10e464c5..c901992824f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1387,6 +1387,14 @@ if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") # The FIPS shared module on MinGW must contain its own copy of the # winpthread implementation so that the integrity-checked DLL does not # depend on an external libwinpthread DLL at runtime. + # + # NOTE: crypto links Threads::Threads PUBLIC, so mutating the imported + # target's interface here statically links winpthread into *every* artifact + # in this build that pulls in threads (libssl.dll, bssl.exe, and the test + # binaries), not just the crypto DLL. That is intentional for the integrity + # boundary's sake and otherwise harmless (just some duplicated code), but it + # is broader than "the FIPS DLL only". This mutation is build-local and is + # not propagated to downstream consumers of the installed crypto target. set_property(TARGET Threads::Threads PROPERTY INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") endif() diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index 9aaea4ab5bd..2b888829c66 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -185,16 +185,14 @@ extern const uint8_t BORINGSSL_bcm_rodata_end[]; #endif #if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) +#if !defined(OPENSSL_64_BIT) +#error "FIPS shared MinGW builds are only supported on 64-bit (x86_64) targets." +#endif // Defined in fips_shared_support.c and filled in by inject_hash.go with the // link-time preferred PE image base. See the comment there for why the runtime // cannot read this from the in-memory PE header. extern const uint64_t BORINGSSL_bcm_preferred_base; -#endif -#if defined(__MINGW32__) && defined(BORINGSSL_SHARED_LIBRARY) -#if !defined(OPENSSL_64_BIT) -#error "FIPS shared MinGW builds are only supported on 64-bit (x86_64) targets." -#endif // MinGW is the only supported FIPS target whose hashed module region contains // base relocations: GCC emits .refptr indirection cells (and genuine imports // such as the CRT's free) into .rdata, inside the integrity boundary. Every From 98521d28b6ef3ceafe4a210fd6970c062db2ae9e Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 20:00:43 +0000 Subject: [PATCH 15/19] Move cross-mingw-fips CI to Ubuntu 24.04 (mingw-w64 gcc-13) The MinGW FIPS shared build relies on the linker preserving the dllexport directives (.drectve) for the FIPS module's symbols through the bcm.o partial link. binutils 2.38 (Ubuntu 22.04) drops them, leaving libcrypto.dll under-exported; binutils >= ~2.41 (Ubuntu 24.04 / mingw-w64 gcc-13) preserves them. Move the job to ubuntu-24.04 and target gcc-13 so the module can be exported via dllexport rather than a blanket -Wl,--export-all-symbols, and point WineHQ at the noble repo to match the new runner. MinGW FIPS shared is a new build target with no existing consumers, so setting this toolchain floor breaks nothing. --- .github/workflows/windows-alt.yml | 4 ++-- tests/ci/run_cross_mingw_fips_tests.sh | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/windows-alt.yml b/.github/workflows/windows-alt.yml index 4c67ff26496..eace44335f5 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -209,7 +209,7 @@ jobs: ./tests/ci/run_cross_mingw_tests.sh x86_64 w64-mingw32 "-DCMAKE_BUILD_TYPE=Release" cross-mingw-fips: if: github.repository_owner == 'aws' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Install Tools run: | @@ -220,7 +220,7 @@ jobs: sudo dpkg --add-architecture i386 sudo mkdir -pm755 /etc/apt/keyrings sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key - sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources + sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none sudo apt-get install --assume-yes --no-install-recommends build-essential cmake golang-go nasm clang wget mingw-w64 sudo apt-get install --assume-yes --install-recommends winehq-stable wine-binfmt diff --git a/tests/ci/run_cross_mingw_fips_tests.sh b/tests/ci/run_cross_mingw_fips_tests.sh index 43a28486ecc..e33a494bd40 100755 --- a/tests/ci/run_cross_mingw_fips_tests.sh +++ b/tests/ci/run_cross_mingw_fips_tests.sh @@ -14,7 +14,13 @@ set -ex TARGET_CPU="${1}" TARGET_PLATFORM="${2}" BUILD_OPTIONS=("${@:3:5}") -GCC_VERSION="10" +# MinGW FIPS shared requires a reasonably recent binutils (>= ~2.41, as shipped +# by Ubuntu 24.04 / mingw-w64 gcc-13). Older linkers (e.g. binutils 2.38 on +# Ubuntu 22.04) drop the dllexport directives (.drectve) for the FIPS module's +# symbols during the bcm.o partial link, leaving libcrypto.dll under-exported. +# This is a new build target with no existing consumers, so we set the floor +# here rather than fall back to -Wl,--export-all-symbols. +GCC_VERSION="13" THREAD_MODEL="posix" if [ "${#BUILD_OPTIONS[@]}" -lt 1 ]; then From 337cd7f1f702c0e07d6a9e1e9f3951b0257e5a7a Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 20:00:43 +0000 Subject: [PATCH 16/19] Export FIPS module and jitterentropy via dllexport on MinGW Drop -Wl,--export-all-symbols from the MinGW FIPS shared crypto DLL so it exports the same OPENSSL_EXPORT surface as the MSVC and ELF builds instead of every internal global symbol. The FIPS module's public API is already OPENSSL_EXPORT (dllexport) and exports correctly once the toolchain preserves .drectve through the bcm.o partial link (see the Ubuntu 24.04 bump). jitterentropy's JENT_PRIVATE_STATIC keyed its export decision on _MSC_VER, so MinGW-GCC fell into the ELF visibility("default") branch -- a no-op on PE/COFF -- and jent_* (called directly by cpu_jitter_test) were never exported. Key on _WIN32 so MinGW exports them via dllexport like MSVC does. Verified with mingw-w64 gcc-13/binutils-2.41 under Wine: libcrypto.dll, libssl.dll, bssl, crypto_test, ssl_test and test_fips all link, and the FIPS positive and negative (break-hash -mingw) integrity tests pass. --- crypto/CMakeLists.txt | 8 -------- .../jitterentropy/jitterentropy-library/jitterentropy.h | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index a7d425f8246..54bad19f6ef 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -616,14 +616,6 @@ function(build_libcrypto) # MinGW ignores that pragma so we need to add the link explicitly. if(MINGW) target_link_libraries(${arg_NAME} PUBLIC bcrypt) - if(FIPS AND BUILD_SHARED_LIBS) - # MinGW's default auto-export handling does not reliably include all - # symbols consumers need from libcrypto.dll in FIPS shared builds, where - # the module is linked through bcm.o and jitterentropy is built into - # crypto. Force a complete import library so libssl.dll, bssl.exe, and - # test binaries can resolve those symbols. - target_link_options(${arg_NAME} PRIVATE "-Wl,--export-all-symbols") - endif() endif() endif() diff --git a/third_party/jitterentropy/jitterentropy-library/jitterentropy.h b/third_party/jitterentropy/jitterentropy-library/jitterentropy.h index ec6f5bc2832..52e79ef6af0 100644 --- a/third_party/jitterentropy/jitterentropy-library/jitterentropy.h +++ b/third_party/jitterentropy/jitterentropy-library/jitterentropy.h @@ -412,7 +412,7 @@ struct rand_data #ifdef JENT_PRIVATE_COMPILE # define JENT_PRIVATE_STATIC static #else /* JENT_PRIVATE_COMPILE */ -#if defined(_MSC_VER) +#if defined(_WIN32) #define JENT_PRIVATE_STATIC __declspec(dllexport) #else #define JENT_PRIVATE_STATIC __attribute__((visibility("default"))) From 9caeb823835efd7b7c80c6007a47892e66709ac3 Mon Sep 17 00:00:00 2001 From: Justin W Smith Date: Fri, 19 Jun 2026 20:12:09 +0000 Subject: [PATCH 17/19] Install binfmt-support for cross-mingw-fips on Ubuntu 24.04 The job registers Wine as the PE interpreter via `update-binfmts` so the test binaries (test_fips.exe, bssl.exe) can run directly. On Ubuntu 22.04 that tool came in transitively with wine-binfmt, but on 24.04 (noble) wine-binfmt no longer pulls in binfmt-support, so `update-binfmts` was missing and the install step failed with "command not found". Install binfmt-support explicitly. --- .github/workflows/windows-alt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-alt.yml b/.github/workflows/windows-alt.yml index eace44335f5..ca908e7298c 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -223,7 +223,7 @@ jobs: sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none sudo apt-get install --assume-yes --no-install-recommends build-essential cmake golang-go nasm clang wget mingw-w64 - sudo apt-get install --assume-yes --install-recommends winehq-stable wine-binfmt + sudo apt-get install --assume-yes --install-recommends winehq-stable wine-binfmt binfmt-support sudo update-binfmts --display sudo update-binfmts --disable sudo update-binfmts --enable wine From a75710e882f3ee049407ba8c79b41ebeb4ff601a Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Tue, 23 Jun 2026 13:35:40 -0400 Subject: [PATCH 18/19] Harden MinGW FIPS integrity check and inject_hash input validation - bcm.c: fail closed in hmac_update_module_region if BORINGSSL_bcm_preferred_base still holds the injection sentinel (or 0), so a DLL whose hash injection never ran cannot reach the relocation normalization path and compute a load delta against a bogus base. Add a BORINGSSL_BCM_PREFERRED_BASE_UNSET macro for the sentinel value. - inject_hash.go: make -apple/-windows/-mingw mutually exclusive, reject -map outside -windows, and reject static archives on -mingw, matching the existing -windows guards. - Comment clarifications in bcm.c, fips_shared_support.c, check_fips_mingw_sections.cmake, and CMakeLists.txt. --- CMakeLists.txt | 6 ++--- crypto/fipsmodule/bcm.c | 15 +++++++---- .../check_fips_mingw_sections.cmake | 5 ++++ crypto/fipsmodule/fips_shared_support.c | 16 ++++++------ util/fipstools/inject_hash/inject_hash.go | 25 +++++++++++++++---- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c901992824f..f0f6134d6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1384,9 +1384,9 @@ endif() if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) if(MINGW AND FIPS) - # The FIPS shared module on MinGW must contain its own copy of the - # winpthread implementation so that the integrity-checked DLL does not - # depend on an external libwinpthread DLL at runtime. + # If CMake's MinGW Threads::Threads target resolves to winpthread, keep it + # statically linked so the FIPS build products do not acquire an external + # libwinpthread DLL dependency at runtime. # # NOTE: crypto links Threads::Threads PUBLIC, so mutating the imported # target's interface here statically links winpthread into *every* artifact diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c index 2b888829c66..dea55b85ef9 100644 --- a/crypto/fipsmodule/bcm.c +++ b/crypto/fipsmodule/bcm.c @@ -189,9 +189,9 @@ extern const uint8_t BORINGSSL_bcm_rodata_end[]; #error "FIPS shared MinGW builds are only supported on 64-bit (x86_64) targets." #endif // Defined in fips_shared_support.c and filled in by inject_hash.go with the -// link-time preferred PE image base. See the comment there for why the runtime -// cannot read this from the in-memory PE header. +// link-time preferred PE image base used to normalize ASLR relocations. extern const uint64_t BORINGSSL_bcm_preferred_base; +#define BORINGSSL_BCM_PREFERRED_BASE_UNSET UINT64_C(0xBADC0FFEE0DDF00D) // MinGW is the only supported FIPS target whose hashed module region contains // base relocations: GCC emits .refptr indirection cells (and genuine imports @@ -222,9 +222,14 @@ static int hmac_update_module_region(HMAC_CTX *hmac_ctx, const uint8_t *start, return 0; } - // The Windows loader overwrites the in-memory OptionalHeader.ImageBase with - // the actual load address, so use the link-time base recorded by - // inject_hash.go to recover the ASLR load delta. + // Use the same link-time preferred base that inject_hash.go used when it + // hashed the on-disk PE image. Keeping this value in .fipshash avoids relying + // on loader-specific treatment of PE headers that are outside the module + // boundary. + if (BORINGSSL_bcm_preferred_base == BORINGSSL_BCM_PREFERRED_BASE_UNSET || + BORINGSSL_bcm_preferred_base == 0) { + return 0; + } const uintptr_t preferred_base = (uintptr_t)BORINGSSL_bcm_preferred_base; const uintptr_t load_delta = (uintptr_t)image_base - preferred_base; const IMAGE_DATA_DIRECTORY *const reloc_dir = diff --git a/crypto/fipsmodule/check_fips_mingw_sections.cmake b/crypto/fipsmodule/check_fips_mingw_sections.cmake index c36934bcd8d..3108b18efef 100644 --- a/crypto/fipsmodule/check_fips_mingw_sections.cmake +++ b/crypto/fipsmodule/check_fips_mingw_sections.cmake @@ -44,6 +44,11 @@ endif() # and flag any that indicate split-out module code or data. Dollar-suffixed # grouped .rdata sections contain MinGW refptr cells, so the build must merge # them into .rdata before this check runs. +# +# MinGW may also emit .ctors/.dtors records for module constructors and +# destructors. Those are loader registration metadata analogous to ELF +# .init_array/.fini_array or MSVC .CRT$XCU records, and are intentionally not +# treated as module rodata by this check. string(REPLACE "\n" ";" objdump_lines "${objdump_output}") set(forbidden_sections "") set(forbidden_section_metadata "") diff --git a/crypto/fipsmodule/fips_shared_support.c b/crypto/fipsmodule/fips_shared_support.c index d4d6cc4afd3..0e3c1aab9ee 100644 --- a/crypto/fipsmodule/fips_shared_support.c +++ b/crypto/fipsmodule/fips_shared_support.c @@ -33,18 +33,18 @@ const uint8_t BORINGSSL_bcm_text_hash[32] AWSLC_FIPS_HASH_SECTION = { #if defined(__MINGW32__) // BORINGSSL_bcm_preferred_base records the link-time (preferred) PE image base -// of the crypto DLL. When a module is relocated for ASLR, the Windows loader -// rewrites OptionalHeader.ImageBase in the in-memory PE header to the actual -// load address, so the runtime integrity check cannot recover the load delta -// from the header. inject_hash.go writes the real preferred base here during the -// build. Like BORINGSSL_bcm_text_hash it lives outside the hashed module -// boundary, and because it is an integer (not a pointer) it carries no base -// relocation of its own, so its value is identical on disk and in memory. +// of the crypto DLL. When a module is relocated for ASLR, the runtime integrity +// check subtracts the load delta from relocated cells before hashing, matching +// the on-disk preferred-base bytes that inject_hash.go measured. Like +// BORINGSSL_bcm_text_hash it lives outside the hashed module boundary, and +// because it is an integer (not a pointer) it carries no base relocation of its +// own, so its value is identical on disk and in memory. // // The initializer is a recognizable sentinel: if it survives to runtime, base // injection did not run and the integrity check fails closed. +#define BORINGSSL_BCM_PREFERRED_BASE_UNSET UINT64_C(0xBADC0FFEE0DDF00D) const uint64_t BORINGSSL_bcm_preferred_base AWSLC_FIPS_HASH_SECTION = - UINT64_C(0xBADC0FFEE0DDF00D); + BORINGSSL_BCM_PREFERRED_BASE_UNSET; #endif #else // C requires a translation unit to contain at least one declaration. Since diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go index 9fc9b803924..3d3c2a59277 100644 --- a/util/fipstools/inject_hash/inject_hash.go +++ b/util/fipstools/inject_hash/inject_hash.go @@ -457,9 +457,8 @@ func doMingw(objectBytes []byte) ([]byte, []byte, error) { } // Record the link-time preferred PE image base in BORINGSSL_bcm_preferred_base - // so the runtime integrity check can compute the ASLR load delta. The Windows - // loader rewrites OptionalHeader.ImageBase in the in-memory image once the DLL - // is relocated, so the runtime cannot read the preferred base from the header. + // so the runtime integrity check computes the ASLR load delta against the + // same preferred-base image bytes hashed here. var imageBase uint64 switch oh := object.OptionalHeader.(type) { case *pe.OptionalHeader64: @@ -504,13 +503,26 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma var isStatic bool var perm os.FileMode - if appleOS && windowsOS { - return fmt.Errorf("-apple and -windows are mutually exclusive") + modeCount := 0 + if appleOS { + modeCount++ + } + if windowsOS { + modeCount++ + } + if mingw { + modeCount++ + } + if modeCount > 1 { + return fmt.Errorf("-apple, -windows, and -mingw are mutually exclusive") } if windowsOS && len(mapFile) == 0 { return fmt.Errorf("-map is required when -windows is set") } + if !windowsOS && len(mapFile) != 0 { + return fmt.Errorf("-map is only valid when -windows is set") + } if len(arInput) > 0 { isStatic = true @@ -526,6 +538,9 @@ func do(outPath, oInput string, arInput string, appleOS bool, windowsOS bool, ma if windowsOS { return fmt.Errorf("only shared libraries can be handled on Windows") } + if mingw { + return fmt.Errorf("only shared libraries can be handled on MinGW") + } fi, err := os.Stat(arInput) if err != nil { From eaabe35142e7d8d47d48140e00e67c0e245e3e7b Mon Sep 17 00:00:00 2001 From: Justin Smith Date: Wed, 24 Jun 2026 08:18:28 -0400 Subject: [PATCH 19/19] PR feedback --- CMakeLists.txt | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f0f6134d6cf..675b02736b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1064,9 +1064,11 @@ if(FIPS) endif() # -ffunction-sections / -fdata-sections are incompatible with FIPS_SHARED. -# The Android CMake files set these flags. Although the MinGW build does not -# currently enable them, defensively disabling them here to keep the FIPS -# integrity check robust if added in the future. +# On ELF, gcc_fips_shared.lds already handles this: it gathers known split +# sections into .text and /DISCARD/s anything unexpected. Android and MinGW +# lack that linker-script safety net (Android's NDK toolchain injects these +# flags externally; MinGW/PE has no linker script support), so we override +# at the compiler level. if(FIPS_SHARED AND (ANDROID OR MINGW)) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-function-sections -fno-data-sections") @@ -1384,17 +1386,10 @@ endif() if(NOT CMAKE_SYSTEM_NAME MATCHES "^(Generic|Android|Emscripten)$") find_package(Threads REQUIRED) if(MINGW AND FIPS) - # If CMake's MinGW Threads::Threads target resolves to winpthread, keep it - # statically linked so the FIPS build products do not acquire an external - # libwinpthread DLL dependency at runtime. - # - # NOTE: crypto links Threads::Threads PUBLIC, so mutating the imported - # target's interface here statically links winpthread into *every* artifact - # in this build that pulls in threads (libssl.dll, bssl.exe, and the test - # binaries), not just the crypto DLL. That is intentional for the integrity - # boundary's sake and otherwise harmless (just some duplicated code), but it - # is broader than "the FIPS DLL only". This mutation is build-local and is - # not propagated to downstream consumers of the installed crypto target. + # Statically link winpthread so the FIPS module has no DLL dependency on it. + # This overwrites (not appends) INTERFACE_LINK_LIBRARIES because + # find_package(Threads) already set it to -lpthread (dynamic); appending + # would leave the dynamic entry on the link line. set_property(TARGET Threads::Threads PROPERTY INTERFACE_LINK_LIBRARIES "-Wl,-Bstatic,-lwinpthread;-Wl,-Bdynamic") endif()