From b049d575176858fe8f573901711ae1310ff70885 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 7bac21b44d0..e548e7bf7f0 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -680,12 +680,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 f8f43e4aa4a..11d8899d27b 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -790,6 +790,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 9fe2498daf581acbacb18f12ca79841d7071fdab 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 f0f318a1156..28d9a09ddf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1481,6 +1481,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 6203c7ebf0481270ae2e364cc77f09f5d9140787 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 31a714b007f..49073fee5ae 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 00501971252010b43f3d156c70131ff2efae4038 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 28af050760ea925ae69e3ca11da4d617034ca1b1 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 49073fee5ae..3c4c0faeeee 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 dc28a06e746c5178bf241fe76fca1b384ac4e0cf 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 28d9a09ddf4..da16a2ab48d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1482,8 +1482,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 4d8ccb39fb76ce261ccf60141fe3e20858f09a46 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 | 7 +++++-- crypto/fipsmodule/CMakeLists.txt | 17 +++++++++++------ util/fipstools/inject_hash/inject_hash.go | 20 ++++++++------------ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index da16a2ab48d..5975a8e08b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1163,7 +1163,7 @@ endif() # FIPS_SHARED's linker script does not collect per-symbol sections, which causes # `ld -r` to fail. Disable GNU-style section flags even when inherited from a -# toolchain or CFLAGS. +# toolchain or CFLAGS (e.g. Android CMake files, or a future MinGW default). if(FIPS_SHARED AND (GCC OR CLANG) AND NOT MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-function-sections -fno-data-sections") @@ -1481,7 +1481,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 11d8899d27b..b87b542a8cf 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -796,20 +796,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 b23a863c5a4217fbec03ee0b0a2208665e322c89 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 39799dd3a2d..8d23868dfea 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -255,6 +255,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 79cfca5393b8de1e32a0e2be6190990dde4da9ec 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 b87b542a8cf..a98142a917a 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -803,12 +803,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 a6d4d6e807827dbd06f087e87ae834fb258c74e8 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 5975a8e08b5..745a6c47082 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1171,6 +1171,18 @@ if(FIPS_SHARED AND (GCC OR CLANG) AND NOT MSVC) "${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) # Apply its x86_64 AVX-512 implication after ARCH is detected. diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt index a98142a917a..da120b47776 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -801,6 +801,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 @@ -814,7 +833,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 e7b37a88c2b9b0d82a24e80b5751d8869fd78159 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 e548e7bf7f0..3ae304016b0 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -622,6 +622,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 da120b47776..258d277464e 100644 --- a/crypto/fipsmodule/CMakeLists.txt +++ b/crypto/fipsmodule/CMakeLists.txt @@ -805,11 +805,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}") @@ -820,6 +821,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 @@ -830,13 +844,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 9dd146434c30e0617905ca0541d55b84bb21cdf7 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 b499a80064c04b61a29b531c4f325064b0810de6 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 0ce0f9fb157..e6ac43134ff 100644 --- a/crypto/thread_win.c +++ b/crypto/thread_win.c @@ -185,6 +185,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. @@ -192,13 +193,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 e3a73bd3c2162c59bf0c14b69225a05820635c7a 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 745a6c47082..58d26056d34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1497,6 +1497,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 f808b9087d70bfebec85f9565a2db9f2893b1a1b 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 8d23868dfea..971c2d72b73 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -257,7 +257,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: | @@ -268,7 +268,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 c4046a1c84ed5704056ac66243aaabaa63125923 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 -------- 1 file changed, 8 deletions(-) diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 3ae304016b0..e548e7bf7f0 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -622,14 +622,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() From f5ef29de3bcc3e8cbe0d9b8a8b676653f62795a3 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 971c2d72b73..bb8f58ccba5 100644 --- a/.github/workflows/windows-alt.yml +++ b/.github/workflows/windows-alt.yml @@ -271,7 +271,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 775833b047e674c8673ffabb52326ffe6eb18ed9 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 58d26056d34..a6b4690369e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1494,9 +1494,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 9832950ec4b949d8f80602389686073f1c14c2cf 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 | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6b4690369e..23d57ebd461 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1164,6 +1164,10 @@ endif() # FIPS_SHARED's linker script does not collect per-symbol sections, which causes # `ld -r` to fail. Disable GNU-style section flags even when inherited from a # toolchain or CFLAGS (e.g. Android CMake files, or a future MinGW default). +# On ELF, gcc_fips_shared.lds gathers known split sections into .text and +# /DISCARD/s anything unexpected, but Android and MinGW lack that safety net +# (Android's NDK toolchain injects these flags externally; MinGW/PE has no +# linker script support). if(FIPS_SHARED AND (GCC OR CLANG) AND NOT MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-function-sections -fno-data-sections") @@ -1494,17 +1498,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()