]> ruderich.org/simon Gitweb - blhc/blhc.git/blobdiff - bin/blhc
update copyright years
[blhc/blhc.git] / bin / blhc
index e03ac2889aa039ed04eaad66720dec1ac73fab18..d2a8771515e5e01d8a3890db542cb7cf08244f21 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-2015  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.05';
 
 
 # CONSTANTS/VARIABLES
@@ -48,6 +48,9 @@ my $cc_regex_normal = qr/
     /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=/;
 
 # List of source file extensions which require preprocessing.
 my @source_preprocess_compile_cpp = (
@@ -176,13 +179,19 @@ 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 = (
@@ -190,7 +199,10 @@ my @def_cflags_fortify = (
 );
 my @def_cflags_stack = (
     '-fstack-protector',
-    '--param=ssp-buffer-size=4',
+    '--param[= ]ssp-buffer-size=4',
+);
+my @def_cflags_stack_strong = (
+    '-fstack-protector-strong',
 );
 my @def_cflags_pie = (
     '-fPIE',
@@ -202,7 +214,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.
@@ -232,6 +245,7 @@ my @flag_refs = (
     \@def_cflags_format,
     \@def_cflags_fortify,
     \@def_cflags_stack,
+    \@def_cflags_stack_strong,
     \@def_cflags_pie,
     \@def_cxxflags,
     \@def_cppflags,
@@ -244,15 +258,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 = (
@@ -319,6 +336,8 @@ sub error_flags {
     printf '%s (%s)%s %s',
            error_color($message, 'red'), $flags, error_color(':', 'yellow'),
            $line;
+
+    return;
 }
 sub error_non_verbose_build {
     my ($line) = @_;
@@ -327,6 +346,8 @@ sub error_non_verbose_build {
            error_color('NONVERBOSE BUILD', 'red'),
            error_color(':', 'yellow'),
            $line;
+
+    return;
 }
 sub error_invalid_cmake {
     my ($version) = @_;
@@ -335,12 +356,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) = @_;
@@ -418,6 +443,20 @@ sub pic_pie_conflict {
 sub is_non_verbose_build {
     my ($line, $next_line, $skip_ref) = @_;
 
+    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+(.+?)(?:\.\.\.)?$/
@@ -430,6 +469,9 @@ 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|el)['"]?\s*(?:\.\.\.)?$/;
     # "Compiling" with no file name.
     if ($line =~ /^\s*[Cc]ompiling\s+(.+?)(?:\.\.\.)?$/) {
         # $file_extension_regex may need spaces around the filename.
@@ -464,6 +506,7 @@ sub is_non_verbose_build {
     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) = @_;
 
@@ -477,37 +520,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;
 }
 
 
@@ -553,7 +600,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-2015  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
@@ -567,7 +615,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;
 }
 
@@ -656,6 +704,7 @@ 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
@@ -670,8 +719,13 @@ foreach my $file (@ARGV) {
         # 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
@@ -680,11 +734,25 @@ 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.
         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 ($disable) {
                 $harden_format  = 0;
                 $harden_fortify = 0;
                 $harden_stack   = 0;
@@ -692,6 +760,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
@@ -711,7 +782,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.
@@ -745,6 +821,7 @@ foreach my $file (@ARGV) {
 
     my $continuation = 0;
     my $complete_line = undef;
+    my $non_verbose;
     while (my $line = <$fh>) {
         # 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
@@ -752,10 +829,21 @@ foreach my $file (@ARGV) {
         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: 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;
+            }
         }
 
         # Ignore compiler warnings for now.
@@ -774,7 +862,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, undef, \$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.
@@ -829,13 +919,20 @@ 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;
+            # `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.
@@ -885,7 +982,7 @@ foreach my $file (@ARGV) {
 
     # Option or auto detected.
     if ($arch) {
-        # The following was partially copied from dpkg-dev 1.16.4.3
+        # The following was partially copied from dpkg-dev 1.17.11
         # (/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
@@ -895,8 +992,12 @@ foreach my $file (@ARGV) {
         my ($abi, $os, $cpu) = Dpkg::Arch::debarch_to_debtriplet($arch);
 
         # Disable unsupported hardening options.
-        if ($cpu =~ /^(?:ia64|alpha|mips|mipsel|hppa)$/ or $arch eq 'arm') {
+        if ($os !~ /^(?:linux|knetbsd|hurd)$/ or $cpu =~ /^(?:hppa|avr32)$/) {
+            $harden_pie = 0;
+        }
+        if ($cpu =~ /^(?:ia64|alpha|hppa)$/ or $arch eq 'arm') {
             $harden_stack = 0;
+            $harden_stack_strong = 0;
         }
         if ($cpu =~ /^(?:ia64|hppa|avr32)$/) {
             $harden_relro   = 0;
@@ -915,7 +1016,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);
     }
@@ -937,8 +1041,8 @@ foreach my $file (@ARGV) {
 
     # Stores normal CFLAGS when @cflags_ada are temporarily used.
     my @cflags_backup;
-    # Ada CFLAGS.
-    my @cflags_ada = @cflags;
+    # Ada CFLAGS, only set if ada is used.
+    my @cflags_ada;
     # Ada doesn't support format hardening flags, see #680117 for more
     # information. Filter them out if ada is used.
     if ($ada and $harden_format) {
@@ -993,7 +1097,8 @@ LINE:
             }
             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;
 
         # Remove everything until and including the compiler command. Makes
@@ -1100,6 +1205,14 @@ 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)
@@ -1219,14 +1332,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
@@ -1455,7 +1572,7 @@ E<lt>jari.aalto@cante.netE<gt> for their valuable input and suggestions.
 
 =head1 LICENSE AND COPYRIGHT
 
-Copyright (C) 2012 by Simon Ruderich
+Copyright (C) 2012-2015 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