diff --git a/pkg/build/linux.go b/pkg/build/linux.go index 6fc15733dcc5..baa77dc62e2e 100644 --- a/pkg/build/linux.go +++ b/pkg/build/linux.go @@ -10,7 +10,6 @@ import ( "debug/elf" "encoding/hex" "fmt" - "io" "os" "path" "path/filepath" @@ -260,55 +259,80 @@ func queryLinuxCompiler(kernelDir string) (string, error) { return string(result[1]), nil } -// ElfSymbolHashes returns a map of sha256 hashes per a symbol contained in the elf file. +type SectionHashes struct { + Text map[string]string `json:"text"` + Data map[string]string `json:"data"` // Merged .data and .rodata. +} + +// ElfSymbolHashes returns a map of sha256 hashes per section per symbol contained in the elf file. // It's best to call it on vmlinux.o since PCs in the binary code are not patched yet. -func ElfSymbolHashes(bin string) (map[string]string, error) { +func ElfSymbolHashes(bin string) (SectionHashes, error) { + result := SectionHashes{ + Text: make(map[string]string), + Data: make(map[string]string), + } + file, err := elf.Open(bin) if err != nil { - return nil, err + return SectionHashes{}, err } defer file.Close() symbols, err := file.Symbols() if err != nil { - return nil, err + return SectionHashes{}, err } - textSection := file.Section(".text") - if textSection == nil { - return nil, fmt.Errorf(".text section not found") + rawFile, err := os.Open(bin) + if err != nil { + return SectionHashes{}, err } + defer rawFile.Close() - sectionReader, ok := textSection.Open().(io.ReaderAt) - if !ok { - return nil, fmt.Errorf(".text section reader does not support ReadAt") + sections := make(map[elf.SectionIndex]*elf.Section) + for i, s := range file.Sections { + sections[elf.SectionIndex(i)] = s } - hashes := make(map[string]string) for _, s := range symbols { - if elf.ST_TYPE(s.Info) != elf.STT_FUNC || s.Size == 0 { + if s.Name == "" || s.Size == 0 || s.Section >= elf.SHN_LORESERVE { continue } - if s.Section >= elf.SHN_LORESERVE || int(s.Section) >= len(file.Sections) || - file.Sections[s.Section] != textSection { + symbolSection, ok := sections[s.Section] + if !ok || symbolSection.Type == elf.SHT_NOBITS { continue } - offset := s.Value - textSection.Addr - if offset+s.Size > textSection.Size { + var targetMap map[string]string + + symbolType := elf.ST_TYPE(s.Info) + sectionFlags := symbolSection.Flags + switch { + case symbolType == elf.STT_FUNC && (sectionFlags&elf.SHF_EXECINSTR) != 0: + targetMap = result.Text + case symbolType == elf.STT_OBJECT && (sectionFlags&elf.SHF_ALLOC) != 0 && + (sectionFlags&elf.SHF_EXECINSTR) == 0: + targetMap = result.Data + default: continue } - code := make([]byte, s.Size) - _, err := sectionReader.ReadAt(code, int64(offset)) + offset := s.Value - symbolSection.Addr + if offset+s.Size > symbolSection.Size { + continue + } + + data := make([]byte, s.Size) + _, err := rawFile.ReadAt(data, int64(symbolSection.Offset+offset)) if err != nil { continue } - hash := sha256.Sum256(code) - hashes[s.Name] = hex.EncodeToString(hash[:]) + + hash := sha256.Sum256(data) + targetMap[s.Name] = hex.EncodeToString(hash[:]) } - return hashes, nil + return result, nil } // elfBinarySignature calculates signature of an elf binary aiming at runtime behavior diff --git a/syz-cluster/workflow/build-step/main.go b/syz-cluster/workflow/build-step/main.go index 943b18700d63..0fb89225fa3f 100644 --- a/syz-cluster/workflow/build-step/main.go +++ b/syz-cluster/workflow/build-step/main.go @@ -293,7 +293,8 @@ func saveSymbolHashes(tracer debugtracer.DebugTracer) error { if err != nil { return fmt.Errorf("failed to query symbol hashes: %w", err) } - tracer.Log("extracted hashes for %d symbols", len(hashes)) + tracer.Log("extracted hashes for %d text symbols and %d data symbols", + len(hashes.Text), len(hashes.Data)) file, err := os.Create(filepath.Join(*flagOutput, "symbol_hashes.json")) if err != nil { return fmt.Errorf("failed to open symbol_hashes.json: %w", err) diff --git a/syz-cluster/workflow/fuzz-step/main.go b/syz-cluster/workflow/fuzz-step/main.go index 7e1589115006..efbbdd3d4f2f 100644 --- a/syz-cluster/workflow/fuzz-step/main.go +++ b/syz-cluster/workflow/fuzz-step/main.go @@ -16,6 +16,7 @@ import ( "path/filepath" "time" + "github.com/google/syzkaller/pkg/build" "github.com/google/syzkaller/pkg/config" "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/manager" @@ -108,7 +109,7 @@ func run(baseCtx context.Context, client *api.Client, timeout time.Duration, if shouldSkipFuzzing(baseSymbols, patchedSymbols) { return errSkipFuzzing } - manager.PatchFocusAreas(patched, series.PatchBodies(), baseSymbols, patchedSymbols) + manager.PatchFocusAreas(patched, series.PatchBodies(), baseSymbols.Text, patchedSymbols.Text) if *flagCorpusURL != "" { err := downloadCorpus(baseCtx, patched.Workdir, *flagCorpusURL) @@ -311,15 +312,32 @@ func reportFinding(ctx context.Context, client *api.Client, bug *manager.UniqueB return client.UploadFinding(ctx, finding) } -func shouldSkipFuzzing(baseSymbols, patchedSymbols map[string]string) bool { - if len(baseSymbols) == 0 || len(patchedSymbols) == 0 { +var ignoreLinuxVariables = map[string]bool{ + "raw_data": true, // from arch/x86/entry/vdso/vdso-image + // Build versions / timestamps. + "linux_banner": true, + "vermagic": true, + "init_uts_ns": true, +} + +func shouldSkipFuzzing(base, patched build.SectionHashes) bool { + if len(base.Text) == 0 || len(patched.Text) == 0 { // Likely, something went wrong during the kernel build step. log.Logf(0, "skipped the binary equality check because some of them have 0 symbols") return false } - same := len(baseSymbols) == len(patchedSymbols) - for name, hash := range baseSymbols { - if patchedSymbols[name] != hash { + same := len(base.Text) == len(patched.Text) && len(base.Data) == len(patched.Data) + // For .text, demand all symbols to be equal. + for name, hash := range base.Text { + if patched.Text[name] != hash { + same = false + break + } + } + // For data sections ignore some of them. + for name, hash := range base.Data { + if !ignoreLinuxVariables[name] && patched.Data[name] != hash { + log.Logf(1, "symbol %q has different values in base vs patch", name) same = false break } @@ -332,31 +350,31 @@ func shouldSkipFuzzing(baseSymbols, patchedSymbols map[string]string) bool { return false } -func readSymbolHashes() (base, patched map[string]string, err error) { +func readSymbolHashes() (base, patched build.SectionHashes, err error) { // These are saved by the build step. - base, err = readJSONMap("/base/symbol_hashes.json") + base, err = readSectionHashes("/base/symbol_hashes.json") if err != nil { - return nil, nil, fmt.Errorf("failed to read base hashes: %w", err) + return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read base hashes: %w", err) } - patched, err = readJSONMap("/patched/symbol_hashes.json") + patched, err = readSectionHashes("/patched/symbol_hashes.json") if err != nil { - return nil, nil, fmt.Errorf("failed to read patched hashes: %w", err) + return build.SectionHashes{}, build.SectionHashes{}, fmt.Errorf("failed to read patched hashes: %w", err) } - log.Logf(0, "extracted %d symbol hashes for base and %d for patched", len(base), len(patched)) + log.Logf(0, "extracted %d text symbol hashes for base and %d for patched", len(base.Text), len(patched.Text)) return } -func readJSONMap(file string) (map[string]string, error) { +func readSectionHashes(file string) (build.SectionHashes, error) { f, err := os.Open(file) if err != nil { - return nil, err + return build.SectionHashes{}, err } defer f.Close() - var data map[string]string + var data build.SectionHashes err = json.NewDecoder(f).Decode(&data) if err != nil { - return nil, err + return build.SectionHashes{}, err } return data, nil } diff --git a/syz-cluster/workflow/fuzz-step/main_test.go b/syz-cluster/workflow/fuzz-step/main_test.go index 0336281b8c3c..038690bd6aad 100644 --- a/syz-cluster/workflow/fuzz-step/main_test.go +++ b/syz-cluster/workflow/fuzz-step/main_test.go @@ -4,11 +4,16 @@ package main import ( + "encoding/json" "io/fs" + "os" "path/filepath" "testing" + "github.com/google/syzkaller/pkg/build" + "github.com/google/syzkaller/pkg/osutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfigLoad(t *testing.T) { @@ -29,26 +34,86 @@ func TestConfigLoad(t *testing.T) { }) } +func TestReadSectionHashes(t *testing.T) { + hashes := build.SectionHashes{ + Text: map[string]string{"A": "1"}, + Data: map[string]string{"B": "2"}, + } + + jsonData, err := json.Marshal(hashes) + require.NoError(t, err) + + file, err := osutil.WriteTempFile(jsonData) + require.NoError(t, err) + defer os.Remove(file) + + fromFile, err := readSectionHashes(file) + require.NoError(t, err) + assert.Equal(t, hashes, fromFile) +} + +// nolint: dupl func TestShouldSkipFuzzing(t *testing.T) { t.Run("one empty", func(t *testing.T) { - assert.False(t, shouldSkipFuzzing(nil, map[string]string{"A": "1"})) + assert.False(t, shouldSkipFuzzing( + build.SectionHashes{}, + build.SectionHashes{ + Text: map[string]string{"A": "1"}, + }, + )) + }) + t.Run("equal symbols", func(t *testing.T) { + assert.True(t, shouldSkipFuzzing( + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "D": "2"}, + }, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "D": "2"}, + }, + )) }) - t.Run("equal", func(t *testing.T) { + t.Run("ignore known variables", func(t *testing.T) { assert.True(t, shouldSkipFuzzing( - map[string]string{"A": "1", "B": "2"}, - map[string]string{"A": "1", "B": "2"}, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "raw_data": "A", "vermagic": "A"}, + }, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "raw_data": "B", "vermagic": "B"}, + }, )) }) t.Run("same len, different hashes", func(t *testing.T) { assert.False(t, shouldSkipFuzzing( - map[string]string{"A": "1", "B": "2"}, - map[string]string{"A": "1", "B": "different"}, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + }, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "different"}, + }, + )) + assert.False(t, shouldSkipFuzzing( + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "D": "2"}, + }, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + Data: map[string]string{"C": "1", "D": "different"}, + }, )) }) t.Run("different len, same hashes", func(t *testing.T) { assert.False(t, shouldSkipFuzzing( - map[string]string{"A": "1", "B": "2", "C": "3"}, - map[string]string{"A": "1", "B": "2"}, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2"}, + }, + build.SectionHashes{ + Text: map[string]string{"A": "1", "B": "2", "C": "new"}, + }, )) }) }