diff --git a/kernel/module.c b/kernel/module.c index 837fb1a82699..e337b3af13c7 100644 --- a/kernel/module.c +++ b/kernel/module.c @@ -4254,6 +4254,11 @@ static inline int is_arm_mapping_symbol(const char *str) && (str[2] == '\0' || str[2] == '.'); } +static inline int is_cfi_typeid_symbol(const char *str) +{ + return !strncmp(str, "__typeid__", 10); +} + static const char *kallsyms_symbol_name(struct mod_kallsyms *kallsyms, unsigned int symnum) { return kallsyms->strtab + kallsyms->symtab[symnum].st_name; @@ -4292,7 +4297,8 @@ static const char *find_kallsyms_symbol(struct module *mod, /* We ignore unnamed symbols: they're uninformative * and inserted at a whim. */ if (*kallsyms_symbol_name(kallsyms, i) == '\0' - || is_arm_mapping_symbol(kallsyms_symbol_name(kallsyms, i))) + || is_arm_mapping_symbol(kallsyms_symbol_name(kallsyms, i)) + || is_cfi_typeid_symbol(kallsyms_symbol_name(kallsyms, i))) continue; if (thisval <= addr && thisval > bestval) { diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal index a76948df3c51..9ae3d9ae3d36 100644 --- a/scripts/Makefile.modfinal +++ b/scripts/Makefile.modfinal @@ -58,6 +58,23 @@ quiet_cmd_ld_ko_o = LD [M] $@ -T scripts/module.lds -o $@ $(filter %.o, $^); \ $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true) +ifdef CONFIG_CFI_CLANG +# LLVM can drops jump table symbols from the final binary. Add them +# back to make stack traces and other symbol output readable. +cmd_ld_ko_o += ; \ + $(srctree)/scripts/generate_cfi_kallsyms.pl --module \ + $@ > $(@:.ko=.lds); \ + if [ -s $(@:.ko=.lds) ]; then \ + $(LD) -r $(KBUILD_LDFLAGS) \ + $(KBUILD_LDFLAGS_MODULE) $(LDFLAGS_MODULE) \ + -T $(@:.ko=.lds) \ + -o $(@:.ko=.tmp.ko) $@; \ + mv -f $(@:.ko=.tmp.ko) $@; \ + else \ + rm -f $(@:.ko=.lds); \ + fi +endif + $(modules): %.ko: %$(prelink-ext).o %.mod.o scripts/module.lds FORCE +$(call if_changed,ld_ko_o) diff --git a/scripts/generate_cfi_kallsyms.pl b/scripts/generate_cfi_kallsyms.pl new file mode 100755 index 000000000000..6aabd686fba7 --- /dev/null +++ b/scripts/generate_cfi_kallsyms.pl @@ -0,0 +1,308 @@ +#!/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(); +} diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh index 3b261b0f74f0..6ae0d70089b4 100755 --- a/scripts/link-vmlinux.sh +++ b/scripts/link-vmlinux.sh @@ -260,7 +260,15 @@ kallsyms() fi info KSYMS ${2} - ${NM} -n ${1} | scripts/kallsyms ${kallsymopt} > ${2} + + if [ -n "${CONFIG_CFI_CLANG}" ]; then + ${PERL} ${srctree}/scripts/generate_cfi_kallsyms.pl ${1} | \ + sort -n > .tmp_kallsyms + else + ${NM} -n ${1} > .tmp_kallsyms + fi + + scripts/kallsyms ${kallsymopt} < .tmp_kallsyms > ${2} } # Perform one step in kallsyms generation, including temporary linking of @@ -298,6 +306,7 @@ cleanup() { rm -f .btf.* rm -f .tmp_System.map + rm -f .tmp_kallsyms rm -f .tmp_initcalls.lds rm -f .tmp_symversions.lds rm -f .tmp_vmlinux* diff --git a/scripts/module.lds.S b/scripts/module.lds.S index a1e1f95c1f0d..488a6f742624 100644 --- a/scripts/module.lds.S +++ b/scripts/module.lds.S @@ -60,7 +60,10 @@ SECTIONS { */ .text : ALIGN(PAGE_SIZE) { *(.text.__cfi_check) - *(.text .text.[0-9a-zA-Z_]* .text..L.cfi*) + *(.text .text.[0-9a-zA-Z_]*) + __cfi_jt_start = .; + *(.text..L.cfi.jumptable .text..L.cfi.jumptable.*) + __cfi_jt_end = .; } #endif }