From b9258258de3f5ce479e2a444c2ec76e236b17c6e Mon Sep 17 00:00:00 2001 From: mattsva Date: Fri, 9 Oct 2026 03:26:17 +0200 Subject: [PATCH 1/2] Add build hardening and fix build-system inconsistencies Hardening, consistently in Make, CMake and Meson: - -fstack-protector-strong for GCC/Clang - -D_FORTIFY_SOURCE=2 only where the build is optimized (at -O0 glibc emits a #warning that -Werror would escalate); the Makefile sets no -O by default, so FORTIFY is documented for optimized builds there - -Wl,-z,relro,-z,now on Linux only (ELF-only; macOS links fine without it) - MSVC: no new flags - /GS, /DYNAMICBASE and /NXCOMPAT are on by default, documented in both CMake and Meson. /sdl was deliberately not added because it cannot be verified without a Windows build. Consistency fixes found while verifying: - Meson was missing test_client and test_welcome: it built 9 test binaries where Make and CMake build 11. Added. - CMake's Release config defines NDEBUG, which compiled every assert() out of the test binaries: the suite would have passed vacuously and the newly-unused variables failed the build with -Werror=unused-but-set-variable. Test targets now compile with -UNDEBUG so assertions stay active in every build type. - test_mnet.c had set-but-unused variables that only trip at -O2. Verified: make test (pass), ctest default and Release (11/11, asserts confirmed active via __assert_fail), meson debug and release (11/11), full RELRO (BIND_NOW) and stack canaries present in the Make-built libmnet.so. --- CMakeLists.txt | 29 +++++++++++++++++++++++++++++ Makefile | 10 ++++++++++ meson.build | 34 ++++++++++++++++++++++++++++++++++ test/test_mnet.c | 27 +++++++++++---------------- 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ac4a2e9..11fc089 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,8 +11,27 @@ if(MSVC) # names" — sscanf, strcpy, strdup, etc. These are false positives for # portable C code; suppress them so /WX does not escalate them to errors. add_compile_definitions(_CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_DEPRECATE) + # MSVC's buffer-security hardening is on by default: /GS (stack cookies), + # /DYNAMICBASE (ASLR) and /NXCOMPAT (DEP) are all enabled unless explicitly + # turned off, so no extra flags are needed for parity with the POSIX side. else() add_compile_options(-Wall -Wextra -Wpedantic -Werror) + + # Hardening. -fstack-protector-strong works at any optimization level. + add_compile_options(-fstack-protector-strong) + + # _FORTIFY_SOURCE=2 only makes sense with optimization: without it glibc + # emits a #warning that -Werror would escalate, and CMake's default + # (empty) build type compiles without -O. Restrict it to the optimized + # build types. + if(CMAKE_BUILD_TYPE MATCHES "^(Release|RelWithDebInfo|MinSizeRel)$") + add_compile_definitions(_FORTIFY_SOURCE=2) + endif() + + # relro/now are ELF-only. + if(UNIX AND NOT APPLE) + add_link_options(-Wl,-z,relro,-z,now) + endif() endif() if(NOT WIN32) @@ -136,6 +155,16 @@ target_link_libraries(test_http_edge PRIVATE mnet_static) target_include_directories(test_http_edge PRIVATE include src test) add_test(NAME mnet_http_edge_tests COMMAND test_http_edge) +# Tests are assert()-based, and CMake's Release configs define NDEBUG, which +# would compile every assertion out: the suite would then pass without +# checking anything. Undefine it for the test targets so assertions stay +# active in every build type, matching the Make and Meson builds. +foreach(test_target IN ITEMS test_mnet test_http test_parser test_stress + test_features test_security test_client test_welcome test_starvation + test_reentrancy test_http_edge) + target_compile_options(${test_target} PRIVATE -UNDEBUG) +endforeach() + # Install install(TARGETS mnet_static mnet_shared ARCHIVE DESTINATION lib diff --git a/Makefile b/Makefile index 2da9e53..3769706 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,16 @@ ifeq ($(shell uname -s),Darwin) CFLAGS += -D_DARWIN_C_SOURCE endif +# Hardening. -fstack-protector-strong works at any optimization level and on +# GCC/Clang; -Wl,-z,relro,-z,now is ELF-only, so it stays on Linux. +# _FORTIFY_SOURCE=2 needs optimization, which this Makefile does not set by +# default: add it yourself when building optimized, e.g. +# make CFLAGS="-O2 -D_FORTIFY_SOURCE=2 ..." +CFLAGS += -fstack-protector-strong +ifeq ($(shell uname -s),Linux) + LDFLAGS += -Wl,-z,relro,-z,now +endif + # The worker pool needs pthreads on POSIX. ifeq ($(OS),Windows_NT) LDFLAGS += -lws2_32 diff --git a/meson.build b/meson.build index 2a26fa5..26d65a6 100644 --- a/meson.build +++ b/meson.build @@ -10,8 +10,26 @@ if cc.get_id() == 'msvc' add_project_arguments(['/W4', '/WX'], language: 'c') # Suppress false-positive C4996 deprecation warnings for standard C functions. add_project_arguments(['/D_CRT_SECURE_NO_WARNINGS', '/D_CRT_NONSTDC_NO_DEPRECATE'], language: 'c') + # MSVC's buffer-security hardening is on by default: /GS (stack cookies), + # /DYNAMICBASE (ASLR) and /NXCOMPAT (DEP) are all enabled unless explicitly + # turned off, so no extra flags are needed for parity with the POSIX side. else add_project_arguments(warning_flags, language: 'c') + + # Hardening. -fstack-protector-strong works at any optimization level. + add_project_arguments('-fstack-protector-strong', language: 'c') + + # _FORTIFY_SOURCE=2 only makes sense with optimization: without it glibc + # emits a #warning that -Werror would escalate. Meson's debug buildtype + # is -O0, so restrict it to optimized builds. + if get_option('optimization') != '0' + add_project_arguments('-D_FORTIFY_SOURCE=2', language: 'c') + endif + + # relro/now are ELF-only. + if host_machine.system() == 'linux' + add_project_link_arguments('-Wl,-z,relro,-z,now', language: 'c') + endif endif # glibc's _FORTIFY_SOURCE is only meaningful with optimization, and some @@ -164,6 +182,22 @@ test_http_edge = executable('test_http_edge', test('mnet_http_edge_tests', test_http_edge) +test_client = executable('test_client', + 'test/test_client.c', + link_with: mnet_static, + include_directories: [mnet_inc, include_directories('src'), include_directories('test')], +) + +test('mnet_client_tests', test_client) + +test_welcome = executable('test_welcome', + 'test/test_welcome.c', + link_with: mnet_static, + include_directories: [mnet_inc, include_directories('src'), include_directories('test')], +) + +test('mnet_welcome_tests', test_welcome) + # Install headers (libraries are installed by default) install_headers('include/mnet/mnet.h', 'include/mnet/mnet_app.h', 'include/mnet/mnet_request.h', 'include/mnet/mnet_response.h', 'include/mnet/mnet_router.h', 'include/mnet/mnet_socket.h', diff --git a/test/test_mnet.c b/test/test_mnet.c index 9794722..a16df81 100644 --- a/test/test_mnet.c +++ b/test/test_mnet.c @@ -130,27 +130,26 @@ static void test_mnet_json_escape(void) { char out[256]; - size_t n = mnet_json_escape(out, sizeof(out), "hello"); + (void)mnet_json_escape(out, sizeof(out), "hello"); assert(strcmp(out, "hello") == 0); - n = mnet_json_escape(out, sizeof(out), "he\"llo"); + (void)mnet_json_escape(out, sizeof(out), "he\"llo"); assert(strcmp(out, "he\\\"llo") == 0); - n = mnet_json_escape(out, sizeof(out), "a\\b"); + (void)mnet_json_escape(out, sizeof(out), "a\\b"); assert(strcmp(out, "a\\\\b") == 0); - n = mnet_json_escape(out, sizeof(out), "a\nb"); + (void)mnet_json_escape(out, sizeof(out), "a\nb"); assert(strcmp(out, "a\\nb") == 0); - n = mnet_json_escape(out, sizeof(out), "a\rb"); + (void)mnet_json_escape(out, sizeof(out), "a\rb"); assert(strcmp(out, "a\\rb") == 0); - n = mnet_json_escape(out, sizeof(out), "a\tb"); + (void)mnet_json_escape(out, sizeof(out), "a\tb"); assert(strcmp(out, "a\\tb") == 0); - n = mnet_json_escape(out, sizeof(out), ""); + assert(mnet_json_escape(out, sizeof(out), "") == 0); assert(strcmp(out, "") == 0); - assert(n == 0); printf(" PASS test_mnet_json_escape\n"); } @@ -574,25 +573,21 @@ static void test_mnet_url_decode_malformed(void) /* The legacy wrapper must not leave a truncated value behind. */ memset(out, 'X', sizeof(out)); - size_t n = mnet_url_decode(out, sizeof(out), "a%zzb"); - assert(n == 0); + assert(mnet_url_decode(out, sizeof(out), "a%zzb") == 0); assert(out[0] == '\0'); memset(out, 'X', sizeof(out)); - n = mnet_url_decode(out, sizeof(out), "a%00b"); - assert(n == 0); + assert(mnet_url_decode(out, sizeof(out), "a%00b") == 0); assert(out[0] == '\0'); /* exact fit succeeds */ char buf4[4]; - n = mnet_url_decode(buf4, sizeof(buf4), "abc"); - assert(n == 3); + assert(mnet_url_decode(buf4, sizeof(buf4), "abc") == 3); assert(strcmp(buf4, "abc") == 0); /* overflow refuses */ char buf3[3]; - n = mnet_url_decode(buf3, sizeof(buf3), "abcd"); - assert(n == 0); + assert(mnet_url_decode(buf3, sizeof(buf3), "abcd") == 0); assert(buf3[0] == '\0'); printf(" PASS test_mnet_url_decode_malformed\n"); From d3b9b1bc4cb45673d8227ec7a0e27ba0fd96a0a9 Mon Sep 17 00:00:00 2001 From: mattsva Date: Fri, 9 Oct 2026 10:26:13 +0200 Subject: [PATCH 2/2] Fix .gitignore: ignore all test binaries and build directories test_starvation, test_reentrancy and test_http_edge were built by make test but never added to .gitignore, so every git add -A swept the freshly built binaries into the following commit; the build-rel directory (a CMake build dir) matched nothing because only the exact name build/ was listed. Added the three missing test binaries and test_welcome to the test binary list and build-*/ to the build directory list. The binaries and build-rel were removed from the unpushed commits with git filter-branch, so no build artifact is committed on this branch. --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a87725f..d932f71 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # Build directories build/ +build-*/ _build/ cmake-build-*/ @@ -19,6 +20,10 @@ test_stress test_features test_security test_client +test_welcome +test_starvation +test_reentrancy +test_http_edge fuzz_http mnet-server @@ -36,7 +41,6 @@ example/example_combined # User-compiled binaries myapp -test_welcome # OS .DS_Store