# Build log hardening check, checks build logs for missing hardening flags.
-# Copyright (C) 2012-2019 Simon Ruderich
+# Copyright (C) 2012-2021 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
use Getopt::Long ();
use Text::ParseWords ();
-our $VERSION = '0.09';
+our $VERSION = '0.13';
# CONSTANTS/VARIABLES
my $warning_regex = qr/^(.+?):(\d+):\d+: warning: (.+?) \[(.+?)\]$/;
# Regex to catch libtool commands and not lines which show commands executed
# by libtool (e.g. libtool: link: ...).
-my $libtool_regex = qr/\blibtool\s.*--mode=/;
+my $libtool_regex = qr/\blibtool["']?\s.*--mode=/;
my $libtool_link_regex = qr/\blibtool: link: /;
# List of source file extensions which require preprocessing.
# FUNCTIONS
-# Only works for single-level arrays with no undef values. Thanks to perlfaq4.
-sub array_equal {
- my ($first_ref, $second_ref) = @_;
+sub split_line {
+ my ($line) = @_;
- return 0 if scalar @{$first_ref} != scalar @{$second_ref};
-
- my $length = scalar @{$first_ref};
- for (my $i = 0; $i < $length; $i++) {
- return 0 if $first_ref->[$i] ne $second_ref->[$i];
+ my @work = ($line);
+ foreach my $delim (';', '&&', '||') {
+ my @x;
+ foreach (@work) {
+ push @x, Text::ParseWords::parse_line(qr/\Q$delim\E/, 1, $_);
+ }
+ @work = @x;
}
- return 1;
+ return map {
+ # Ensure newline at the line end - necessary for
+ # correct parsing later.
+ $_ =~ s/\s+$//;
+ $_ .= "\n";
+ } @work;
}
sub error_flags {
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;
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|el)['"]?\s*(?:\.\.\.)?$/;
+ 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.
my @result = ();
foreach my $flag (@flags) {
# Compile flag regexp for faster execution.
- my $regex = qr/\s$flag(?:\s|\\)/;
+ my $regex = qr/\s(['"]?)$flag\1(?:\s|\\)/;
# Store flag name in replacement string for correct flags in messages
# with qr//ed flag regexps.
}
if ($option_version) {
print <<"EOF";
-blhc $VERSION Copyright (C) 2012-2019 Simon Ruderich
+blhc $VERSION Copyright (C) 2012-2021 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
}
# 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$/;
}
my $harden_bindnow = $option_bindnow; # defaults to 0
my $harden_pie = $option_pie; # defaults to 0
- # Does this build log use ada? Ada also uses gcc as compiler but uses
- # different CFLAGS. But only perform ada checks if an ada compiler is used
- # for performance reasons.
- my $ada = 0;
- # Fortran also requires different CFLAGS.
- my $fortran = 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.
}
next FILE;
}
-
- # Ada compiler.
- if ($line =~ /\bgnat\b/) {
- $ada = 1;
- }
- # Fortran compiler.
- if ($line =~ /\bgfortran\b/) {
- $fortran = 1;
- }
}
# This flags is not always available, but if it is use it.
# 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
}
}
+ # 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;
$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;
$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
# 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
# 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.
# Option or auto detected.
if ($arch) {
- # The following was partially copied from dpkg-dev 1.19.5
+ # The following was partially copied from dpkg-dev 1.20.5
# (/usr/share/perl5/Dpkg/Vendor/Debian.pm, _add_build_flags()),
# copyright Raphaƫl Hertzog <hertzog@debian.org>, Guillem Jover
# <guillem@debian.org>, Kees Cook <kees@debian.org>, Canonical, Ltd.
}
# Ada doesn't support format hardening flags, see #680117 for more
- # information. Same for fortran. Filter them out if either language is
- # used.
+ # information. Same for fortran.
my @cflags_backup;
- my @cflags_noformat;
- if (($ada or $fortran) and $harden_format) {
- @cflags_noformat = grep {
- my $ok = 1;
- foreach my $flag (@def_cflags_format) {
- $ok = 0 if $_ eq $flag;
- }
- $ok;
- } @cflags;
- }
+ 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
and extension_found(\%extensions_compile_cpp, @extensions)) {
$compile = 0;
$compile_cpp = 1;
- # Ada needs special CFLAGS, use them if only ada files are compiled.
- } elsif ($ada
- and extension_found(\%extensions_ada, @extensions)) {
+ # 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 ($fortran
- and extension_found(\%extensions_fortran, @extensions)) {
+ # Same for fortran
+ } elsif (extension_found(\%extensions_fortran, @extensions)) {
$restore_cflags = 1;
@cflags_backup = @cflags;
@cflags = @cflags_noformat;
See F<README> 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<debian/rules>; 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<blhc --all --debian package.build> no longer
+reports any false positives.
+
=head1 OPTIONS
=over 8
=head1 LICENSE AND COPYRIGHT
-Copyright (C) 2012-2019 by Simon Ruderich
+Copyright (C) 2012-2020 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