From e0f6367446d8f3f2f37acb27f9e2fc0f51768721 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Sun, 12 Feb 2023 21:53:08 +0000 Subject: [PATCH] cc-wrapper, binutils-wrapper: add tests hardening-flags-handling most tests use debian-devscripts' hardening-check, so only work on ELF systems and can only detect a limited subset of flags. some extra tests actually execute fortify-protected programs and should be slightly more universally applicable. --- pkgs/test/cc-wrapper/fortify1-example.c | 16 + pkgs/test/cc-wrapper/fortify2-example.c | 16 + pkgs/test/cc-wrapper/fortify3-example.c | 13 + pkgs/test/cc-wrapper/hardening.nix | 396 ++++++++++++++++++++++++ pkgs/test/default.nix | 8 + 5 files changed, 449 insertions(+) create mode 100644 pkgs/test/cc-wrapper/fortify1-example.c create mode 100644 pkgs/test/cc-wrapper/fortify2-example.c create mode 100644 pkgs/test/cc-wrapper/fortify3-example.c create mode 100644 pkgs/test/cc-wrapper/hardening.nix diff --git a/pkgs/test/cc-wrapper/fortify1-example.c b/pkgs/test/cc-wrapper/fortify1-example.c new file mode 100644 index 000000000000..48b9c268e728 --- /dev/null +++ b/pkgs/test/cc-wrapper/fortify1-example.c @@ -0,0 +1,16 @@ +/* an example that should be protected by FORTIFY_SOURCE=1 */ +#include +#include +#include + + +int main(int argc, char *argv[]) { + /* allocate on the heap so we're likely to get an + * over-allocation and can be more sure that a + * failure is because of fortify protection rather + * than a genuine segfault */ + char* buffer = malloc(sizeof(char) * 7); + strcpy(buffer, argv[1]); + puts(buffer); + return 0; +} diff --git a/pkgs/test/cc-wrapper/fortify2-example.c b/pkgs/test/cc-wrapper/fortify2-example.c new file mode 100644 index 000000000000..dfb5a8e87294 --- /dev/null +++ b/pkgs/test/cc-wrapper/fortify2-example.c @@ -0,0 +1,16 @@ +/* an example that should be protected by FORTIFY_SOURCE=2 but + * not FORTIFY_SOURCE=1 */ +#include +#include + +struct buffer_with_pad { + char buffer[7]; + char pad[25]; +}; + +int main(int argc, char *argv[]) { + struct buffer_with_pad b; + strcpy(b.buffer, argv[1]); + puts(b.buffer); + return 0; +} diff --git a/pkgs/test/cc-wrapper/fortify3-example.c b/pkgs/test/cc-wrapper/fortify3-example.c new file mode 100644 index 000000000000..9a0a5f4792c3 --- /dev/null +++ b/pkgs/test/cc-wrapper/fortify3-example.c @@ -0,0 +1,13 @@ +/* an example that should be protected by FORTIFY_SOURCE=3 but + * not FORTIFY_SOURCE=2 */ +#include +#include +#include + + +int main(int argc, char *argv[]) { + char* buffer = malloc(atoi(argv[2])); + strcpy(buffer, argv[1]); + puts(buffer); + return 0; +} diff --git a/pkgs/test/cc-wrapper/hardening.nix b/pkgs/test/cc-wrapper/hardening.nix new file mode 100644 index 000000000000..41ddaefdfea8 --- /dev/null +++ b/pkgs/test/cc-wrapper/hardening.nix @@ -0,0 +1,396 @@ +{ lib +, stdenv +, runCommand +, runCommandWith +, runCommandCC +, debian-devscripts +}: + +let + # writeCBin from trivial-builders won't let us choose + # our own stdenv + writeCBinWithStdenv = codePath: stdenv': env: runCommandWith { + name = "test-bin"; + stdenv = stdenv'; + derivationArgs = { + inherit codePath; + preferLocalBuild = true; + allowSubstitutes = false; + } // env; + } '' + [ -n "$preBuild" ] && eval "$preBuild" + n=$out/bin/test-bin + mkdir -p "$(dirname "$n")" + cp "$codePath" code.c + NIX_DEBUG=1 $CC -x c code.c -O1 $TEST_EXTRA_FLAGS -o "$n" + ''; + + f1exampleWithStdEnv = writeCBinWithStdenv ./fortify1-example.c; + f2exampleWithStdEnv = writeCBinWithStdenv ./fortify2-example.c; + f3exampleWithStdEnv = writeCBinWithStdenv ./fortify3-example.c; + + stdenvUnsupport = additionalUnsupported: stdenv.override { + cc = stdenv.cc.override { + cc = (lib.extendDerivation true { + hardeningUnsupportedFlags = (stdenv.cc.cc.hardeningUnsupportedFlags or []) ++ additionalUnsupported; + } stdenv.cc.cc); + }; + allowedRequisites = null; + }; + + checkTestBin = testBin: { + # can only test flags that are detectable by hardening-check + ignoreBindNow ? true, + ignoreFortify ? true, + ignorePie ? true, + ignoreRelRO ? true, + ignoreStackProtector ? true, + expectFailure ? false, + }: let + expectFailureClause = lib.optionalString expectFailure + " && echo 'ERROR: Expected hardening-check to fail, but it passed!' >&2 && exit 1"; + in runCommandCC "check-test-bin" { + nativeBuildInputs = [ debian-devscripts ]; + buildInputs = [ testBin ]; + meta.platforms = lib.platforms.linux; # ELF-reliant + } '' + hardening-check --nocfprotection \ + ${lib.optionalString ignoreBindNow "--nobindnow"} \ + ${lib.optionalString ignoreFortify "--nofortify"} \ + ${lib.optionalString ignorePie "--nopie"} \ + ${lib.optionalString ignoreRelRO "--norelro"} \ + ${lib.optionalString ignoreStackProtector "--nostackprotector"} \ + $(PATH=$HOST_PATH type -P test-bin) ${expectFailureClause} + touch $out + ''; + + nameDrvAfterAttrName = builtins.mapAttrs (name: drv: + drv.overrideAttrs (_: { name = "test-${name}"; }) + ); + + # returning a specific exit code when aborting due to a fortify + # check isn't mandated. so it's better to just ensure that a + # nonzero exit code is returned when we go a single byte beyond + # the buffer, with the example programs being designed to be + # unlikely to genuinely segfault for such a small overflow. + fortifyExecTest = testBin: runCommand "exec-test" { + buildInputs = [ + testBin + ]; + meta.broken = !(stdenv.buildPlatform.canExecute stdenv.hostPlatform); + } '' + ( + export PATH=$HOST_PATH + echo "Saturated buffer:" # check program isn't completly broken + test-bin 012345 7 + echo "One byte too far:" # eighth byte being the null terminator + (! test-bin 0123456 7) || (echo 'Expected failure, but succeeded!' && exit 1) + ) + echo "Expected behaviour observed" + touch $out + ''; + + brokenIf = cond: drv: if cond then drv.overrideAttrs (old: { meta = old.meta or {} // { broken = true; }; }) else drv; + +in nameDrvAfterAttrName ({ + bindNowExplicitEnabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "bindnow" ]; + }) { + ignoreBindNow = false; + }); + + # musl implementation undetectable by this means even if present + fortifyExplicitEnabled = brokenIf stdenv.hostPlatform.isMusl (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + }) { + ignoreFortify = false; + }); + + fortify1ExplicitEnabledExecTest = fortifyExecTest (f1exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + }); + + # musl implementation is effectively FORTIFY_SOURCE=1-only, + # clang-on-glibc also only appears to support FORTIFY_SOURCE=1 (!) + fortifyExplicitEnabledExecTest = brokenIf ( + stdenv.hostPlatform.isMusl || (stdenv.cc.isClang && stdenv.hostPlatform.libc == "glibc") + ) (fortifyExecTest (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + })); + + fortify3ExplicitEnabled = brokenIf ( + stdenv.hostPlatform.isMusl || !stdenv.cc.isGNU || lib.versionOlder stdenv.cc.version "12" + ) (checkTestBin (f3exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify3" ]; + }) { + ignoreFortify = false; + }); + + # musl implementation is effectively FORTIFY_SOURCE=1-only + fortify3ExplicitEnabledExecTest = brokenIf ( + stdenv.hostPlatform.isMusl || !stdenv.cc.isGNU || lib.versionOlder stdenv.cc.version "12" + ) (fortifyExecTest (f3exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify3" ]; + })); + + pieExplicitEnabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "pie" ]; + }) { + ignorePie = false; + }); + + relROExplicitEnabled = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "relro" ]; + }) { + ignoreRelRO = false; + }; + + stackProtectorExplicitEnabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "stackprotector" ]; + }) { + ignoreStackProtector = false; + }); + + bindNowExplicitDisabled = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "bindnow" ]; + }) { + ignoreBindNow = false; + expectFailure = true; + }; + + fortifyExplicitDisabled = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortify3ExplicitDisabled = checkTestBin (f3exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify3" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortifyExplicitDisabledDisablesFortify3 = checkTestBin (f3exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify3" ]; + hardeningDisable = [ "fortify" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortify3ExplicitDisabledDoesntDisableFortify = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + hardeningDisable = [ "fortify3" ]; + }) { + ignoreFortify = false; + }; + + pieExplicitDisabled = brokenIf ( + stdenv.hostPlatform.isMusl && stdenv.cc.isClang + ) (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "pie" ]; + }) { + ignorePie = false; + expectFailure = true; + }); + + # can't force-disable ("partial"?) relro + relROExplicitDisabled = brokenIf true (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "pie" ]; + }) { + ignoreRelRO = false; + expectFailure = true; + }); + + stackProtectorExplicitDisabled = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "stackprotector" ]; + }) { + ignoreStackProtector = false; + expectFailure = true; + }; + + # most flags can't be "unsupported" by compiler alone and + # binutils doesn't have an accessible hardeningUnsupportedFlags + # mechanism, so can only test a couple of flags through altered + # stdenv trickery + + fortifyStdenvUnsupp = checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["fortify"]) { + hardeningEnable = [ "fortify" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortify3StdenvUnsupp = checkTestBin (f3exampleWithStdEnv (stdenvUnsupport ["fortify3"]) { + hardeningEnable = [ "fortify3" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortifyStdenvUnsuppUnsupportsFortify3 = checkTestBin (f3exampleWithStdEnv (stdenvUnsupport ["fortify"]) { + hardeningEnable = [ "fortify3" ]; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + fortify3StdenvUnsuppDoesntUnsuppFortify = brokenIf stdenv.hostPlatform.isMusl (checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["fortify3"]) { + hardeningEnable = [ "fortify" ]; + }) { + ignoreFortify = false; + }); + + fortify3StdenvUnsuppDoesntUnsuppFortifyExecTest = fortifyExecTest (f2exampleWithStdEnv (stdenvUnsupport ["fortify3"]) { + hardeningEnable = [ "fortify" ]; + }); + + stackProtectorStdenvUnsupp = checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["stackprotector"]) { + hardeningEnable = [ "stackprotector" ]; + }) { + ignoreStackProtector = false; + expectFailure = true; + }; + + # NIX_HARDENING_ENABLE set in the shell overrides hardeningDisable + # and hardeningEnable + + stackProtectorReenabledEnv = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "stackprotector" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="stackprotector" + ''; + }) { + ignoreStackProtector = false; + }; + + stackProtectorReenabledFromAllEnv = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "all" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="stackprotector" + ''; + }) { + ignoreStackProtector = false; + }; + + stackProtectorRedisabledEnv = checkTestBin (f2exampleWithStdEnv stdenv { + hardeningEnable = [ "stackprotector" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="" + ''; + }) { + ignoreStackProtector = false; + expectFailure = true; + }; + + fortify3EnabledEnvEnablesFortify = brokenIf stdenv.hostPlatform.isMusl (checkTestBin (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" "fortify3" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="fortify3" + ''; + }) { + ignoreFortify = false; + }); + + fortify3EnabledEnvEnablesFortifyExecTest = fortifyExecTest (f2exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" "fortify3" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="fortify3" + ''; + }); + + fortifyEnabledEnvDoesntEnableFortify3 = checkTestBin (f3exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" "fortify3" ]; + preBuild = '' + export NIX_HARDENING_ENABLE="fortify" + ''; + }) { + ignoreFortify = false; + expectFailure = true; + }; + + # NIX_HARDENING_ENABLE can't enable an unsupported feature + + stackProtectorUnsupportedEnabledEnv = checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["stackprotector"]) { + preBuild = '' + export NIX_HARDENING_ENABLE="stackprotector" + ''; + }) { + ignoreStackProtector = false; + expectFailure = true; + }; + + # undetectable by this means on static even if present + fortify1ExplicitEnabledCmdlineDisabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f1exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + preBuild = '' + export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=0' + ''; + }) { + ignoreFortify = false; + expectFailure = true; + }); + + # musl implementation undetectable by this means even if present + fortify1ExplicitDisabledCmdlineEnabled = brokenIf ( + stdenv.hostPlatform.isMusl || stdenv.hostPlatform.isStatic + ) (checkTestBin (f1exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" ]; + preBuild = '' + export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=1' + ''; + }) { + ignoreFortify = false; + }); + + fortify1ExplicitDisabledCmdlineEnabledExecTest = fortifyExecTest (f1exampleWithStdEnv stdenv { + hardeningDisable = [ "fortify" ]; + preBuild = '' + export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=1' + ''; + }); + + fortify1ExplicitEnabledCmdlineDisabledNoWarn = f1exampleWithStdEnv stdenv { + hardeningEnable = [ "fortify" ]; + preBuild = '' + export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=0 -Werror' + ''; + }; + +} // (let + tb = f2exampleWithStdEnv stdenv { + hardeningDisable = [ "all" ]; + hardeningEnable = [ "fortify" "pie" ]; + }; +in { + + allExplicitDisabledBindNow = checkTestBin tb { + ignoreBindNow = false; + expectFailure = true; + }; + + allExplicitDisabledFortify = checkTestBin tb { + ignoreFortify = false; + expectFailure = true; + }; + + allExplicitDisabledPie = brokenIf ( + stdenv.hostPlatform.isMusl && stdenv.cc.isClang + ) (checkTestBin tb { + ignorePie = false; + expectFailure = true; + }); + + # can't force-disable ("partial"?) relro + allExplicitDisabledRelRO = brokenIf true (checkTestBin tb { + ignoreRelRO = false; + expectFailure = true; + }); + + allExplicitDisabledStackProtector = checkTestBin tb { + ignoreStackProtector = false; + expectFailure = true; + }; +})) diff --git a/pkgs/test/default.nix b/pkgs/test/default.nix index d6fd75359fc4..c479070c6078 100644 --- a/pkgs/test/default.nix +++ b/pkgs/test/default.nix @@ -23,6 +23,14 @@ with pkgs; stdenv-inputs = callPackage ./stdenv-inputs { }; stdenv = callPackage ./stdenv { }; + hardeningFlags = recurseIntoAttrs (callPackage ./cc-wrapper/hardening.nix {}); + hardeningFlags-gcc = recurseIntoAttrs (callPackage ./cc-wrapper/hardening.nix { + stdenv = gccStdenv; + }); + hardeningFlags-clang = recurseIntoAttrs (callPackage ./cc-wrapper/hardening.nix { + stdenv = llvmPackages.stdenv; + }); + config = callPackage ./config.nix { }; haskell = callPackage ./haskell { };