Merge tag 'dma-mapping-4.16-3' of git://git.infradead.org/users/hch/dma-mapping
[sfrench/cifs-2.6.git] / scripts / leaking_addresses.pl
1 #!/usr/bin/env perl
2 #
3 # (c) 2017 Tobin C. Harding <me@tobin.cc>
4 # Licensed under the terms of the GNU GPL License version 2
5 #
6 # leaking_addresses.pl: Scan 64 bit kernel for potential leaking addresses.
7 #  - Scans dmesg output.
8 #  - Walks directory tree and parses each file (for each directory in @DIRS).
9 #
10 # Use --debug to output path before parsing, this is useful to find files that
11 # cause the script to choke.
12 #
13 # You may like to set kptr_restrict=2 before running script
14 # (see Documentation/sysctl/kernel.txt).
15
16 use warnings;
17 use strict;
18 use POSIX;
19 use File::Basename;
20 use File::Spec;
21 use Cwd 'abs_path';
22 use Term::ANSIColor qw(:constants);
23 use Getopt::Long qw(:config no_auto_abbrev);
24 use Config;
25
26 my $P = $0;
27 my $V = '0.01';
28
29 # Directories to scan.
30 my @DIRS = ('/proc', '/sys');
31
32 # Timer for parsing each file, in seconds.
33 my $TIMEOUT = 10;
34
35 # Script can only grep for kernel addresses on the following architectures. If
36 # your architecture is not listed here and has a grep'able kernel address please
37 # consider submitting a patch.
38 my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64');
39
40 # Command line options.
41 my $help = 0;
42 my $debug = 0;
43 my $raw = 0;
44 my $output_raw = "";    # Write raw results to file.
45 my $input_raw = "";     # Read raw results from file instead of scanning.
46
47 my $suppress_dmesg = 0;         # Don't show dmesg in output.
48 my $squash_by_path = 0;         # Summary report grouped by absolute path.
49 my $squash_by_filename = 0;     # Summary report grouped by filename.
50
51 # Do not parse these files (absolute path).
52 my @skip_parse_files_abs = ('/proc/kmsg',
53                             '/proc/kcore',
54                             '/proc/fs/ext4/sdb1/mb_groups',
55                             '/proc/1/fd/3',
56                             '/sys/firmware/devicetree',
57                             '/proc/device-tree',
58                             '/sys/kernel/debug/tracing/trace_pipe',
59                             '/sys/kernel/security/apparmor/revision');
60
61 # Do not parse these files under any subdirectory.
62 my @skip_parse_files_any = ('0',
63                             '1',
64                             '2',
65                             'pagemap',
66                             'events',
67                             'access',
68                             'registers',
69                             'snapshot_raw',
70                             'trace_pipe_raw',
71                             'ptmx',
72                             'trace_pipe');
73
74 # Do not walk these directories (absolute path).
75 my @skip_walk_dirs_abs = ();
76
77 # Do not walk these directories under any subdirectory.
78 my @skip_walk_dirs_any = ('self',
79                           'thread-self',
80                           'cwd',
81                           'fd',
82                           'usbmon',
83                           'stderr',
84                           'stdin',
85                           'stdout');
86
87 sub help
88 {
89         my ($exitcode) = @_;
90
91         print << "EOM";
92
93 Usage: $P [OPTIONS]
94 Version: $V
95
96 Options:
97
98         -o, --output-raw=<file>  Save results for future processing.
99         -i, --input-raw=<file>   Read results from file instead of scanning.
100             --raw                Show raw results (default).
101             --suppress-dmesg     Do not show dmesg results.
102             --squash-by-path     Show one result per unique path.
103             --squash-by-filename Show one result per unique filename.
104         -d, --debug              Display debugging output.
105         -h, --help, --version    Display this help and exit.
106
107 Examples:
108
109         # Scan kernel and dump raw results.
110         $0
111
112         # Scan kernel and save results to file.
113         $0 --output-raw scan.out
114
115         # View summary report.
116         $0 --input-raw scan.out --squash-by-filename
117
118 Scans the running (64 bit) kernel for potential leaking addresses.
119
120 EOM
121         exit($exitcode);
122 }
123
124 GetOptions(
125         'd|debug'               => \$debug,
126         'h|help'                => \$help,
127         'version'               => \$help,
128         'o|output-raw=s'        => \$output_raw,
129         'i|input-raw=s'         => \$input_raw,
130         'suppress-dmesg'        => \$suppress_dmesg,
131         'squash-by-path'        => \$squash_by_path,
132         'squash-by-filename'    => \$squash_by_filename,
133         'raw'                   => \$raw,
134 ) or help(1);
135
136 help(0) if ($help);
137
138 if ($input_raw) {
139         format_output($input_raw);
140         exit(0);
141 }
142
143 if (!$input_raw and ($squash_by_path or $squash_by_filename)) {
144         printf "\nSummary reporting only available with --input-raw=<file>\n";
145         printf "(First run scan with --output-raw=<file>.)\n";
146         exit(128);
147 }
148
149 if (!is_supported_architecture()) {
150         printf "\nScript does not support your architecture, sorry.\n";
151         printf "\nCurrently we support: \n\n";
152         foreach(@SUPPORTED_ARCHITECTURES) {
153                 printf "\t%s\n", $_;
154         }
155
156         my $archname = $Config{archname};
157         printf "\n\$ perl -MConfig -e \'print \"\$Config{archname}\\n\"\'\n";
158         printf "%s\n", $archname;
159
160         exit(129);
161 }
162
163 if ($output_raw) {
164         open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n";
165         select $fh;
166 }
167
168 parse_dmesg();
169 walk(@DIRS);
170
171 exit 0;
172
173 sub dprint
174 {
175         printf(STDERR @_) if $debug;
176 }
177
178 sub is_supported_architecture
179 {
180         return (is_x86_64() or is_ppc64());
181 }
182
183 sub is_x86_64
184 {
185         my $archname = $Config{archname};
186
187         if ($archname =~ m/x86_64/) {
188                 return 1;
189         }
190         return 0;
191 }
192
193 sub is_ppc64
194 {
195         my $archname = $Config{archname};
196
197         if ($archname =~ m/powerpc/ and $archname =~ m/64/) {
198                 return 1;
199         }
200         return 0;
201 }
202
203 sub is_false_positive
204 {
205         my ($match) = @_;
206
207         if ($match =~ '\b(0x)?(f|F){16}\b' or
208             $match =~ '\b(0x)?0{16}\b') {
209                 return 1;
210         }
211
212         if (is_x86_64) {
213                 # vsyscall memory region, we should probably check against a range here.
214                 if ($match =~ '\bf{10}600000\b' or
215                     $match =~ '\bf{10}601000\b') {
216                         return 1;
217                 }
218         }
219
220         return 0;
221 }
222
223 # True if argument potentially contains a kernel address.
224 sub may_leak_address
225 {
226         my ($line) = @_;
227         my $address_re;
228
229         # Signal masks.
230         if ($line =~ '^SigBlk:' or
231             $line =~ '^SigIgn:' or
232             $line =~ '^SigCgt:') {
233                 return 0;
234         }
235
236         if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or
237             $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') {
238                 return 0;
239         }
240
241         # One of these is guaranteed to be true.
242         if (is_x86_64()) {
243                 $address_re = '\b(0x)?ffff[[:xdigit:]]{12}\b';
244         } elsif (is_ppc64()) {
245                 $address_re = '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b';
246         }
247
248         while (/($address_re)/g) {
249                 if (!is_false_positive($1)) {
250                         return 1;
251                 }
252         }
253
254         return 0;
255 }
256
257 sub parse_dmesg
258 {
259         open my $cmd, '-|', 'dmesg';
260         while (<$cmd>) {
261                 if (may_leak_address($_)) {
262                         print 'dmesg: ' . $_;
263                 }
264         }
265         close $cmd;
266 }
267
268 # True if we should skip this path.
269 sub skip
270 {
271         my ($path, $paths_abs, $paths_any) = @_;
272
273         foreach (@$paths_abs) {
274                 return 1 if (/^$path$/);
275         }
276
277         my($filename, $dirs, $suffix) = fileparse($path);
278         foreach (@$paths_any) {
279                 return 1 if (/^$filename$/);
280         }
281
282         return 0;
283 }
284
285 sub skip_parse
286 {
287         my ($path) = @_;
288         return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any);
289 }
290
291 sub timed_parse_file
292 {
293         my ($file) = @_;
294
295         eval {
296                 local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required.
297                 alarm $TIMEOUT;
298                 parse_file($file);
299                 alarm 0;
300         };
301
302         if ($@) {
303                 die unless $@ eq "alarm\n";     # Propagate unexpected errors.
304                 printf STDERR "timed out parsing: %s\n", $file;
305         }
306 }
307
308 sub parse_file
309 {
310         my ($file) = @_;
311
312         if (! -R $file) {
313                 return;
314         }
315
316         if (skip_parse($file)) {
317                 dprint "skipping file: $file\n";
318                 return;
319         }
320         dprint "parsing: $file\n";
321
322         open my $fh, "<", $file or return;
323         while ( <$fh> ) {
324                 if (may_leak_address($_)) {
325                         print $file . ': ' . $_;
326                 }
327         }
328         close $fh;
329 }
330
331
332 # True if we should skip walking this directory.
333 sub skip_walk
334 {
335         my ($path) = @_;
336         return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any)
337 }
338
339 # Recursively walk directory tree.
340 sub walk
341 {
342         my @dirs = @_;
343
344         while (my $pwd = shift @dirs) {
345                 next if (skip_walk($pwd));
346                 next if (!opendir(DIR, $pwd));
347                 my @files = readdir(DIR);
348                 closedir(DIR);
349
350                 foreach my $file (@files) {
351                         next if ($file eq '.' or $file eq '..');
352
353                         my $path = "$pwd/$file";
354                         next if (-l $path);
355
356                         if (-d $path) {
357                                 push @dirs, $path;
358                         } else {
359                                 timed_parse_file($path);
360                         }
361                 }
362         }
363 }
364
365 sub format_output
366 {
367         my ($file) = @_;
368
369         # Default is to show raw results.
370         if ($raw or (!$squash_by_path and !$squash_by_filename)) {
371                 dump_raw_output($file);
372                 return;
373         }
374
375         my ($total, $dmesg, $paths, $files) = parse_raw_file($file);
376
377         printf "\nTotal number of results from scan (incl dmesg): %d\n", $total;
378
379         if (!$suppress_dmesg) {
380                 print_dmesg($dmesg);
381         }
382
383         if ($squash_by_filename) {
384                 squash_by($files, 'filename');
385         }
386
387         if ($squash_by_path) {
388                 squash_by($paths, 'path');
389         }
390 }
391
392 sub dump_raw_output
393 {
394         my ($file) = @_;
395
396         open (my $fh, '<', $file) or die "$0: $file: $!\n";
397         while (<$fh>) {
398                 if ($suppress_dmesg) {
399                         if ("dmesg:" eq substr($_, 0, 6)) {
400                                 next;
401                         }
402                 }
403                 print $_;
404         }
405         close $fh;
406 }
407
408 sub parse_raw_file
409 {
410         my ($file) = @_;
411
412         my $total = 0;          # Total number of lines parsed.
413         my @dmesg;              # dmesg output.
414         my %files;              # Unique filenames containing leaks.
415         my %paths;              # Unique paths containing leaks.
416
417         open (my $fh, '<', $file) or die "$0: $file: $!\n";
418         while (my $line = <$fh>) {
419                 $total++;
420
421                 if ("dmesg:" eq substr($line, 0, 6)) {
422                         push @dmesg, $line;
423                         next;
424                 }
425
426                 cache_path(\%paths, $line);
427                 cache_filename(\%files, $line);
428         }
429
430         return $total, \@dmesg, \%paths, \%files;
431 }
432
433 sub print_dmesg
434 {
435         my ($dmesg) = @_;
436
437         print "\ndmesg output:\n";
438
439         if (@$dmesg == 0) {
440                 print "<no results>\n";
441                 return;
442         }
443
444         foreach(@$dmesg) {
445                 my $index = index($_, ': ');
446                 $index += 2;    # skid ': '
447                 print substr($_, $index);
448         }
449 }
450
451 sub squash_by
452 {
453         my ($ref, $desc) = @_;
454
455         print "\nResults squashed by $desc (excl dmesg). ";
456         print "Displaying [<number of results> <$desc>], <example result>\n";
457
458         if (keys %$ref == 0) {
459                 print "<no results>\n";
460                 return;
461         }
462
463         foreach(keys %$ref) {
464                 my $lines = $ref->{$_};
465                 my $length = @$lines;
466                 printf "[%d %s] %s", $length, $_, @$lines[0];
467         }
468 }
469
470 sub cache_path
471 {
472         my ($paths, $line) = @_;
473
474         my $index = index($line, ': ');
475         my $path = substr($line, 0, $index);
476
477         $index += 2;            # skip ': '
478         add_to_cache($paths, $path, substr($line, $index));
479 }
480
481 sub cache_filename
482 {
483         my ($files, $line) = @_;
484
485         my $index = index($line, ': ');
486         my $path = substr($line, 0, $index);
487         my $filename = basename($path);
488
489         $index += 2;            # skip ': '
490         add_to_cache($files, $filename, substr($line, $index));
491 }
492
493 sub add_to_cache
494 {
495         my ($cache, $key, $value) = @_;
496
497         if (!$cache->{$key}) {
498                 $cache->{$key} = ();
499         }
500         push @{$cache->{$key}}, $value;
501 }