| #!/usr/bin/env perl |
| # SPDX-License-Identifier: GPL-2.0 |
| # |
| # Generates a list of Control-Flow Integrity (CFI) jump table symbols |
| # for kallsyms. |
| # |
| # Copyright (C) 2021 Google LLC |
| |
| use strict; |
| use warnings; |
| |
| ## parameters |
| my $ismodule = 0; |
| my $file; |
| |
| foreach (@ARGV) { |
| if ($_ eq '--module') { |
| $ismodule = 1; |
| } elsif (!defined($file)) { |
| $file = $_; |
| } else { |
| die "$0: usage $0 [--module] binary"; |
| } |
| } |
| |
| ## environment |
| my $readelf = $ENV{'READELF'} || die "$0: ERROR: READELF not set?"; |
| my $objdump = $ENV{'OBJDUMP'} || die "$0: ERROR: OBJDUMP not set?"; |
| my $nm = $ENV{'NM'} || die "$0: ERROR: NM not set?"; |
| |
| ## jump table addresses |
| my $cfi_jt = {}; |
| ## text symbols |
| my $text_symbols = {}; |
| |
| ## parser state |
| use constant { |
| UNKNOWN => 0, |
| SYMBOL => 1, |
| HINT => 2, |
| BRANCH => 3, |
| RELOC => 4 |
| }; |
| |
| ## trims leading zeros from a string |
| sub trim_zeros { |
| my ($n) = @_; |
| $n =~ s/^0+//; |
| $n = 0 if ($n eq ''); |
| return $n; |
| } |
| |
| ## finds __cfi_jt_* symbols from the binary to locate the start and end of the |
| ## jump table |
| sub find_cfi_jt { |
| open(my $fh, "\"$readelf\" --symbols \"$file\" 2>/dev/null | grep __cfi_jt_ |") |
| or die "$0: ERROR: failed to execute \"$readelf\": $!"; |
| |
| while (<$fh>) { |
| chomp; |
| |
| my ($addr, $name) = $_ =~ /\:.*([a-f0-9]{16}).*\s__cfi_jt_(.*)/; |
| if (defined($addr) && defined($name)) { |
| $cfi_jt->{$name} = $addr; |
| } |
| } |
| |
| close($fh); |
| |
| die "$0: ERROR: __cfi_jt_start symbol missing" if !exists($cfi_jt->{"start"}); |
| die "$0: ERROR: __cfi_jt_end symbol missing" if !exists($cfi_jt->{"end"}); |
| } |
| |
| my $last = UNKNOWN; |
| my $last_symbol; |
| my $last_hint_addr; |
| my $last_branch_addr; |
| my $last_branch_target; |
| my $last_reloc_target; |
| |
| sub is_symbol { |
| my ($line) = @_; |
| my ($addr, $symbol) = $_ =~ /^([a-f0-9]{16})\s<([^>]+)>\:/; |
| |
| if (defined($addr) && defined($symbol)) { |
| $last = SYMBOL; |
| $last_symbol = $symbol; |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| sub is_hint { |
| my ($line) = @_; |
| my ($hint) = $_ =~ /^\s*([a-f0-9]+)\:.*\s+hint\s+#/; |
| |
| if (defined($hint)) { |
| $last = HINT; |
| $last_hint_addr = $hint; |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| sub find_text_symbol { |
| my ($target) = @_; |
| |
| my ($symbol, $expr, $offset) = $target =~ /^(\S*)([-\+])0x([a-f0-9]+)?$/; |
| |
| if (!defined($symbol) || !defined(!$expr) || !defined($offset)) { |
| return $target; |
| } |
| |
| if ($symbol =~ /^\.((init|exit)\.)?text$/ && $expr eq '+') { |
| $offset = trim_zeros($offset); |
| my $actual = $text_symbols->{"$symbol+$offset"}; |
| |
| if (!defined($actual)) { |
| die "$0: unknown symbol at $symbol+0x$offset"; |
| } |
| |
| $symbol = $actual; |
| } |
| |
| return $symbol; |
| } |
| |
| sub is_branch { |
| my ($line) = @_; |
| my ($addr, $instr, $branch_target) = $_ =~ |
| /^\s*([a-f0-9]+)\:.*(b|jmpq?)\s+0x[a-f0-9]+\s+<([^>]+)>/; |
| |
| if (defined($addr) && defined($instr) && defined($branch_target)) { |
| if ($last eq HINT) { |
| $last_branch_addr = $last_hint_addr; |
| } else { |
| $last_branch_addr = $addr; |
| } |
| |
| $last = BRANCH; |
| $last_branch_target = find_text_symbol($branch_target); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| sub is_branch_reloc { |
| my ($line) = @_; |
| |
| if ($last ne BRANCH) { |
| return 0; |
| } |
| |
| my ($addr, $type, $reloc_target) = /\s*([a-f0-9]{16})\:\s+R_(\S+)\s+(\S+)$/; |
| |
| if (defined($addr) && defined($type) && defined($reloc_target)) { |
| $last = RELOC; |
| $last_reloc_target = find_text_symbol($reloc_target); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| ## walks through the jump table looking for branches and prints out a jump |
| ## table symbol for each branch if one is missing |
| sub print_missing_symbols { |
| my @symbols; |
| |
| open(my $fh, "\"$objdump\" -d -r " . |
| "--start-address=0x" . $cfi_jt->{"start"} . |
| " --stop-address=0x" . $cfi_jt->{"end"} . |
| " \"$file\" 2>/dev/null |") |
| or die "$0: ERROR: failed to execute \"$objdump\": $!"; |
| |
| while (<$fh>) { |
| chomp; |
| |
| if (is_symbol($_) || is_hint($_)) { |
| next; |
| } |
| |
| my $cfi_jt_symbol; |
| |
| if (is_branch($_)) { |
| if ($ismodule) { |
| next; # wait for the relocation |
| } |
| |
| $cfi_jt_symbol = $last_branch_target; |
| } elsif (is_branch_reloc($_)) { |
| $cfi_jt_symbol = $last_reloc_target; |
| } else { |
| next; |
| } |
| |
| # ignore functions with a canonical jump table |
| if ($cfi_jt_symbol =~ /\.cfi$/) { |
| next; |
| } |
| |
| $cfi_jt_symbol .= ".cfi_jt"; |
| $cfi_jt->{$last_branch_addr} = $cfi_jt_symbol; |
| |
| if (defined($last_symbol) && $last_symbol eq $cfi_jt_symbol) { |
| next; # already exists |
| } |
| |
| # print out the symbol |
| if ($ismodule) { |
| push(@symbols, "\t\t$cfi_jt_symbol = . + 0x$last_branch_addr;"); |
| } else { |
| push(@symbols, "$last_branch_addr t $cfi_jt_symbol"); |
| } |
| } |
| |
| close($fh); |
| |
| if (!scalar(@symbols)) { |
| return; |
| } |
| |
| if ($ismodule) { |
| print "SECTIONS {\n"; |
| # With -fpatchable-function-entry, LLD isn't happy without this |
| print "\t__patchable_function_entries : { *(__patchable_function_entries) }\n"; |
| print "\t.text : {\n"; |
| } |
| |
| foreach (@symbols) { |
| print "$_\n"; |
| } |
| |
| if ($ismodule) { |
| print "\t}\n}\n"; |
| } |
| } |
| |
| ## reads defined text symbols from the file |
| sub read_symbols { |
| open(my $fh, "\"$objdump\" --syms \"$file\" 2>/dev/null |") |
| or die "$0: ERROR: failed to execute \"$nm\": $!"; |
| |
| while (<$fh>) { |
| chomp; |
| |
| # llvm/tools/llvm-objdump/objdump.cpp:objdump::printSymbol |
| my ($addr, $debug, $section, $ref, $symbol) = $_ =~ |
| /^([a-f0-9]{16})\s.{5}(.).{2}(\S+)\s[a-f0-9]{16}(\s\.\S+)?\s(.*)$/; |
| |
| if (defined($addr) && defined($section) && defined($symbol)) { |
| if (!($section =~ /^\.((init|exit)\.)?text$/)) { |
| next; |
| } |
| # skip arm mapping symbols |
| if ($symbol =~ /^\$[xd]\.\d+$/) { |
| next; |
| } |
| if (defined($debug) && $debug eq "d") { |
| next; |
| } |
| |
| $addr = trim_zeros($addr); |
| $text_symbols->{"$section+$addr"} = $symbol; |
| } |
| } |
| |
| close($fh); |
| } |
| |
| ## prints out the remaining symbols from nm -n, filtering out the unnecessary |
| ## __typeid__ symbols aliasing the jump table symbols we added |
| sub print_kallsyms { |
| open(my $fh, "\"$nm\" -n \"$file\" 2>/dev/null |") |
| or die "$0: ERROR: failed to execute \"$nm\": $!"; |
| |
| while (<$fh>) { |
| chomp; |
| |
| my ($addr, $symbol) = $_ =~ /^([a-f0-9]{16})\s.\s(.*)$/; |
| |
| if (defined($addr) && defined($symbol)) { |
| # drop duplicate __typeid__ symbols |
| if ($symbol =~ /^__typeid__.*_global_addr$/ && |
| exists($cfi_jt->{$addr})) { |
| next; |
| } |
| } |
| |
| print "$_\n"; |
| } |
| |
| close($fh); |
| } |
| |
| ## main |
| find_cfi_jt(); |
| |
| if ($ismodule) { |
| read_symbols(); |
| print_missing_symbols(); |
| } else { |
| print_missing_symbols(); |
| print_kallsyms(); |
| } |