nixpkgs/pkgs/test/cc-wrapper/hardening.nix
Maximilian Bosch aaeeef5b6c
stdenv: fix custom hardening settings when using __structuredAttrs = true;
Replaces / Closes #353131

A while ago `postgresql` switched to using structured attrs[1]. In the
PR it was reported that this made postgresql notably slower when
importing SQL dumps[2].

After a bit of debugging it turned out that the hardening was entirely
missing and the following combination of settings was the culprit:

    hardeningEnable = [ "pie" ];
    __structuredAttrs = true;

I.e. the combination of custom hardening settings and structured attrs.

What happened here is that internally the default and enabled hardening
flags get written into `NIX_HARDENING_ENABLE`. However, the value is a list
and the setting is not in the `env` section. This means that in the
structured-attrs case we get something like

    declare -ax NIX_HARDENING_ENABLE=([0]="bindnow" [1]="format" [2]="fortify" [3]="fortify3" [4]="pic" [5]="relro" [6]="stackprotector" [7]="strictoverflow" [8]="zerocallusedregs" [9]="pie")

i.e. an actual array rather than a string with all hardening flags being
space-separated which is what the hardening code of the cc-wrapper
expects[3].

This only happens if `hardeningEnable` or `hardeningDisable` are
explicitly set by a derivation: if none of those are set,
`NIX_HARDENING_ENABLE` won't be set by `stdenv.mkDerivation` and the
default hardening flags are configured by the setup hook of the
cc-wrapper[4].

In other words, this _only_ applies to derivations that have both custom
hardening settings _and_ `__structuredAttrs = true;`.

All values of `NIX_HARDENING_ENABLE` are well-known, so we don't have to
worry about escaping issues. Just forcing it to a string by
concatenating the list everytime solves the issue without additional
issues like eval errors when inheriting `env` from a structuredAttrs
derivation[5]. The price we're paying is a full rebuild.

[1] https://github.com/NixOS/nixpkgs/pull/294504
[2] https://github.com/NixOS/nixpkgs/pull/294504#issuecomment-2451482522
[3] cf3e5d3744/pkgs/build-support/cc-wrapper/add-hardening.sh (L9)
[4] cf3e5d3744/pkgs/build-support/cc-wrapper/setup-hook.sh (L114)
[5] 1e84a7fb95
2024-11-02 22:30:01 +01:00

476 lines
15 KiB
Nix

{ lib
, stdenv
, runCommand
, runCommandWith
, runCommandCC
, hello
, 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 "$postConfigure" ] && eval "$postConfigure"
[ -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;
# for when we need a slightly more complicated program
helloWithStdEnv = stdenv': env: (hello.override { stdenv = stdenv'; }).overrideAttrs ({
preBuild = ''
export CFLAGS="$TEST_EXTRA_FLAGS"
'';
NIX_DEBUG = "1";
postFixup = ''
cp $out/bin/hello $out/bin/test-bin
'';
} // env);
stdenvUnsupport = additionalUnsupported: stdenv.override {
cc = stdenv.cc.override {
cc = (lib.extendDerivation true rec {
# this is ugly - have to cross-reference from
# hardeningUnsupportedFlagsByTargetPlatform to hardeningUnsupportedFlags
# because the finalAttrs mechanism that hardeningUnsupportedFlagsByTargetPlatform
# implementations use to do this won't work with lib.extendDerivation.
# but it's simplified by the fact that targetPlatform is already fixed
# at this point.
hardeningUnsupportedFlagsByTargetPlatform = _: hardeningUnsupportedFlags;
hardeningUnsupportedFlags = (
if stdenv.cc.cc ? hardeningUnsupportedFlagsByTargetPlatform
then stdenv.cc.cc.hardeningUnsupportedFlagsByTargetPlatform stdenv.targetPlatform
else (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,
ignoreStackClashProtection ? true,
expectFailure ? false,
}: let
stackClashStr = "Stack clash protection: yes";
expectFailureClause = lib.optionalString expectFailure
" && echo 'ERROR: Expected hardening-check to fail, but it passed!' >&2 && false";
in runCommandCC "check-test-bin" {
nativeBuildInputs = [ debian-devscripts ];
buildInputs = [ testBin ];
meta.platforms = if ignoreStackClashProtection
then lib.platforms.linux # ELF-reliant
else [ "x86_64-linux" ]; # stackclashprotection test looks for x86-specific instructions
} (''
if ${lib.optionalString (!expectFailure) "!"} {
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) | tee $out
'' + lib.optionalString (!ignoreStackClashProtection) ''
# stack clash protection doesn't actually affect the exit code of
# hardening-check (likely authors think false negatives too common)
{ grep -F '${stackClashStr}' $out || { echo "Didn't find '${stackClashStr}' in output" && false ;} ;}
'' + ''
} ; then
'' + lib.optionalString expectFailure ''
echo 'ERROR: Expected hardening-check to fail, but it passed!' >&2
'' + ''
exit 2
fi
'');
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;
});
pieExplicitEnabledStructuredAttrs = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f2exampleWithStdEnv stdenv {
hardeningEnable = [ "pie" ];
__structuredAttrs = true;
}) {
ignorePie = false;
});
relROExplicitEnabled = checkTestBin (f2exampleWithStdEnv stdenv {
hardeningEnable = [ "relro" ];
}) {
ignoreRelRO = false;
};
stackProtectorExplicitEnabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f2exampleWithStdEnv stdenv {
hardeningEnable = [ "stackprotector" ];
}) {
ignoreStackProtector = false;
});
# protection patterns generated by clang not detectable?
stackClashProtectionExplicitEnabled = brokenIf stdenv.cc.isClang (checkTestBin (helloWithStdEnv stdenv {
hardeningEnable = [ "stackclashprotection" ];
}) {
ignoreStackClashProtection = 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;
};
stackClashProtectionExplicitDisabled = checkTestBin (helloWithStdEnv stdenv {
hardeningDisable = [ "stackclashprotection" ];
}) {
ignoreStackClashProtection = 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" "fortify3"]) {
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;
};
# musl implementation undetectable by this means even if present
fortify3StdenvUnsuppDoesntUnsuppFortify1 = brokenIf stdenv.hostPlatform.isMusl (checkTestBin (f1exampleWithStdEnv (stdenvUnsupport ["fortify3"]) {
hardeningEnable = [ "fortify" ];
}) {
ignoreFortify = false;
});
fortify3StdenvUnsuppDoesntUnsuppFortify1ExecTest = fortifyExecTest (f1exampleWithStdEnv (stdenvUnsupport ["fortify3"]) {
hardeningEnable = [ "fortify" ];
});
stackProtectorStdenvUnsupp = checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["stackprotector"]) {
hardeningEnable = [ "stackprotector" ];
}) {
ignoreStackProtector = false;
expectFailure = true;
};
stackClashProtectionStdenvUnsupp = checkTestBin (helloWithStdEnv (stdenvUnsupport ["stackclashprotection"]) {
hardeningEnable = [ "stackclashprotection" ];
}) {
ignoreStackClashProtection = false;
expectFailure = true;
};
# NIX_HARDENING_ENABLE set in the shell overrides hardeningDisable
# and hardeningEnable
stackProtectorReenabledEnv = checkTestBin (f2exampleWithStdEnv stdenv {
hardeningDisable = [ "stackprotector" ];
postConfigure = ''
export NIX_HARDENING_ENABLE="stackprotector"
'';
}) {
ignoreStackProtector = false;
};
stackProtectorReenabledFromAllEnv = checkTestBin (f2exampleWithStdEnv stdenv {
hardeningDisable = [ "all" ];
postConfigure = ''
export NIX_HARDENING_ENABLE="stackprotector"
'';
}) {
ignoreStackProtector = false;
};
stackProtectorRedisabledEnv = checkTestBin (f2exampleWithStdEnv stdenv {
hardeningEnable = [ "stackprotector" ];
postConfigure = ''
export NIX_HARDENING_ENABLE=""
'';
}) {
ignoreStackProtector = false;
expectFailure = true;
};
# musl implementation undetectable by this means even if present
fortify3EnabledEnvEnablesFortify1 = brokenIf stdenv.hostPlatform.isMusl (checkTestBin (f1exampleWithStdEnv stdenv {
hardeningDisable = [ "fortify" "fortify3" ];
postConfigure = ''
export NIX_HARDENING_ENABLE="fortify3"
'';
}) {
ignoreFortify = false;
});
fortify3EnabledEnvEnablesFortify1ExecTest = fortifyExecTest (f1exampleWithStdEnv stdenv {
hardeningDisable = [ "fortify" "fortify3" ];
postConfigure = ''
export NIX_HARDENING_ENABLE="fortify3"
'';
});
fortifyEnabledEnvDoesntEnableFortify3 = checkTestBin (f3exampleWithStdEnv stdenv {
hardeningDisable = [ "fortify" "fortify3" ];
postConfigure = ''
export NIX_HARDENING_ENABLE="fortify"
'';
}) {
ignoreFortify = false;
expectFailure = true;
};
# NIX_HARDENING_ENABLE can't enable an unsupported feature
stackProtectorUnsupportedEnabledEnv = checkTestBin (f2exampleWithStdEnv (stdenvUnsupport ["stackprotector"]) {
postConfigure = ''
export NIX_HARDENING_ENABLE="stackprotector"
'';
}) {
ignoreStackProtector = false;
expectFailure = true;
};
# current implementation prevents the command-line from disabling
# fortify if cc-wrapper is enabling it.
# undetectable by this means on static even if present
fortify1ExplicitEnabledCmdlineDisabled = brokenIf stdenv.hostPlatform.isStatic (checkTestBin (f1exampleWithStdEnv stdenv {
hardeningEnable = [ "fortify" ];
postConfigure = ''
export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=0'
'';
}) {
ignoreFortify = false;
expectFailure = false;
});
# current implementation doesn't force-disable fortify if
# command-line enables it even if we use hardeningDisable.
# musl implementation undetectable by this means even if present
fortify1ExplicitDisabledCmdlineEnabled = brokenIf (
stdenv.hostPlatform.isMusl || stdenv.hostPlatform.isStatic
) (checkTestBin (f1exampleWithStdEnv stdenv {
hardeningDisable = [ "fortify" ];
postConfigure = ''
export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=1'
'';
}) {
ignoreFortify = false;
});
fortify1ExplicitDisabledCmdlineEnabledExecTest = fortifyExecTest (f1exampleWithStdEnv stdenv {
hardeningDisable = [ "fortify" ];
postConfigure = ''
export TEST_EXTRA_FLAGS='-D_FORTIFY_SOURCE=1'
'';
});
fortify1ExplicitEnabledCmdlineDisabledNoWarn = f1exampleWithStdEnv stdenv {
hardeningEnable = [ "fortify" ];
postConfigure = ''
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;
};
allExplicitDisabledStackClashProtection = checkTestBin tb {
ignoreStackClashProtection = false;
expectFailure = true;
};
}))