aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/clangtool/clangtool.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/clangtool/clangtool.go')
-rw-r--r--pkg/clangtool/clangtool.go165
1 files changed, 165 insertions, 0 deletions
diff --git a/pkg/clangtool/clangtool.go b/pkg/clangtool/clangtool.go
new file mode 100644
index 000000000..4e2605a48
--- /dev/null
+++ b/pkg/clangtool/clangtool.go
@@ -0,0 +1,165 @@
+// Copyright 2024 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package clangtool
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math/rand"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/google/syzkaller/pkg/declextract"
+ "github.com/google/syzkaller/pkg/osutil"
+)
+
+type Config struct {
+ ToolBin string
+ KernelSrc string
+ KernelObj string
+ CacheDir string
+ ReuseCache bool
+}
+
+// Run runs the clang tool on all files in the compilation database
+// in the kernel build dir and returns combined output for all files.
+// It always caches results, and optionally reuses previously cached results.
+func Run(cfg *Config) (*declextract.Output, error) {
+ dbFile := filepath.Join(cfg.KernelObj, "compile_commands.json")
+ cmds, err := loadCompileCommands(dbFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load compile commands: %w", err)
+ }
+
+ type result struct {
+ out *declextract.Output
+ err error
+ }
+ results := make(chan *result, 10)
+ files := make(chan string, len(cmds))
+ for w := 0; w < runtime.NumCPU(); w++ {
+ go func() {
+ for file := range files {
+ out, err := runTool(cfg, dbFile, file)
+ results <- &result{out, err}
+ }
+ }()
+ }
+ for _, cmd := range cmds {
+ files <- cmd.File
+ }
+ close(files)
+
+ out := new(declextract.Output)
+ for range cmds {
+ res := <-results
+ if res.err != nil {
+ return nil, res.err
+ }
+ out.Merge(res.out)
+ }
+ out.SortAndDedup()
+ return out, nil
+}
+
+func runTool(cfg *Config, dbFile, file string) (*declextract.Output, error) {
+ relFile := strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(filepath.Clean(file),
+ cfg.KernelSrc), cfg.KernelObj), "/")
+ cacheFile := filepath.Join(cfg.CacheDir, relFile+".json")
+ if cfg.ReuseCache {
+ data, err := os.ReadFile(cacheFile)
+ if err == nil {
+ out, err := unmarshal(data)
+ if err == nil {
+ return out, nil
+ }
+ }
+ }
+ // Suppress warning since we may build the tool on a different clang
+ // version that produces more warnings.
+ data, err := exec.Command(cfg.ToolBin, "-p", dbFile, "--extra-arg=-w", file).Output()
+ if err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ err = fmt.Errorf("%v: %w\n%s", relFile, err, exitErr.Stderr)
+ }
+ return nil, err
+ }
+ out, err := unmarshal(data)
+ if err != nil {
+ return nil, err
+ }
+ fixupFileNames(cfg, out, relFile)
+ normalized, err := json.MarshalIndent(out, "", "\t")
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal output data: %w", err)
+ }
+ osutil.MkdirAll(filepath.Dir(cacheFile))
+ if err := osutil.WriteFile(cacheFile, normalized); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func unmarshal(data []byte) (*declextract.Output, error) {
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.DisallowUnknownFields()
+ out := new(declextract.Output)
+ if err := dec.Decode(out); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal clang tool output: %w\n%s", err, data)
+ }
+ return out, nil
+}
+
+func fixupFileNames(cfg *Config, out *declextract.Output, file string) {
+ // All includes in the tool output are relative to the build dir.
+ // Make them relative to the source dir.
+ for i, inc := range out.Includes {
+ if file, err := filepath.Rel(cfg.KernelSrc, filepath.Join(cfg.KernelObj, inc)); err == nil {
+ out.Includes[i] = file
+ }
+ }
+ out.SetSourceFile(file)
+}
+
+type compileCommand struct {
+ Command string
+ Directory string
+ File string
+}
+
+func loadCompileCommands(dbFile string) ([]compileCommand, error) {
+ data, err := os.ReadFile(dbFile)
+ if err != nil {
+ return nil, err
+ }
+ var cmds []compileCommand
+ if err := json.Unmarshal(data, &cmds); err != nil {
+ return nil, err
+ }
+ // Remove commands that don't relate to the kernel build
+ // (probably some host tools, etc).
+ cmds = slices.DeleteFunc(cmds, func(cmd compileCommand) bool {
+ return !strings.HasSuffix(cmd.File, ".c") ||
+ // Files compiled with gcc are not a part of the kernel
+ // (assuming compile commands were generated with make CC=clang).
+ // They are probably a part of some host tool.
+ strings.HasPrefix(cmd.Command, "gcc") ||
+ // KBUILD should add this define all kernel files.
+ !strings.Contains(cmd.Command, "-DKBUILD_BASENAME")
+ })
+ // Shuffle the order to detect any non-determinism caused by the order early.
+ // The result should be the same regardless.
+ rand.New(rand.NewSource(time.Now().UnixNano())).Shuffle(len(cmds), func(i, j int) {
+ cmds[i], cmds[j] = cmds[j], cmds[i]
+ })
+ return cmds, nil
+}