X-Git-Url: https://ruderich.org/simon/gitweb/?a=blobdiff_plain;ds=sidebyside;f=bin%2Fblhc;h=ba27c55dd08ce6abf0038b239e81204dad01dd18;hb=88f15389b533468857f01490368376b539a598b3;hp=75888b86835469f3de4aa00898670084893f8801;hpb=e703305cc4c2d1717d6b63987d13951910b682ce;p=blhc%2Fblhc.git
diff --git a/bin/blhc b/bin/blhc
index 75888b8..ba27c55 100755
--- a/bin/blhc
+++ b/bin/blhc
@@ -2,7 +2,7 @@
# Build log hardening check, checks build logs for missing hardening flags.
-# Copyright (C) 2012 Simon Ruderich
+# Copyright (C) 2012-2022 Simon Ruderich
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -24,7 +24,7 @@ use warnings;
use Getopt::Long ();
use Text::ParseWords ();
-our $VERSION = '0.03';
+our $VERSION = '0.13';
# CONSTANTS/VARIABLES
@@ -33,13 +33,16 @@ our $VERSION = '0.03';
my $cc_regex = qr/
(? 1 } (
@source_preprocess_compile_cpp,
@source_no_preprocess_compile_cpp,
);
+my %extensions_ada = map { $_ => 1 } (
+ @source_no_preprocess_compile_ada,
+ @source_no_preprocess_no_compile_ada,
+);
+my %extensions_fortran = map { $_ => 1 } (
+ @source_no_preprocess_compile_fortran,
+ @source_preprocess_compile_fortran,
+);
my %extensions_object = map { $_ => 1 } (
@object,
);
@@ -170,21 +203,36 @@ my $file_extension_regex = qr/
# terminated with "\n".
/x;
-# Expected (hardening) flags. All flags are used as regexps.
+# Expected (hardening) flags. All flags are used as regexps (and compiled to
+# real regexps below for better execution speed).
my @def_cflags = (
'-g',
- '-O(?:2|3)',
+ '-O(?:2|3)', # keep at index 1, search for @def_cflags_debug to change it
+);
+my @def_cflags_debug = (
+ # These flags indicate a debug build which disables checks for -O2.
+ '-O0',
+ '-Og',
);
my @def_cflags_format = (
- '-Wformat',
+ '-Wformat(?:=2)?', # -Wformat=2 implies -Wformat, accept it too
'-Werror=format-security', # implies -Wformat-security
);
my @def_cflags_fortify = (
# fortify needs at least -O1, but -O2 is recommended anyway
);
my @def_cflags_stack = (
- '-fstack-protector',
- '--param=ssp-buffer-size=4',
+ '-fstack-protector', # keep first, used by cflags_stack_broken()
+ '--param[= ]ssp-buffer-size=4',
+);
+my @def_cflags_stack_strong = (
+ '-fstack-protector-strong', # keep first, used by cflags_stack_broken()
+);
+my @def_cflags_stack_bad = (
+ # Blacklist all stack protector options for simplicity.
+ '-fno-stack-protector',
+ '-fno-stack-protector-all',
+ '-fno-stack-protector-strong',
);
my @def_cflags_pie = (
'-fPIE',
@@ -195,8 +243,9 @@ my @def_cxxflags = (
# @def_cxxflags_* is the same as @def_cflags_*.
my @def_cppflags = ();
my @def_cppflags_fortify = (
- '-D_FORTIFY_SOURCE=2', # must be first, see cppflags_fortify_broken()
- # If you add another flag fix hack below (search for "Hack to fix").
+ '-D_FORTIFY_SOURCE=[23]', # must be first, see cppflags_fortify_broken()
+ # If you add another flag fix hack below (search for "Hack to fix") and
+ # $def_cppflags_fortify[0].
);
my @def_cppflags_fortify_bad = (
# These flags may overwrite -D_FORTIFY_SOURCE=2.
@@ -226,6 +275,8 @@ my @flag_refs = (
\@def_cflags_format,
\@def_cflags_fortify,
\@def_cflags_stack,
+ \@def_cflags_stack_strong,
+ \@def_cflags_stack_bad,
\@def_cflags_pie,
\@def_cxxflags,
\@def_cppflags,
@@ -238,15 +289,19 @@ my @flag_refs = (
# References to all used flags.
my @flag_refs_all = (
@flag_refs,
+ \@def_cflags_debug,
\@def_cppflags_fortify_bad,
\@def_ldflags_pic,
);
# Renaming rules for the output so the regex parts are not visible. Also
# stores string values of flag regexps above, see compile_flag_regexp().
my %flag_renames = (
- '-O(?:2|3)' => '-O2',
- '-Wl,(?:-z,)?relro' => '-Wl,-z,relro',
- '-Wl,(?:-z,)?now' => '-Wl,-z,now',
+ '-O(?:2|3)' => '-O2',
+ '-Wformat(?:=2)?' => '-Wformat',
+ '--param[= ]ssp-buffer-size=4' => '--param=ssp-buffer-size=4',
+ '-D_FORTIFY_SOURCE=[23]' => '-D_FORTIFY_SOURCE=2',
+ '-Wl,(?:-z,)?relro' => '-Wl,-z,relro',
+ '-Wl,(?:-z,)?now' => '-Wl,-z,now',
);
my %exit_code = (
@@ -287,8 +342,28 @@ my $option_color;
# FUNCTIONS
+sub split_line {
+ my ($line) = @_;
+
+ my @work = ($line);
+ foreach my $delim (';', '&&', '||') {
+ my @x;
+ foreach (@work) {
+ push @x, Text::ParseWords::parse_line(qr/\Q$delim\E/, 1, $_);
+ }
+ @work = @x;
+ }
+
+ return map {
+ # Ensure newline at the line end - necessary for
+ # correct parsing later.
+ $_ =~ s/\s+$//;
+ $_ .= "\n";
+ } @work;
+}
+
sub error_flags {
- my ($message, $missing_flags_ref, $flag_renames_ref, $line) = @_;
+ my ($message, $missing_flags_ref, $flag_renames_ref, $line, $number) = @_;
# Get string value of qr//-escaped regexps and if requested rename them.
my @missing_flags = map {
@@ -296,17 +371,23 @@ sub error_flags {
} @{$missing_flags_ref};
my $flags = join ' ', @missing_flags;
+ printf '%d:', $number if defined $number;
printf '%s (%s)%s %s',
error_color($message, 'red'), $flags, error_color(':', 'yellow'),
$line;
+
+ return;
}
sub error_non_verbose_build {
- my ($line) = @_;
+ my ($line, $number) = @_;
+ printf '%d:', $number if defined $number;
printf '%s%s %s',
error_color('NONVERBOSE BUILD', 'red'),
error_color(':', 'yellow'),
$line;
+
+ return;
}
sub error_invalid_cmake {
my ($version) = @_;
@@ -315,12 +396,16 @@ sub error_invalid_cmake {
error_color('INVALID CMAKE', 'red'),
error_color(':', 'yellow'),
$version;
+
+ return;
}
sub error_hardening_wrapper {
printf "%s%s %s\n",
error_color('HARDENING WRAPPER', 'red'),
error_color(':', 'yellow'),
'no checks possible, aborting';
+
+ return;
}
sub error_color {
my ($message, $color) = @_;
@@ -356,21 +441,61 @@ sub all_flags_used {
@{$missing_flags_ref} = @missing_flags;
return 0;
}
+# Check if any of \@bad_flags occurs after $good_flag. Doesn't check if
+# $good_flag is present.
+sub flag_overwritten {
+ my ($line, $good_flag, $bad_flags) = @_;
+
+ if (not any_flags_used($line, @{$bad_flags})) {
+ return 0;
+ }
+
+ my $bad_pos = 0;
+ foreach my $flag (@{$bad_flags}) {
+ while ($line =~ /$flag/g) {
+ if ($bad_pos < $+[0]) {
+ $bad_pos = $+[0];
+ }
+ }
+ }
+ my $good_pos = 0;
+ while ($line =~ /$good_flag/g) {
+ $good_pos = $+[0];
+ }
+ if ($good_pos > $bad_pos) {
+ return 0;
+ }
+ return 1;
+}
sub cppflags_fortify_broken {
my ($line, $missing_flags) = @_;
- # This doesn't take the position into account, but is a simple solution.
- # And if the build system tries to force -D_FORTIFY_SOURCE=0/1, something
- # is wrong anyway.
+ # $def_cppflags_fortify[0] must be -D_FORTIFY_SOURCE=2!
+ my $fortify_source = $def_cppflags_fortify[0];
- if (any_flags_used($line, @def_cppflags_fortify_bad)) {
- # $def_cppflags_fortify[0] must be -D_FORTIFY_SOURCE=2!
- push @{$missing_flags}, $def_cppflags_fortify[0];
- return 1;
+ # Some build systems enable/disable fortify source multiple times, check
+ # the final result.
+ if (not flag_overwritten($line,
+ $fortify_source,
+ \@def_cppflags_fortify_bad)) {
+ return 0;
}
+ push @{$missing_flags}, $fortify_source;
+ return 1;
+}
- return 0;
+sub cflags_stack_broken {
+ my ($line, $missing_flags, $strong) = @_;
+
+ my $flag = $strong ? $def_cflags_stack_strong[0]
+ : $def_cflags_stack[0];
+
+ if (not flag_overwritten($line, $flag, \@def_cflags_stack_bad)) {
+ return 0;
+ }
+ push @{$missing_flags}, $flag;
+ return 1;
}
# Modifies $missing_flags_ref array.
@@ -396,11 +521,25 @@ sub pic_pie_conflict {
}
sub is_non_verbose_build {
- my ($line, $next_line, $skip_ref) = @_;
+ my ($line, $skip_ref, $input_ref, $line_offset, $line_count) = @_;
+
+ if ($line =~ /$libtool_regex/o) {
+ # libtool's --silent hides the real compiler flags.
+ if ($line =~ /\s--silent/) {
+ return 1;
+ # If --silent is not present, skip this line as some compiler flags
+ # might be missing (e.g. -fPIE) which are handled correctly by libtool
+ # internally. libtool displays the real compiler command on the next
+ # line, so the flags are checked as usual.
+ } else {
+ ${$skip_ref} = 1;
+ return 0;
+ }
+ }
if (not (index($line, 'checking if you want to see long compiling messages... no') == 0
or $line =~ /^\s*\[?(?:CC|CCLD|C\+\+|CXX|CXXLD|LD|LINK)\]?\s+(.+?)$/
- or $line =~ /^\s*[Cc]ompiling\s+(.+?)(?:\.\.\.)?$/
+ or $line =~ /^\s*[][\/0-9 ]*[Cc]ompiling\s+(.+?)(?:\.\.\.)?$/
or $line =~ /^\s*[Bb]uilding (?:program|shared library)\s+(.+?)$/
or $line =~ /^\s*\[[\d ]+%\] Building (?:C|CXX) object (.+?)$/)) {
return 0;
@@ -410,6 +549,10 @@ sub is_non_verbose_build {
#
# C++ compiler setting.
return 0 if $line =~ /^\s*C\+\+.+?:\s+(?:yes|no)\s*$/;
+ return 0 if $line =~ /^\s*C\+\+ Library: stdc\+\+$/;
+ # "Compiling" non binary files.
+ return 0 if $line =~ /^\s*Compiling \S+\.(?:py|pyx|el)['"]?\s*(?:\.\.\.|because it changed\.)?$/;
+ return 0 if $line =~ /^\s*[Cc]ompiling catalog \S+\.po\b/;
# "Compiling" with no file name.
if ($line =~ /^\s*[Cc]ompiling\s+(.+?)(?:\.\.\.)?$/) {
# $file_extension_regex may need spaces around the filename.
@@ -419,31 +562,37 @@ sub is_non_verbose_build {
my $file = $1;
# On the first pass we only check if this line is verbose or not.
- return 1 if not defined $next_line;
+ return 1 if not defined $input_ref;
- # Second pass, we have access to the next line.
+ # Second pass, we have access to the next lines.
${$skip_ref} = 0;
# CMake and other build systems print the non-verbose messages also when
# building verbose. If a compiler and the file name occurs in the next
- # line, treat it as verbose build.
+ # lines, treat it as verbose build.
if (defined $file) {
# Get filename, we can't use the complete path as only parts of it are
# used in the real compiler command.
$file =~ m{/([^/\s]+)$};
$file = $1;
- if (index($next_line, $file) != -1 and $next_line =~ /$cc_regex/o) {
- # Not a non-verbose line, but we still have to skip the current line
- # as it doesn't contain any compiler commands.
- ${$skip_ref} = 1;
- return 0;
+ for (my $i = 1; $i <= $line_count; $i++) {
+ my $next_line = $input_ref->[$line_offset + $i];
+ last unless defined $next_line;
+
+ if (index($next_line, $file) != -1 and $next_line =~ /$cc_regex/o) {
+ # Not a non-verbose line, but we still have to skip the
+ # current line as it doesn't contain any compiler commands.
+ ${$skip_ref} = 1;
+ return 0;
+ }
}
}
return 1;
}
+# Remove @flags from $flag_refs_ref, uses $flag_renames_ref as reference.
sub remove_flags {
my ($flag_refs_ref, $flag_renames_ref, @flags) = @_;
@@ -457,37 +606,41 @@ sub remove_flags {
or not exists $removes{$flag_renames_ref->{$_}})
} @{$flags};
}
+
+ return;
}
+# Modifies $flag_renames_ref hash.
sub compile_flag_regexp {
my ($flag_renames_ref, @flags) = @_;
my @result = ();
foreach my $flag (@flags) {
+ # Compile flag regexp for faster execution.
+ my $regex = qr/\s(['"]?)$flag\1(?:\s|\\)/;
+
# Store flag name in replacement string for correct flags in messages
# with qr//ed flag regexps.
- $flag_renames_ref->{qr/\s$flag(?:\s|\\)/}
+ $flag_renames_ref->{$regex}
= (exists $flag_renames_ref->{$flag})
? $flag_renames_ref->{$flag}
: $flag;
- # Compile flag regexp for faster execution.
- push @result, qr/\s$flag(?:\s|\\)/;
+ push @result, $regex;
}
return @result;
}
+# Does any extension in @extensions exist in %{$extensions_ref}?
sub extension_found {
my ($extensions_ref, @extensions) = @_;
- my $found = 0;
foreach my $extension (@extensions) {
if (exists $extensions_ref->{$extension}) {
- $found = 1;
- last;
+ return 1;
}
}
- return $found;
+ return 0;
}
@@ -506,7 +659,9 @@ my @option_ignore_arch_line = ();
my $option_all = 0;
my $option_arch = undef;
my $option_buildd = 0;
+my $option_debian = 0;
$option_color = 0;
+my $option_line_numbers = 0;
if (not Getopt::Long::GetOptions(
'help|h|?' => \$option_help,
'version' => \$option_version,
@@ -524,6 +679,8 @@ if (not Getopt::Long::GetOptions(
'color' => \$option_color,
'arch=s' => \$option_arch,
'buildd' => \$option_buildd,
+ 'debian' => \$option_debian,
+ 'line-numbers' => \$option_line_numbers,
)) {
require Pod::Usage;
Pod::Usage::pod2usage(2);
@@ -533,7 +690,8 @@ if ($option_help) {
Pod::Usage::pod2usage(1);
}
if ($option_version) {
- print "blhc $VERSION Copyright (C) 2012 Simon Ruderich
+ print <<"EOF";
+blhc $VERSION Copyright (C) 2012-2022 Simon Ruderich
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -547,7 +705,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
-";
+EOF
exit 0;
}
@@ -599,6 +757,7 @@ foreach my $flags (@flag_refs_all) {
}
# Precompile ignore line regexps, also anchor at beginning and end of line.
+# Additional entries are also extracted from the build log, see below.
foreach my $ignore (@option_ignore_line) {
$ignore = qr/^$ignore$/;
}
@@ -636,17 +795,38 @@ foreach my $file (@ARGV) {
my $harden_format = 1;
my $harden_fortify = 1;
my $harden_stack = 1;
+ my $harden_stack_strong = 1;
my $harden_relro = 1;
my $harden_bindnow = $option_bindnow; # defaults to 0
my $harden_pie = $option_pie; # defaults to 0
+ # Number of parallel jobs to prevent false positives when detecting
+ # non-verbose builds. As not all jobs declare the number of parallel jobs
+ # use a large enough default.
+ my $parallel = 10;
+
+ # Don't check for PIE flags if automatically applied by the compiler. Only
+ # used in buildd and Debian mode.
+ my $disable_harden_pie = 0;
+ if ($option_debian) {
+ $disable_harden_pie = 1;
+ }
+
+ my $number = 0;
while (my $line = <$fh>) {
+ $number++;
+
# Detect architecture automatically unless overridden. For buildd logs
# only, doesn't use the dpkg-buildpackage header. Necessary to ignore
# build logs which aren't built (wrong architecture, build error,
# etc.).
- if (not $arch and index($line, 'Architecture: ') == 0) {
- $arch = substr $line, 14, -1; # -1 to ignore '\n' at the end
+ if (not $arch) {
+ if (index($line, 'Build Architecture: ') == 0) {
+ $arch = substr $line, 20, -1; # -1 to ignore '\n' at the end
+ # For old logs (sbuild << 0.63.0-1).
+ } elsif (index($line, 'Architecture: ') == 0) {
+ $arch = substr $line, 14, -1; # -1 to ignore '\n' at the end
+ }
}
# dpkg-buildflags only provides hardening flags since 1.16.1, don't
@@ -655,11 +835,32 @@ foreach my $file (@ARGV) {
#
# Packages which were built before 1.16.1 but used their own hardening
# flags are not checked.
+ #
+ # Strong stack protector is used since dpkg 1.17.11.
+ #
+ # Recent GCC versions automatically use PIE (only on supported
+ # architectures) and dpkg respects this properly since 1.18.15 and
+ # doesn't pass PIE flags manually.
if ($option_buildd
and index($line, 'Toolchain package versions: ') == 0) {
require Dpkg::Version;
- if (not $line =~ /\bdpkg-dev_(\S+)/
- or Dpkg::Version::version_compare($1, '1.16.1') < 0) {
+
+ my $disable = 1;
+ my $disable_strong = 1;
+
+ if ($line =~ /\bdpkg-dev_(\S+)/) {
+ if (Dpkg::Version::version_compare($1, '1.16.1') >= 0) {
+ $disable = 0;
+ }
+ if (Dpkg::Version::version_compare($1, '1.17.11') >= 0) {
+ $disable_strong = 0;
+ }
+ if (Dpkg::Version::version_compare($1, '1.18.15') >= 0) {
+ $disable_harden_pie = 1;
+ }
+ }
+
+ if ($disable) {
$harden_format = 0;
$harden_fortify = 0;
$harden_stack = 0;
@@ -667,6 +868,9 @@ foreach my $file (@ARGV) {
$harden_bindnow = 0;
$harden_pie = 0;
}
+ if ($disable_strong) {
+ $harden_stack_strong = 0;
+ }
}
# The following two versions of CMake in Debian obeyed CPPFLAGS, but
@@ -686,7 +890,12 @@ foreach my $file (@ARGV) {
}
}
- if (index($line, 'Build-Depends: ') == 0) {
+ # Debian's build daemons use "Filtered Build-Depends:" (or just
+ # "Build-Depends:" in older versions) for the build dependencies, but
+ # pbuilder uses "Depends:"; support both.
+ if (index($line, 'Filtered Build-Depends: ') == 0
+ or index($line, 'Build-Depends: ') == 0
+ or index($line, 'Depends: ') == 0) {
# If hardening wrapper is used (wraps calls to gcc and adds
# hardening flags automatically) we can't perform any checks,
# abort.
@@ -701,6 +910,11 @@ foreach my $file (@ARGV) {
}
}
+ # This flags is not always available, but if it is use it.
+ if ($line =~ /^DEB_BUILD_OPTIONS=.*\bparallel=(\d+)/) {
+ $parallel = $1 * 2;
+ }
+
# We skip over unimportant lines at the beginning of the log to
# prevent false positives.
last if index($line, 'dpkg-buildpackage: ') == 0;
@@ -712,22 +926,52 @@ foreach my $file (@ARGV) {
# is_non_verbose_build() (which is quite slow) in the second loop when
# it's already clear if a line is non-verbose or not.
my @input_nonverbose = ();
+ # Input line number.
+ my @input_number = ();
my $continuation = 0;
my $complete_line = undef;
+ my $non_verbose;
while (my $line = <$fh>) {
+ $number++;
+
# And stop at the end of the build log. Package details (reported by
# the buildd logs) are not important for us. This also prevents false
# positives.
last if index($line, 'Build finished at ') == 0
and $line =~ /^Build finished at \d{8}-\d{4}$/;
+ if (not $continuation) {
+ $non_verbose = 0;
+ }
+
# Detect architecture automatically unless overridden.
if (not $arch
+ and index($line, 'dpkg-buildpackage: info: host architecture ') == 0) {
+ $arch = substr $line, 43, -1; # -1 to ignore '\n' at the end
+ # Older versions of dpkg-buildpackage
+ } elsif (not $arch
and index($line, 'dpkg-buildpackage: host architecture ') == 0) {
$arch = substr $line, 37, -1; # -1 to ignore '\n' at the end
+
+ # Old buildd logs use e.g. "host architecture is alpha", remove
+ # the "is", otherwise debarch_to_debtriplet() will not detect the
+ # architecture.
+ if (index($arch, 'is ') == 0) {
+ $arch = substr $arch, 3;
+ }
}
+ # Permit dynamic excludes from within the build log to ignore false
+ # positives. Cannot use a separate config file as we often only have
+ # the build log itself.
+ if (index($line, 'blhc: ignore-line-regexp: ') == 0) {
+ my $ignore = substr $line, 26, -1; # -1 to ignore '\n' at the end
+ push @option_ignore_line, qr/^$ignore$/;
+ next;
+ }
+
+ next if $line =~ /^\s*#/;
# Ignore compiler warnings for now.
next if $line =~ /$warning_regex/o;
@@ -744,18 +988,16 @@ foreach my $file (@ARGV) {
}
# Check if this line indicates a non verbose build.
- my $non_verbose = is_non_verbose_build($line);
+ my $skip = 0;
+ $non_verbose |= is_non_verbose_build($line, \$skip);
+ next if $skip;
- # One line may contain multiple commands (";"). Treat each one as
- # single line. parse_line() is slow, only use it when necessary.
- my @line = (index($line, ';') == -1)
+ # Treat each command as a single line so we don't ignore valid
+ # commands when handling false positives. split_line() is slow, only
+ # use it when necessary.
+ my @line = ($line !~ /(?:;|&&|\|\|)/)
? ($line)
- : map {
- # Ensure newline at the line end - necessary for
- # correct parsing later.
- $_ =~ s/\s+$//;
- $_ .= "\n";
- } Text::ParseWords::parse_line(';', 1, $line);
+ : split_line($line);
foreach my $line (@line) {
if ($continuation) {
$continuation = 0;
@@ -781,9 +1023,29 @@ foreach my $file (@ARGV) {
$complete_line = undef;
}
+ my $noenv = $line;
+ # Strip (basic) environment variables for compiler detection. This
+ # prevents false positives when environment variables contain
+ # compiler binaries. Nested quotes, command substitution, etc. is
+ # not supported.
+ $noenv =~ s/^
+ \s*
+ (?:
+ [a-zA-Z_]+ # environment variable name
+ =
+ (?:
+ [^\s"'\$`\\]+ # non-quoted string
+ |
+ '[^"'\$`\\]*' # single-quoted string
+ |
+ "[^"'\$`\\]*" # double-quoted string
+ )
+ \s+
+ )*
+ //x;
# Ignore lines with no compiler commands.
next if not $non_verbose
- and not $line =~ /$cc_regex_normal/o;
+ and not $noenv =~ /$cc_regex_normal/o;
# Ignore lines with no filenames with extensions. May miss some
# non-verbose builds (e.g. "gcc -o test" [sic!]), but shouldn't be
# a problem as the log will most likely contain other non-verbose
@@ -799,13 +1061,25 @@ foreach my $file (@ARGV) {
next if $line =~ /^\s*(?:Host\s+)?(?:C(?:\+\+)?\s+)?
[Cc]ompiler[\s.]*:?\s+
/x;
- next if $line =~ /^\s*(?:- )?(?:HOST_)?(?:CC|CXX)\s*=\s*$cc_regex_full\s*$/o;
- # `moc-qt4`, contains '-I/usr/share/qt4/mkspecs/linux-g++' (or
- # similar for other architectures) which gets recognized as a
- # compiler line. Ignore it.
- next if $line =~ m{^/usr/bin/moc-qt4
+ next if $line =~ m{^\s*(?:-\s)?(?:HOST_)?(?:CC|CXX)
+ \s*=\s*$cc_regex_full
+ # optional compiler options, don't allow
+ # "everything" here to prevent false negatives
+ \s*(?:\s-\S+)*\s*$}xo;
+ # `echo` is never a compiler command
+ next if $line =~ /^\s*echo\s/;
+ # Ignore calls to `make` because they can contain environment
+ # variables which look like compiler commands, e.g. CC=).
+ next if $line =~ /^\s*make\s/;
+ # `moc-qt4`/`moc-qt5` contain '-I.../linux-g++' in their command
+ # line (or similar for other architectures) which gets recognized
+ # as a compiler line, but `moc-qt*` is only a preprocessor for Qt
+ # C++ files. No hardening flags are relevant during this step,
+ # thus ignore `moc-qt*` lines. The resulting files will be
+ # compiled in a separate step (and therefore checked).
+ next if $line =~ m{^\S+(?:/bin/moc(?:-qt[45])?|/lib/qt6/libexec/moc)
\s.+\s
- -I/usr/share/qt4/mkspecs/[a-z]+-g\++(?:-64)?
+ -I\S+/mkspecs/[a-z]+-g\++(?:-64)?
\s}x;
# Ignore false positives when the line contains only CC=gcc but no
# other gcc command.
@@ -815,6 +1089,27 @@ foreach my $file (@ARGV) {
next if not $before =~ /$cc_regex_normal/o
and not $after =~ /$cc_regex_normal/o;
}
+ # Ignore false positives caused by gcc -v. It outputs a line
+ # looking like a normal compiler line but which is sometimes
+ # missing hardening flags, although the normal compiler line
+ # contains them.
+ next if $line =~ m{^\s+/usr/lib/gcc/$cc_regex_full_prefix/
+ [0-9.]+/cc1(?:plus)?}xo;
+ # Ignore false positive with `rm` which may remove files which
+ # look like a compiler executable thus causing the line to be
+ # treated as a normal compiler line.
+ next if $line =~ m{^\s*rm\s+};
+ next if $line =~ m{^\s*dwz\s+};
+ # Some build systems emit "gcc > file".
+ next if $line =~ m{$cc_regex_normal\s*>\s*\S+}o;
+ # Hex output may contain "cc".
+ next if $line =~ m#(?:\b[0-9a-fA-F]{2,}\b\s*){5}#;
+ # Meson build output
+ next if $line =~ /^C\+\+ linker for the host machine: /;
+ # Embedded `gcc -print-*` commands
+ next if $line =~ /`$cc_regex_normal\s*[^`]*-print-\S+`/;
+ # cmake checking for compiler flags without setting CPPFLAGS
+ next if $line =~ m{^\s*/usr/(bin|lib)/(ccache/)?c\+\+ -dM -E -c /usr/share/cmake-\S+/Modules/CMakeCXXCompilerABI\.cpp};
# Check if additional hardening options were used. Used to ensure
# they are used for the complete build.
@@ -824,6 +1119,7 @@ foreach my $file (@ARGV) {
push @input, $line;
push @input_nonverbose, $non_verbose;
+ push @input_number, $number if $option_line_numbers;
}
}
@@ -855,23 +1151,60 @@ foreach my $file (@ARGV) {
# Option or auto detected.
if ($arch) {
- # The following was partially copied from dpkg-dev 1.16.4.3
- # (/usr/share/perl5/Dpkg/Vendor/Debian.pm, add_hardening_flags()),
- # copyright Raphaël Hertzog , Kees Cook
- # , Canonical, Ltd. licensed under GPL version 2 or
- # later. Keep it in sync.
+ # The following was partially copied from dpkg-dev 1.21.13
+ # (/usr/share/perl5/Dpkg/Vendor/Debian.pm, _add_build_flags()),
+ # copyright Raphaël Hertzog , Guillem Jover
+ # , Kees Cook , Canonical, Ltd.
+ # licensed under GPL version 2 or later. Keep it in sync.
require Dpkg::Arch;
- my ($abi, $os, $cpu) = Dpkg::Arch::debarch_to_debtriplet($arch);
+ my ($os, $cpu);
+ # Recent dpkg versions use a quadruplet for arch. Support both.
+ eval {
+ (undef, undef, $os, $cpu) = Dpkg::Arch::debarch_to_debtuple($arch);
+ };
+ if ($@) {
+ (undef, $os, $cpu) = Dpkg::Arch::debarch_to_debtriplet($arch);
+ }
+
+ my %builtin_pie_arch = map { $_ => 1 } qw(
+ amd64
+ arm64
+ armel
+ armhf
+ hurd-i386
+ i386
+ kfreebsd-amd64
+ kfreebsd-i386
+ mips
+ mipsel
+ mips64el
+ powerpc
+ ppc64
+ ppc64el
+ riscv64
+ s390x
+ sparc
+ sparc64
+ );
# Disable unsupported hardening options.
- if ($cpu =~ /^(?:ia64|alpha|mips|mipsel|hppa)$/ or $arch eq 'arm') {
+ if ($os !~ /^(?:linux|kfreebsd|knetbsd|hurd)$/
+ or $cpu =~ /^(?:hppa|avr32)$/) {
+ $harden_pie = 0;
+ }
+ if ($cpu =~ /^(?:ia64|alpha|hppa|nios2)$/ or $arch eq 'arm') {
$harden_stack = 0;
+ $harden_stack_strong = 0;
}
if ($cpu =~ /^(?:ia64|hppa|avr32)$/) {
$harden_relro = 0;
$harden_bindnow = 0;
}
+
+ if ($disable_harden_pie and exists $builtin_pie_arch{$arch}) {
+ $harden_pie = 0;
+ }
}
# Default values.
@@ -885,7 +1218,10 @@ foreach my $file (@ARGV) {
@cxxflags = (@cxxflags, @def_cflags_pie);
@ldflags = (@ldflags, @def_ldflags_pie);
}
- if ($harden_stack) {
+ if ($harden_stack_strong) {
+ @cflags = (@cflags, @def_cflags_stack_strong);
+ @cxxflags = (@cxxflags, @def_cflags_stack_strong);
+ } elsif ($harden_stack) {
@cflags = (@cflags, @def_cflags_stack);
@cxxflags = (@cxxflags, @def_cflags_stack);
}
@@ -905,6 +1241,17 @@ foreach my $file (@ARGV) {
@ldflags = (@ldflags, @def_ldflags_bindnow);
}
+ # Ada doesn't support format hardening flags, see #680117 for more
+ # information. Same for fortran.
+ my @cflags_backup;
+ my @cflags_noformat = grep {
+ my $ok = 1;
+ foreach my $flag (@def_cflags_format) {
+ $ok = 0 if $_ eq $flag;
+ }
+ $ok;
+ } @cflags;
+
# Hack to fix cppflags_fortify_broken() if --ignore-flag
# -D_FORTIFY_SOURCE=2 is used to ignore missing fortification. Only works
# as long as @def_cppflags_fortify contains only one variable.
@@ -938,18 +1285,22 @@ LINE:
my $skip = 0;
if ($input_nonverbose[$i]
- and is_non_verbose_build($line, $input[$i + 1], \$skip)) {
+ and is_non_verbose_build($line, \$skip,
+ \@input, $i, $parallel)) {
if (not $option_buildd) {
- error_non_verbose_build($line);
+ error_non_verbose_build($line, $input_number[$i]);
$exit |= $exit_code{non_verbose_build};
} else {
$statistics{commands_nonverbose}++;
}
next;
}
- # Even if it's a verbose build, we might have to skip this line.
+ # Even if it's a verbose build, we might have to skip this line (see
+ # is_non_verbose_build()).
next if $skip;
+ my $orig_line = $line;
+
# Remove everything until and including the compiler command. Makes
# checks easier and faster.
$line =~ s/^.*?$cc_regex//o;
@@ -1000,7 +1351,11 @@ LINE:
}
# These file types require preprocessing.
if (extension_found(\%extensions_preprocess, @extensions)) {
- $preprocess = 1;
+ # Prevent false positives with "libtool: link: g++ -include test.h
+ # .." compiler lines.
+ if ($orig_line !~ /$libtool_link_regex/o) {
+ $preprocess = 1;
+ }
}
if (not $flag_preprocess) {
@@ -1029,13 +1384,25 @@ LINE:
}
}
+ my $compile_cpp = 0;
+ my $restore_cflags = 0;
# Assume CXXFLAGS are required when a C++ file is specified in the
# compiler line.
- my $compile_cpp = 0;
if ($compile
and extension_found(\%extensions_compile_cpp, @extensions)) {
$compile = 0;
$compile_cpp = 1;
+ # Ada needs special CFLAGS
+ } elsif (extension_found(\%extensions_ada, @extensions)) {
+ $restore_cflags = 1;
+ $preprocess = 0; # Ada uses no CPPFLAGS
+ @cflags_backup = @cflags;
+ @cflags = @cflags_noformat;
+ # Same for fortran
+ } elsif (extension_found(\%extensions_fortran, @extensions)) {
+ $restore_cflags = 1;
+ @cflags_backup = @cflags;
+ @cflags = @cflags_noformat;
}
if ($option_buildd) {
@@ -1045,9 +1412,20 @@ LINE:
$statistics{link}++ if $link;
}
+ # Check if there are flags indicating a debug build. If that's true,
+ # skip the check for -O2. This prevents fortification, but that's fine
+ # for a debug build.
+ if (any_flags_used($line, @def_cflags_debug)) {
+ remove_flags([\@cflags], \%flag_renames, $def_cflags[1]);
+ remove_flags([\@cppflags], \%flag_renames, $def_cppflags_fortify[0]);
+ }
+
# Check hardening flags.
my @missing;
- if ($compile and not all_flags_used($line, \@missing, @cflags)
+ if ($compile and (not all_flags_used($line, \@missing, @cflags)
+ or (($harden_stack or $harden_stack_strong)
+ and cflags_stack_broken($line, \@missing,
+ $harden_stack_strong)))
# Libraries linked with -fPIC don't have to (and can't) be
# linked with -fPIE as well. It's no error if only PIE flags
# are missing.
@@ -1055,7 +1433,8 @@ LINE:
# Assume dpkg-buildflags returns the correct flags.
and index($line, '`dpkg-buildflags --get CFLAGS`') == -1) {
if (not $option_buildd) {
- error_flags('CFLAGS missing', \@missing, \%flag_renames, $input[$i]);
+ error_flags('CFLAGS missing', \@missing, \%flag_renames,
+ $input[$i], $input_number[$i]);
$exit |= $exit_code{flags_missing};
} else {
$statistics{compile_missing}++;
@@ -1068,7 +1447,8 @@ LINE:
# Assume dpkg-buildflags returns the correct flags.
and index($line, '`dpkg-buildflags --get CXXFLAGS`') == -1) {
if (not $option_buildd) {
- error_flags('CXXFLAGS missing', \@missing, \%flag_renames, $input[$i]);
+ error_flags('CXXFLAGS missing', \@missing, \%flag_renames,
+ $input[$i], $input_number[$i]);
$exit |= $exit_code{flags_missing};
} else {
$statistics{compile_cpp_missing}++;
@@ -1082,7 +1462,8 @@ LINE:
# Assume dpkg-buildflags returns the correct flags.
and index($line, '`dpkg-buildflags --get CPPFLAGS`') == -1) {
if (not $option_buildd) {
- error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $input[$i]);
+ error_flags('CPPFLAGS missing', \@missing, \%flag_renames,
+ $input[$i], $input_number[$i]);
$exit |= $exit_code{flags_missing};
} else {
$statistics{preprocess_missing}++;
@@ -1094,12 +1475,18 @@ LINE:
# Assume dpkg-buildflags returns the correct flags.
and index($line, '`dpkg-buildflags --get LDFLAGS`') == -1) {
if (not $option_buildd) {
- error_flags('LDFLAGS missing', \@missing, \%flag_renames, $input[$i]);
+ error_flags('LDFLAGS missing', \@missing, \%flag_renames,
+ $input[$i], $input_number[$i]);
$exit |= $exit_code{flags_missing};
} else {
$statistics{link_missing}++;
}
}
+
+ # Restore normal CFLAGS.
+ if ($restore_cflags) {
+ @cflags = @cflags_backup;
+ }
}
}
@@ -1159,14 +1546,37 @@ blhc is a small tool which checks build logs for missing hardening flags. It's
licensed under the GPL 3 or later.
It's designed to check build logs generated by Debian's dpkg-buildpackage (or
-tools using dpkg-buildpackage like pbuilder or the official buildd build logs)
-to help maintainers detect missing hardening flags in their packages.
+tools using dpkg-buildpackage like pbuilder or sbuild (which is used for the
+official buildd build logs)) to help maintainers detect missing hardening
+flags in their packages.
Only gcc is detected as compiler at the moment. If other compilers support
hardening flags as well, please report them.
If there's no output, no flags are missing and the build log is fine.
+See F for details about performed checks, auto-detection and
+limitations.
+
+=head1 FALSE POSITIVES
+
+To suppress false positives you can embed the following string in the build
+log:
+
+ blhc: ignore-line-regexp: REGEXP
+
+All lines fully matching REGEXP (see B<--ignore-line> for details) will be
+ignored.
+
+Please use this feature sparingly so that missing flags are not overlooked. If
+you find false positives which affect more packages please report a bug.
+
+To generate this string simply use echo in C; make sure to use @
+to suppress the echo command itself as it could also trigger a false positive.
+If the build process takes a long time edit the C<.build> file in place and
+tweak the ignore string until B no longer
+reports any false positives.
+
=head1 OPTIONS
=over 8
@@ -1214,10 +1624,20 @@ error).
=back
+=item B<--debian>
+
+Apply Debian-specific settings. At the moment this only disables checking for
+PIE which is automatically applied by Debian's GCC and no longer requires a
+compiler command line argument.
+
=item B<--color>
Use colored (ANSI) output for warning messages.
+=item B<--line-numbers>
+
+Display line numbers.
+
=item B<--ignore-arch> I
Ignore build logs from architectures matching I. I is a string.
@@ -1301,6 +1721,13 @@ Use blhc with pbuilder.
pbuilder path/to/package.dsc | tee path/log/file
blhc path/to/file || echo flags missing
+Assume this build log was created on a Debian system and thus don't warn about
+missing PIE flags if the current architecture injects them automatically (this
+is enabled in buildd mode per default). C<--arch> is necessary if the build
+log contains no architecture information as written by dpkg-buildpackage.
+
+ blhc --debian --all --arch=amd64 path/to/log/file
+
=head1 BUILDD TAGS
The following tags are used in I<--buildd> mode. In braces the additional data
@@ -1395,7 +1822,7 @@ Ejari.aalto@cante.netE for their valuable input and suggestions.
=head1 LICENSE AND COPYRIGHT
-Copyright (C) 2012 by Simon Ruderich
+Copyright (C) 2012-2022 by Simon Ruderich
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by