aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-08-29 14:29:23 +0200
committerAleksandr Nogikh <nogikh@google.com>2025-08-29 16:37:53 +0000
commit807a3b61ca22f8988561c180eb47268ea6e244db (patch)
treec29598ca28b49bca60e3961dc49d41ff7142cd8f
parentb0c32b0591eb98d9fd9a2590b140eb771cf86c46 (diff)
syz-cluster: consider global/static variable values
When determining whether a patch series is worth fuzzing, consider not only the hashes of .text symbols, but also the hashes of the global (static and non-static) variables. As before, calculate the hashes during build and process them at the beginning of the fuzz step.
-rw-r--r--pkg/build/linux.go68
-rw-r--r--syz-cluster/workflow/build-step/main.go3
-rw-r--r--syz-cluster/workflow/fuzz-step/main.go50
-rw-r--r--syz-cluster/workflow/fuzz-step/main_test.go81
4 files changed, 155 insertions, 47 deletions
diff --git a/pkg/build/linux.go b/pkg/build/linux.go
index 6fc15733d..baa77dc62 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 943b18700..0fb89225f 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 7e1589115..efbbdd3d4 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 0336281b8..038690bd6 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"},
+ },
))
})
}