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