]> ruderich.org/simon Gitweb - blhc/blhc.git/blobdiff - bin/blhc
Fix false positive in libtool detection with quoted path
[blhc/blhc.git] / bin / blhc
index fa2c507fff10c679a94d277b3b95571bf7cebe31..d6e269091878869dbe8ab371ae954913003af2e8 100755 (executable)
--- 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-2019  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.09';
 
 
 # CONSTANTS/VARIABLES
@@ -33,17 +33,28 @@ our $VERSION = '0.03';
 my $cc_regex = qr/
     (?<!\s-)               # ignore options, e.g. "-c++" [sic!] (used by swig)
     (?<!\.)                # ignore file names, e.g. "test.gcc"
-    (?:cc|gcc|g\+\+|c\+\+)
+    (?:cc|gcc|g\+\+|c\+\+|gfortran|mpicc|mpicxx|mpifort)
     (?:-[\d.]+)?           # version suffix, e.g. "gcc-4.6"
     /x;
 # Full regex which matches the complete compiler name. Used in a few places to
 # prevent false negatives.
+my $cc_regex_full_prefix = qr/
+    [a-z0-9_]+-(?:linux-|kfreebsd-)?gnu(?:eabi|eabihf)?
+    /x;
 my $cc_regex_full = qr/
-    (?:[a-z0-9_]+-(?:linux-|kfreebsd-)?gnu(?:eabi|eabihf)?-)?
+    (?:$cc_regex_full_prefix-)?
     $cc_regex
     /x;
+# Regex to check if a line contains a compiler command.
+my $cc_regex_normal = qr/
+    \b$cc_regex(?:\s|\\)
+    /x;
 # Regex to catch (GCC) compiler warnings.
 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_link_regex = qr/\blibtool: link: /;
 
 # List of source file extensions which require preprocessing.
 my @source_preprocess_compile_cpp = (
@@ -52,6 +63,10 @@ my @source_preprocess_compile_cpp = (
     # Objective-C++
     qw( mm M ),
 );
+my @source_preprocess_compile_fortran = (
+    # Fortran
+    qw( F FOR fpp FPP FTN F90 F95 F03 F08 ),
+);
 my @source_preprocess_compile = (
     # C
     qw( c ),
@@ -60,7 +75,7 @@ my @source_preprocess_compile = (
     # (Objective-)C++
     @source_preprocess_compile_cpp,
     # Fortran
-    qw( F FOR fpp FPP FTN F90 F95 F03 F08 ),
+    @source_preprocess_compile_fortran,
 );
 my @source_preprocess_no_compile = (
     # Assembly
@@ -77,6 +92,16 @@ my @source_no_preprocess_compile_cpp = (
     # Objective-C++
     qw( mii ),
 );
+my @source_no_preprocess_compile_ada = (
+    # Ada source
+    qw( ada ),
+    # Ada body
+    qw( adb ),
+);
+my @source_no_preprocess_compile_fortran = (
+    # Fortran
+    qw( f for ftn f90 f95 f03 f08 ),
+);
 my @source_no_preprocess_compile = (
     # C
     qw( i ),
@@ -85,11 +110,19 @@ my @source_no_preprocess_compile = (
     # Objective-C
     qw( mi ),
     # Fortran
-    qw( f for ftn f90 f95 f03 f08 ),
+    @source_no_preprocess_compile_fortran,
+    # Ada
+    @source_no_preprocess_compile_ada,
+);
+my @source_no_preprocess_no_compile_ada = (
+    # Ada specification
+    qw( ads ),
 );
 my @source_no_preprocess_no_compile = (
     # Assembly
     qw( s ),
+    # Ada
+    @source_no_preprocess_no_compile_ada,
 );
 my @source_no_preprocess = (
     @source_no_preprocess_compile,
@@ -140,6 +173,14 @@ my %extensions_compile_cpp = map { $_ => 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,
 );
@@ -162,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',
@@ -188,7 +244,8 @@ my @def_cxxflags = (
 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").
+    # 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.
@@ -218,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,
@@ -230,15 +289,18 @@ 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',
+    '-Wl,(?:-z,)?relro'            => '-Wl,-z,relro',
+    '-Wl,(?:-z,)?now'              => '-Wl,-z,now',
 );
 
 my %exit_code = (
@@ -279,8 +341,22 @@ my $option_color;
 
 # FUNCTIONS
 
+# Only works for single-level arrays with no undef values. Thanks to perlfaq4.
+sub array_equal {
+    my ($first_ref, $second_ref) = @_;
+
+    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];
+    }
+
+    return 1;
+}
+
 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 {
@@ -288,17 +364,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) = @_;
@@ -307,12 +389,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) = @_;
@@ -348,21 +434,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.
@@ -388,7 +514,21 @@ 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+(.+?)$/
@@ -402,6 +542,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.
@@ -411,31 +555,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) {
-            # 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) = @_;
 
@@ -449,37 +599,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(?:\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;
 }
 
 
@@ -498,7 +652,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,
@@ -516,6 +672,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);
@@ -525,7 +683,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-2019  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
@@ -539,7 +698,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 <http://www.gnu.org/licenses/>.
-";
+EOF
     exit 0;
 }
 
@@ -571,7 +730,7 @@ if (scalar @option_ignore_flag > 0) {
 # Same for arch specific ignore flags, but only prepare here.
 if (scalar @option_ignore_arch_flag > 0) {
     foreach my $ignore (@option_ignore_arch_flag) {
-        my ($ignore_arch, $ignore_flag) = split ':', $ignore, 2;
+        my ($ignore_arch, $ignore_flag) = split /:/, $ignore, 2;
 
         if (not $ignore_arch or not $ignore_flag) {
             printf STDERR 'Value "%s" invalid for option ignore-arch-flag '
@@ -597,7 +756,7 @@ foreach my $ignore (@option_ignore_line) {
 # Same for arch specific ignore lines.
 if (scalar @option_ignore_arch_line > 0) {
     foreach my $ignore (@option_ignore_arch_line) {
-        my ($ignore_arch, $ignore_line) = split ':', $ignore, 2;
+        my ($ignore_arch, $ignore_line) = split /:/, $ignore, 2;
 
         if (not $ignore_arch or not $ignore_line) {
             printf STDERR 'Value "%s" invalid for option ignore-arch-line '
@@ -628,18 +787,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 $line =~ /^Architecture: (.+)$/) {
-            $arch = $1;
+        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
@@ -648,11 +827,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;
@@ -660,6 +860,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
@@ -679,17 +882,29 @@ foreach my $file (@ARGV) {
             }
         }
 
-        # If hardening wrapper is used (wraps calls to gcc and adds hardening
-        # flags automatically) we can't perform any checks, abort.
-        if (index($line, 'Build-Depends: ') == 0
-                and $line =~ /\bhardening-wrapper\b/) {
-            if (not $option_buildd) {
-                error_hardening_wrapper();
-                $exit |= $exit_code{hardening_wrapper};
-            } else {
-                print "$buildd_tag{hardening_wrapper}||\n";
+        # 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.
+            if ($line =~ /\bhardening-wrapper\b/) {
+                if (not $option_buildd) {
+                    error_hardening_wrapper();
+                    $exit |= $exit_code{hardening_wrapper};
+                } else {
+                    print "$buildd_tag{hardening_wrapper}||\n";
+                }
+                next FILE;
             }
-            next FILE;
+        }
+
+        # 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
@@ -699,25 +914,51 @@ foreach my $file (@ARGV) {
 
     # Input lines, contain only the lines with compiler commands.
     my @input = ();
+    # Non-verbose lines in the input. Used to reduce calls to
+    # 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 $line =~ /^Build finished at \d{8}-\d{4}$/;
+        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 $line =~ /^dpkg-buildpackage: host architecture (.+)$/) {
-            $arch = $1;
+                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;
+            }
         }
 
+        next if $line =~ /^\s*#/;
         # Ignore compiler warnings for now.
         next if $line =~ /$warning_regex/o;
 
-        if (not $option_buildd and index($line, "\033") != -1) { # esc
+        if (not $option_buildd and index($line, "\033") != -1) { # \033 = esc
             # Remove all ANSI color sequences which are sometimes used in
             # non-verbose builds.
             $line = Term::ANSIColor::colorstrip($line);
@@ -730,7 +971,9 @@ 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.
@@ -769,7 +1012,7 @@ foreach my $file (@ARGV) {
 
             # Ignore lines with no compiler commands.
             next if not $non_verbose
-                    and not $line =~ /\b$cc_regex(?:\s|\\)/o;
+                    and not $line =~ /$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
@@ -784,15 +1027,44 @@ foreach my $file (@ARGV) {
                     and $line =~ /^(?:checking|[Cc]onfigure:) /;
             next if $line =~ /^\s*(?:Host\s+)?(?:C(?:\+\+)?\s+)?
                                 [Cc]ompiler[\s.]*:?\s+
-                                /xo;
-            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
+                                /x;
+            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;
+            # `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])?
                                \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.
+            if ($line =~ /(.*)CC=$cc_regex_full(.*)/o) {
+                my $before = $1;
+                my $after  = $2;
+                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+};
+            # 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}#;
 
             # Check if additional hardening options were used. Used to ensure
             # they are used for the complete build.
@@ -801,6 +1073,8 @@ foreach my $file (@ARGV) {
             $harden_bindnow = 1 if any_flags_used($line, @def_ldflags_bindnow);
 
             push @input, $line;
+            push @input_nonverbose, $non_verbose;
+            push @input_number, $number if $option_line_numbers;
         }
     }
 
@@ -832,23 +1106,60 @@ foreach my $file (@ARGV) {
 
     # Option or auto detected.
     if ($arch) {
-        # The following was partially copied from dpkg-dev 1.16.1.2
-        # (/usr/share/perl5/Dpkg/Vendor/Debian.pm, add_hardening_flags()),
-        # copyright Raphaël Hertzog <hertzog@debian.org>, Kees Cook
-        # <kees@debian.org>, Canonical, Ltd. licensed under GPL version 2 or
-        # later. Keep it in sync.
+        # The following was partially copied from dpkg-dev 1.19.7
+        # (/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.
+        # 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.
@@ -862,7 +1173,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);
     }
@@ -882,6 +1196,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.
@@ -891,9 +1216,9 @@ foreach my $file (@ARGV) {
 
     # Ignore flags for this arch if requested.
     if ($arch and exists $option_ignore_arch_flag{$arch}) {
-        my @flag_refs = (\@cflags, \@cxxflags, \@cppflags, \@ldflags);
+        my @local_flag_refs = (\@cflags, \@cxxflags, \@cppflags, \@ldflags);
 
-        remove_flags(\@flag_refs,
+        remove_flags(\@local_flag_refs,
                      \%flag_renames,
                      @{$option_ignore_arch_flag{$arch}});
     }
@@ -914,23 +1239,28 @@ LINE:
         }
 
         my $skip = 0;
-        if (is_non_verbose_build($line, $input[$i + 1], \$skip)) {
+        if ($input_nonverbose[$i]
+                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;
         # "([...] test.c)" is not detected as 'test.c' - fix this by removing
-        # the brace and similar characters.
+        # the brace and similar characters at the line end.
         $line =~ s/['")]+$//;
 
         # Skip unnecessary tests when only preprocessing.
@@ -976,7 +1306,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) {
@@ -1005,13 +1339,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) {
@@ -1021,9 +1367,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.
@@ -1031,7 +1388,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}++;
@@ -1044,7 +1402,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}++;
@@ -1058,7 +1417,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}++;
@@ -1070,12 +1430,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;
+        }
     }
 }
 
@@ -1135,14 +1501,18 @@ 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<README> for details about performed checks, auto-detection and
+limitations.
+
 =head1 OPTIONS
 
 =over 8
@@ -1190,10 +1560,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<arch>
 
 Ignore build logs from architectures matching I<arch>. I<arch> is a string.
@@ -1277,6 +1657,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
@@ -1369,9 +1756,9 @@ Simon Ruderich, E<lt>simon@ruderich.orgE<gt>
 Thanks to to Bernhard R. Link E<lt>brlink@debian.orgE<gt> and Jaria Alto
 E<lt>jari.aalto@cante.netE<gt> for their valuable input and suggestions.
 
-=head1 COPYRIGHT AND LICENSE
+=head1 LICENSE AND COPYRIGHT
 
-Copyright (C) 2012 by Simon Ruderich
+Copyright (C) 2012-2019 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