Merge tag 'leaks-4.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/tobin...
authorLinus Torvalds <torvalds@linux-foundation.org>
Sat, 7 Apr 2018 18:56:33 +0000 (11:56 -0700)
committerLinus Torvalds <torvalds@linux-foundation.org>
Sat, 7 Apr 2018 18:56:33 +0000 (11:56 -0700)
Pull leaking-addresses updates from Tobin Harding:
 "This set represents improvements to the scripts/leaking_addresses.pl
  script.

  The major improvement is that with this set applied the script
  actually runs in a reasonable amount of time (less than a minute on a
  standard stock Ubuntu user desktop). Also, we have a second maintainer
  now and a tree hosted on kernel.org

  We do a few code clean ups. We fix the command help output. Handling
  of the vsyscall address range is fixed to check the whole range
  instead of just the start/end addresses. We add support for 5 page
  table levels (suggested on LKML). We use a system command to get the
  machine architecture instead of using Perl. Calling this command for
  every regex comparison is what previously choked the script, caching
  the result of this call gave the major speed improvement. We add
  support for scanning 32-bit kernels using the user/kernel memory
  split. Path skipping code refactored and simplified (meaning easier
  script configuration). We remove version numbering. We add a variable
  name to improve readability of a regex and finally we check filenames
  for leaking addresses.

  Currently script scans /proc/PID for all PID. With this set applied we
  only scan for PID==1. It was observed that on an idle system files
  under /proc/PID are predominantly the same for all processes. Also it
  was noted that the script does not scan _all_ the kernel since it only
  scans active processes. Scanning only for PID==1 makes explicit the
  inherent flaw in the script that the scan is only partial and also
  speeds things up"

* tag 'leaks-4.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/tobin/leaks:
  MAINTAINERS: Update LEAKING_ADDRESSES
  leaking_addresses: check if file name contains address
  leaking_addresses: explicitly name variable used in regex
  leaking_addresses: remove version number
  leaking_addresses: skip '/proc/1/syscall'
  leaking_addresses: skip all /proc/PID except /proc/1
  leaking_addresses: cache architecture name
  leaking_addresses: simplify path skipping
  leaking_addresses: do not parse binary files
  leaking_addresses: add 32-bit support
  leaking_addresses: add is_arch() wrapper subroutine
  leaking_addresses: use system command to get arch
  leaking_addresses: add support for 5 page table levels
  leaking_addresses: add support for kernel config file
  leaking_addresses: add range check for vsyscall memory
  leaking_addresses: indent dependant options
  leaking_addresses: remove command examples
  leaking_addresses: remove mention of kptr_restrict
  leaking_addresses: fix typo function not called

MAINTAINERS
scripts/leaking_addresses.pl

index 7e48624f4f9ff8fab42cbafba00584a17c2a3825..957c48526d5ecee59b77b4bcedc29e4beb85f544 100644 (file)
@@ -7919,7 +7919,10 @@ F:       drivers/scsi/53c700*
 
 LEAKING_ADDRESSES
 M:     Tobin C. Harding <me@tobin.cc>
+M:     Tycho Andersen <tycho@tycho.ws>
+L:     kernel-hardening@lists.openwall.com
 S:     Maintained
+T:     git git://git.kernel.org/pub/scm/linux/kernel/git/tobin/leaks.git
 F:     scripts/leaking_addresses.pl
 
 LED SUBSYSTEM
index bc57880000184d122337e3968b94634dd59ec001..6a897788f5a7ecd3eb699ebd6aec21ee1125aecd 100755 (executable)
@@ -3,15 +3,20 @@
 # (c) 2017 Tobin C. Harding <me@tobin.cc>
 # Licensed under the terms of the GNU GPL License version 2
 #
-# leaking_addresses.pl: Scan 64 bit kernel for potential leaking addresses.
+# leaking_addresses.pl: Scan the kernel for potential leaking addresses.
 #  - Scans dmesg output.
 #  - Walks directory tree and parses each file (for each directory in @DIRS).
 #
 # Use --debug to output path before parsing, this is useful to find files that
 # cause the script to choke.
+
 #
-# You may like to set kptr_restrict=2 before running script
-# (see Documentation/sysctl/kernel.txt).
+# When the system is idle it is likely that most files under /proc/PID will be
+# identical for various processes.  Scanning _all_ the PIDs under /proc is
+# unnecessary and implies that we are thoroughly scanning /proc.  This is _not_
+# the case because there may be ways userspace can trigger creation of /proc
+# files that leak addresses but were not present during a scan.  For these two
+# reasons we exclude all PID directories under /proc except '1/'
 
 use warnings;
 use strict;
@@ -22,9 +27,10 @@ use Cwd 'abs_path';
 use Term::ANSIColor qw(:constants);
 use Getopt::Long qw(:config no_auto_abbrev);
 use Config;
+use bigint qw/hex/;
+use feature 'state';
 
 my $P = $0;
-my $V = '0.01';
 
 # Directories to scan.
 my @DIRS = ('/proc', '/sys');
@@ -32,10 +38,9 @@ my @DIRS = ('/proc', '/sys');
 # Timer for parsing each file, in seconds.
 my $TIMEOUT = 10;
 
-# Script can only grep for kernel addresses on the following architectures. If
-# your architecture is not listed here and has a grep'able kernel address please
-# consider submitting a patch.
-my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64');
+# Kernel addresses vary by architecture.  We can only auto-detect the following
+# architectures (using `uname -m`).  (flag --32-bit overrides auto-detection.)
+my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64', 'x86');
 
 # Command line options.
 my $help = 0;
@@ -43,46 +48,34 @@ my $debug = 0;
 my $raw = 0;
 my $output_raw = "";   # Write raw results to file.
 my $input_raw = "";    # Read raw results from file instead of scanning.
-
 my $suppress_dmesg = 0;                # Don't show dmesg in output.
 my $squash_by_path = 0;                # Summary report grouped by absolute path.
 my $squash_by_filename = 0;    # Summary report grouped by filename.
-
-# Do not parse these files (absolute path).
-my @skip_parse_files_abs = ('/proc/kmsg',
-                           '/proc/kcore',
-                           '/proc/fs/ext4/sdb1/mb_groups',
-                           '/proc/1/fd/3',
-                           '/sys/firmware/devicetree',
-                           '/proc/device-tree',
-                           '/sys/kernel/debug/tracing/trace_pipe',
-                           '/sys/kernel/security/apparmor/revision');
-
-# Do not parse these files under any subdirectory.
-my @skip_parse_files_any = ('0',
-                           '1',
-                           '2',
-                           'pagemap',
-                           'events',
-                           'access',
-                           'registers',
-                           'snapshot_raw',
-                           'trace_pipe_raw',
-                           'ptmx',
-                           'trace_pipe');
-
-# Do not walk these directories (absolute path).
-my @skip_walk_dirs_abs = ();
-
-# Do not walk these directories under any subdirectory.
-my @skip_walk_dirs_any = ('self',
-                         'thread-self',
-                         'cwd',
-                         'fd',
-                         'usbmon',
-                         'stderr',
-                         'stdin',
-                         'stdout');
+my $kernel_config_file = "";   # Kernel configuration file.
+my $opt_32bit = 0;             # Scan 32-bit kernel.
+my $page_offset_32bit = 0;     # Page offset for 32-bit kernel.
+
+# Skip these absolute paths.
+my @skip_abs = (
+       '/proc/kmsg',
+       '/proc/device-tree',
+       '/proc/1/syscall',
+       '/sys/firmware/devicetree',
+       '/sys/kernel/debug/tracing/trace_pipe',
+       '/sys/kernel/security/apparmor/revision');
+
+# Skip these under any subdirectory.
+my @skip_any = (
+       'pagemap',
+       'events',
+       'access',
+       'registers',
+       'snapshot_raw',
+       'trace_pipe_raw',
+       'ptmx',
+       'trace_pipe',
+       'fd',
+       'usbmon');
 
 sub help
 {
@@ -91,31 +84,22 @@ sub help
        print << "EOM";
 
 Usage: $P [OPTIONS]
-Version: $V
 
 Options:
 
-       -o, --output-raw=<file>  Save results for future processing.
-       -i, --input-raw=<file>   Read results from file instead of scanning.
-           --raw                Show raw results (default).
-           --suppress-dmesg     Do not show dmesg results.
-           --squash-by-path     Show one result per unique path.
-           --squash-by-filename Show one result per unique filename.
-       -d, --debug              Display debugging output.
-       -h, --help, --version    Display this help and exit.
-
-Examples:
-
-       # Scan kernel and dump raw results.
-       $0
-
-       # Scan kernel and save results to file.
-       $0 --output-raw scan.out
+       -o, --output-raw=<file>         Save results for future processing.
+       -i, --input-raw=<file>          Read results from file instead of scanning.
+             --raw                     Show raw results (default).
+             --suppress-dmesg          Do not show dmesg results.
+             --squash-by-path          Show one result per unique path.
+             --squash-by-filename      Show one result per unique filename.
+       --kernel-config-file=<file>     Kernel configuration file (e.g /boot/config)
+       --32-bit                        Scan 32-bit kernel.
+       --page-offset-32-bit=o          Page offset (for 32-bit kernel 0xABCD1234).
+       -d, --debug                     Display debugging output.
+       -h, --help, --version           Display this help and exit.
 
-       # View summary report.
-       $0 --input-raw scan.out --squash-by-filename
-
-Scans the running (64 bit) kernel for potential leaking addresses.
+Scans the running kernel for potential leaking addresses.
 
 EOM
        exit($exitcode);
@@ -131,6 +115,9 @@ GetOptions(
        'squash-by-path'        => \$squash_by_path,
        'squash-by-filename'    => \$squash_by_filename,
        'raw'                   => \$raw,
+       'kernel-config-file=s'  => \$kernel_config_file,
+       '32-bit'                => \$opt_32bit,
+       'page-offset-32-bit=o'  => \$page_offset_32bit,
 ) or help(1);
 
 help(0) if ($help);
@@ -146,16 +133,19 @@ if (!$input_raw and ($squash_by_path or $squash_by_filename)) {
        exit(128);
 }
 
-if (!is_supported_architecture()) {
+if (!(is_supported_architecture() or $opt_32bit or $page_offset_32bit)) {
        printf "\nScript does not support your architecture, sorry.\n";
        printf "\nCurrently we support: \n\n";
        foreach(@SUPPORTED_ARCHITECTURES) {
                printf "\t%s\n", $_;
        }
+       printf("\n");
+
+       printf("If you are running a 32-bit architecture you may use:\n");
+       printf("\n\t--32-bit or --page-offset-32-bit=<page offset>\n\n");
 
-       my $archname = $Config{archname};
-       printf "\n\$ perl -MConfig -e \'print \"\$Config{archname}\\n\"\'\n";
-       printf "%s\n", $archname;
+       my $archname = `uname -m`;
+       printf("Machine hardware name (`uname -m`): %s\n", $archname);
 
        exit(129);
 }
@@ -177,49 +167,183 @@ sub dprint
 
 sub is_supported_architecture
 {
-       return (is_x86_64() or is_ppc64());
+       return (is_x86_64() or is_ppc64() or is_ix86_32());
 }
 
-sub is_x86_64
+sub is_32bit
 {
-       my $archname = $Config{archname};
-
-       if ($archname =~ m/x86_64/) {
+       # Allow --32-bit or --page-offset-32-bit to override
+       if ($opt_32bit or $page_offset_32bit) {
                return 1;
        }
-       return 0;
+
+       return is_ix86_32();
+}
+
+sub is_ix86_32
+{
+       state $arch = `uname -m`;
+
+       chomp $arch;
+       if ($arch =~ m/i[3456]86/) {
+               return 1;
+       }
+       return 0;
+}
+
+sub is_arch
+{
+       my ($desc) = @_;
+       my $arch = `uname -m`;
+
+       chomp $arch;
+       if ($arch eq $desc) {
+               return 1;
+       }
+       return 0;
+}
+
+sub is_x86_64
+{
+       state $is = is_arch('x86_64');
+       return $is;
 }
 
 sub is_ppc64
 {
-       my $archname = $Config{archname};
+       state $is = is_arch('ppc64');
+       return $is;
+}
 
-       if ($archname =~ m/powerpc/ and $archname =~ m/64/) {
-               return 1;
+# Gets config option value from kernel config file.
+# Returns "" on error or if config option not found.
+sub get_kernel_config_option
+{
+       my ($option) = @_;
+       my $value = "";
+       my $tmp_file = "";
+       my @config_files;
+
+       # Allow --kernel-config-file to override.
+       if ($kernel_config_file ne "") {
+               @config_files = ($kernel_config_file);
+       } elsif (-R "/proc/config.gz") {
+               my $tmp_file = "/tmp/tmpkconf";
+
+               if (system("gunzip < /proc/config.gz > $tmp_file")) {
+                       dprint "$0: system(gunzip < /proc/config.gz) failed\n";
+                       return "";
+               } else {
+                       @config_files = ($tmp_file);
+               }
+       } else {
+               my $file = '/boot/config-' . `uname -r`;
+               chomp $file;
+               @config_files = ($file, '/boot/config');
        }
-       return 0;
+
+       foreach my $file (@config_files) {
+               dprint("parsing config file: %s\n", $file);
+               $value = option_from_file($option, $file);
+               if ($value ne "") {
+                       last;
+               }
+       }
+
+       if ($tmp_file ne "") {
+               system("rm -f $tmp_file");
+       }
+
+       return $value;
+}
+
+# Parses $file and returns kernel configuration option value.
+sub option_from_file
+{
+       my ($option, $file) = @_;
+       my $str = "";
+       my $val = "";
+
+       open(my $fh, "<", $file) or return "";
+       while (my $line = <$fh> ) {
+               if ($line =~ /^$option/) {
+                       ($str, $val) = split /=/, $line;
+                       chomp $val;
+                       last;
+               }
+       }
+
+       close $fh;
+       return $val;
 }
 
 sub is_false_positive
 {
        my ($match) = @_;
 
+       if (is_32bit()) {
+               return is_false_positive_32bit($match);
+       }
+
+       # 64 bit false positives.
+
        if ($match =~ '\b(0x)?(f|F){16}\b' or
            $match =~ '\b(0x)?0{16}\b') {
                return 1;
        }
 
-       if (is_x86_64) {
-               # vsyscall memory region, we should probably check against a range here.
-               if ($match =~ '\bf{10}600000\b' or
-                   $match =~ '\bf{10}601000\b') {
-                       return 1;
-               }
+       if (is_x86_64() and is_in_vsyscall_memory_region($match)) {
+               return 1;
        }
 
        return 0;
 }
 
+sub is_false_positive_32bit
+{
+       my ($match) = @_;
+       state $page_offset = get_page_offset();
+
+       if ($match =~ '\b(0x)?(f|F){8}\b') {
+               return 1;
+       }
+
+       if (hex($match) < $page_offset) {
+               return 1;
+       }
+
+       return 0;
+}
+
+# returns integer value
+sub get_page_offset
+{
+       my $page_offset;
+       my $default_offset = 0xc0000000;
+
+       # Allow --page-offset-32bit to override.
+       if ($page_offset_32bit != 0) {
+               return $page_offset_32bit;
+       }
+
+       $page_offset = get_kernel_config_option('CONFIG_PAGE_OFFSET');
+       if (!$page_offset) {
+              return $default_offset;
+       }
+       return $page_offset;
+}
+
+sub is_in_vsyscall_memory_region
+{
+       my ($match) = @_;
+
+       my $hex = hex($match);
+       my $region_min = hex("0xffffffffff600000");
+       my $region_max = hex("0xffffffffff601000");
+
+       return ($hex >= $region_min and $hex <= $region_max);
+}
+
 # True if argument potentially contains a kernel address.
 sub may_leak_address
 {
@@ -238,14 +362,8 @@ sub may_leak_address
                return 0;
        }
 
-       # One of these is guaranteed to be true.
-       if (is_x86_64()) {
-               $address_re = '\b(0x)?ffff[[:xdigit:]]{12}\b';
-       } elsif (is_ppc64()) {
-               $address_re = '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b';
-       }
-
-       while (/($address_re)/g) {
+       $address_re = get_address_re();
+       while ($line =~ /($address_re)/g) {
                if (!is_false_positive($1)) {
                        return 1;
                }
@@ -254,6 +372,31 @@ sub may_leak_address
        return 0;
 }
 
+sub get_address_re
+{
+       if (is_ppc64()) {
+               return '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b';
+       } elsif (is_32bit()) {
+               return '\b(0x)?[[:xdigit:]]{8}\b';
+       }
+
+       return get_x86_64_re();
+}
+
+sub get_x86_64_re
+{
+       # We handle page table levels but only if explicitly configured using
+       # CONFIG_PGTABLE_LEVELS.  If config file parsing fails or config option
+       # is not found we default to using address regular expression suitable
+       # for 4 page table levels.
+       state $ptl = get_kernel_config_option('CONFIG_PGTABLE_LEVELS');
+
+       if ($ptl == 5) {
+               return '\b(0x)?ff[[:xdigit:]]{14}\b';
+       }
+       return '\b(0x)?ffff[[:xdigit:]]{12}\b';
+}
+
 sub parse_dmesg
 {
        open my $cmd, '-|', 'dmesg';
@@ -268,26 +411,20 @@ sub parse_dmesg
 # True if we should skip this path.
 sub skip
 {
-       my ($path, $paths_abs, $paths_any) = @_;
+       my ($path) = @_;
 
-       foreach (@$paths_abs) {
+       foreach (@skip_abs) {
                return 1 if (/^$path$/);
        }
 
        my($filename, $dirs, $suffix) = fileparse($path);
-       foreach (@$paths_any) {
+       foreach (@skip_any) {
                return 1 if (/^$filename$/);
        }
 
        return 0;
 }
 
-sub skip_parse
-{
-       my ($path) = @_;
-       return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any);
-}
-
 sub timed_parse_file
 {
        my ($file) = @_;
@@ -313,11 +450,9 @@ sub parse_file
                return;
        }
 
-       if (skip_parse($file)) {
-               dprint "skipping file: $file\n";
+       if (! -T $file) {
                return;
        }
-       dprint "parsing: $file\n";
 
        open my $fh, "<", $file or return;
        while ( <$fh> ) {
@@ -328,12 +463,14 @@ sub parse_file
        close $fh;
 }
 
-
-# True if we should skip walking this directory.
-sub skip_walk
+# Checks if the actual path name is leaking a kernel address.
+sub check_path_for_leaks
 {
        my ($path) = @_;
-       return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any)
+
+       if (may_leak_address($path)) {
+               printf("Path name may contain address: $path\n");
+       }
 }
 
 # Recursively walk directory tree.
@@ -342,7 +479,6 @@ sub walk
        my @dirs = @_;
 
        while (my $pwd = shift @dirs) {
-               next if (skip_walk($pwd));
                next if (!opendir(DIR, $pwd));
                my @files = readdir(DIR);
                closedir(DIR);
@@ -353,11 +489,21 @@ sub walk
                        my $path = "$pwd/$file";
                        next if (-l $path);
 
+                       # skip /proc/PID except /proc/1
+                       next if (($path =~ /^\/proc\/[0-9]+$/) &&
+                                ($path !~ /^\/proc\/1$/));
+
+                       next if (skip($path));
+
+                       check_path_for_leaks($path);
+
                        if (-d $path) {
                                push @dirs, $path;
-                       } else {
-                               timed_parse_file($path);
+                               next;
                        }
+
+                       dprint "parsing: $path\n";
+                       timed_parse_file($path);
                }
        }
 }