123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- #!/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();
- }
|