github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/tools/syz-kconf/kconf.go (about)

     1  // Copyright 2020 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // syz-kconf generates Linux kernel configs in dashboard/config/linux.
     5  // See dashboard/config/linux/README.md for details.
     6  package main
     7  
     8  import (
     9  	"errors"
    10  	"flag"
    11  	"fmt"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/google/syzkaller/pkg/build"
    22  	"github.com/google/syzkaller/pkg/kconfig"
    23  	"github.com/google/syzkaller/pkg/osutil"
    24  	"github.com/google/syzkaller/pkg/tool"
    25  	"github.com/google/syzkaller/pkg/vcs"
    26  	"github.com/google/syzkaller/sys/targets"
    27  )
    28  
    29  const (
    30  	featOverride   = "override"
    31  	featOptional   = "optional"
    32  	featAppend     = "append"
    33  	featWeak       = "weak"
    34  	featBaseline   = "baseline"    // disables extra configs
    35  	featBaseConfig = "base-config" // only set for `-base.config` files
    36  	featModules    = "modules"
    37  	featReduced    = "reduced"
    38  	featClang      = "clang"
    39  	featAndroid    = "android"
    40  	featChromeos   = "chromeos"
    41  )
    42  
    43  func main() {
    44  	var (
    45  		flagSourceDir = flag.String("sourcedir", "", "sourcedir")
    46  		flagConfig    = flag.String("config", "", "config")
    47  		flagInstance  = flag.String("instance", "", "instance")
    48  	)
    49  	flag.Parse()
    50  	if *flagSourceDir == "" {
    51  		tool.Failf("missing mandatory flag -sourcedir")
    52  	}
    53  	repo, err := vcs.NewRepo(targets.Linux, "", *flagSourceDir, vcs.OptPrecious)
    54  	if err != nil {
    55  		tool.Failf("failed to create repo: %v", err)
    56  	}
    57  	instances, unusedFeatures, err := parseMainSpec(*flagConfig)
    58  	if err != nil {
    59  		tool.Fail(err)
    60  	}
    61  	if err := checkConfigs(instances, unusedFeatures); err != nil {
    62  		tool.Fail(err)
    63  	}
    64  	// In order to speed up the process we generate instances that use the same kernel revision in parallel.
    65  	generated := make(map[string]bool)
    66  	for _, inst := range instances {
    67  		// Find the first instance that we did not generate yet.
    68  		if *flagInstance != "" && *flagInstance != inst.Name || generated[inst.Name] {
    69  			continue
    70  		}
    71  		fmt.Printf("git checkout %v %v\n", inst.Kernel.Repo, inst.Kernel.Tag)
    72  		if _, err := repo.SwitchCommit(inst.Kernel.Tag); err != nil {
    73  			if _, err := repo.CheckoutCommit(inst.Kernel.Repo, inst.Kernel.Tag); err != nil {
    74  				tool.Failf("failed to checkout %v/%v: %v", inst.Kernel.Repo, inst.Kernel.Tag, err)
    75  			}
    76  		}
    77  		releaseTag, err := releaseTag(*flagSourceDir)
    78  		if err != nil {
    79  			tool.Fail(err)
    80  		}
    81  		fmt.Printf("kernel release %v\n", releaseTag)
    82  		// Now generate all instances that use this kernel revision in parallel (each will use own build dir).
    83  		batch := 0
    84  		results := make(chan error)
    85  		for _, inst1 := range instances {
    86  			if *flagInstance != "" && *flagInstance != inst1.Name || generated[inst1.Name] || inst1.Kernel != inst.Kernel {
    87  				continue
    88  			}
    89  			fmt.Printf("generating %v...\n", inst1.Name)
    90  			generated[inst1.Name] = true
    91  			batch++
    92  			ctx := &Context{
    93  				Inst:       inst1,
    94  				ConfigDir:  filepath.Dir(*flagConfig),
    95  				SourceDir:  *flagSourceDir,
    96  				ReleaseTag: releaseTag,
    97  			}
    98  			go func() {
    99  				if err := ctx.generate(); err != nil {
   100  					results <- fmt.Errorf("%v failed:\n%w", ctx.Inst.Name, err)
   101  				}
   102  				results <- nil
   103  			}()
   104  		}
   105  		failed := false
   106  		for i := 0; i < batch; i++ {
   107  			if err := <-results; err != nil {
   108  				fmt.Printf("%v\n", err)
   109  				failed = true
   110  			}
   111  		}
   112  		if failed {
   113  			tool.Failf("some configs failed")
   114  		}
   115  	}
   116  	if len(generated) == 0 {
   117  		tool.Failf("unknown instance name")
   118  	}
   119  }
   120  
   121  func checkConfigs(instances []*Instance, unusedFeatures []string) error {
   122  	allFeatures := make(Features)
   123  	for _, feat := range unusedFeatures {
   124  		allFeatures[feat] = true
   125  	}
   126  	for _, inst := range instances {
   127  		for feat := range inst.Features {
   128  			allFeatures[feat] = true
   129  		}
   130  	}
   131  	dedup := make(map[string]bool)
   132  	errorString := ""
   133  	for _, inst := range instances {
   134  		for _, cfg := range inst.Configs {
   135  			if strings.HasPrefix(cfg.Name, "CONFIG_") {
   136  				msg := fmt.Sprintf("Warning: excessive CONFIG_ in %v at %v:%v ?", cfg.Name, cfg.File, cfg.Line)
   137  				errorString += "\n" + msg
   138  			}
   139  			for _, feat := range cfg.Constraints {
   140  				if feat[0] == '-' {
   141  					feat = feat[1:]
   142  				}
   143  				if allFeatures[feat] || releaseRe.MatchString(feat) {
   144  					continue
   145  				}
   146  				msg := fmt.Sprintf("%v:%v: unknown feature %v", cfg.File, cfg.Line, feat)
   147  				if dedup[msg] {
   148  					continue
   149  				}
   150  				dedup[msg] = true
   151  				errorString += "\n" + msg
   152  			}
   153  		}
   154  	}
   155  	if errorString != "" {
   156  		return errors.New(errorString[1:])
   157  	}
   158  	return nil
   159  }
   160  
   161  // Generation context for a single instance.
   162  type Context struct {
   163  	Inst       *Instance
   164  	Target     *targets.Target
   165  	Kconf      *kconfig.KConfig
   166  	ConfigDir  string
   167  	BuildDir   string
   168  	SourceDir  string
   169  	ReleaseTag string
   170  }
   171  
   172  func (ctx *Context) generate() error {
   173  	var err error
   174  	if ctx.BuildDir, err = os.MkdirTemp("", "syz-kconf"); err != nil {
   175  		return err
   176  	}
   177  	defer os.RemoveAll(ctx.BuildDir)
   178  	if err := ctx.setTarget(); err != nil {
   179  		return err
   180  	}
   181  	if ctx.Kconf, err = kconfig.Parse(ctx.Target, filepath.Join(ctx.SourceDir, "Kconfig")); err != nil {
   182  		return err
   183  	}
   184  	if err := ctx.setReleaseFeatures(); err != nil {
   185  		return err
   186  	}
   187  	if err := ctx.mrProper(); err != nil {
   188  		return err
   189  	}
   190  	if err := ctx.executeShell(); err != nil {
   191  		return err
   192  	}
   193  	configFile := filepath.Join(ctx.BuildDir, ".config")
   194  	cf, err := kconfig.ParseConfig(configFile)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	if !ctx.Inst.Features[featBaseline] {
   199  		if err := ctx.addUSBConfigs(cf); err != nil {
   200  			return err
   201  		}
   202  	}
   203  	ctx.applyConfigs(cf)
   204  	if !ctx.Inst.Features[featModules] {
   205  		cf.ModToYes()
   206  	}
   207  	// Set all configs that are not present (actually not present, rather than "is not set") to "is not set".
   208  	// This avoids olddefconfig turning on random things we did not ask for.
   209  	var missing []string
   210  	for _, cfg := range ctx.Kconf.Configs {
   211  		if (cfg.Type == kconfig.TypeTristate || cfg.Type == kconfig.TypeBool) && cf.Map[cfg.Name] == nil {
   212  			missing = append(missing, cfg.Name)
   213  		}
   214  	}
   215  	// Without sorting we can get random changes in the config after olddefconfig.
   216  	sort.Strings(missing)
   217  	for _, name := range missing {
   218  		cf.Set(name, kconfig.No)
   219  	}
   220  	original := cf.Serialize()
   221  	if err := osutil.WriteFile(configFile, original); err != nil {
   222  		return fmt.Errorf("failed to write .config file: %w", err)
   223  	}
   224  	// Save what we've got before olddefconfig for debugging purposes, it allows to see if we did not set a config,
   225  	// or olddefconfig removed it. Save as .tmp so that it's not checked-in accidentially.
   226  	outputFile := filepath.Join(ctx.ConfigDir, ctx.Inst.Name+".config")
   227  	outputFileTmp := outputFile + ".tmp"
   228  	if err := osutil.WriteFile(outputFileTmp, original); err != nil {
   229  		return fmt.Errorf("failed to write tmp config file: %w", err)
   230  	}
   231  	if err := ctx.Make("olddefconfig"); err != nil {
   232  		return err
   233  	}
   234  	cf, err = kconfig.ParseConfig(configFile)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	if err := ctx.verifyConfigs(cf); err != nil {
   239  		return fmt.Errorf("%w: saved config before olddefconfig to %v", err, outputFileTmp)
   240  	}
   241  	if !ctx.Inst.Features[featModules] {
   242  		cf.ModToNo()
   243  	}
   244  	config := []byte(fmt.Sprintf(`# Automatically generated by syz-kconf; DO NOT EDIT.
   245  # Kernel: %v %v
   246  
   247  %s
   248  %s
   249  `,
   250  		ctx.Inst.Kernel.Repo, ctx.Inst.Kernel.Tag, cf.Serialize(), ctx.Inst.Verbatim))
   251  	return osutil.WriteFile(outputFile, config)
   252  }
   253  
   254  func (ctx *Context) executeShell() error {
   255  	envRe := regexp.MustCompile("^[A-Z0-9_]+=")
   256  	for _, shell := range ctx.Inst.Shell {
   257  		if !ctx.Inst.Features.Match(shell.Constraints) {
   258  			continue
   259  		}
   260  		args := strings.Split(shell.Cmd, " ")
   261  		for i := 1; i < len(args); i++ {
   262  			args[i] = ctx.replaceVars(args[i])
   263  		}
   264  		if args[0] == "make" {
   265  			if err := ctx.Make(args[1:]...); err != nil {
   266  				return err
   267  			}
   268  			continue
   269  		}
   270  		env := os.Environ()
   271  		for len(args) > 1 {
   272  			if !envRe.MatchString(args[0]) {
   273  				break
   274  			}
   275  			env = append(env, args[0])
   276  			args = args[1:]
   277  		}
   278  		cmd := osutil.Command(args[0], args[1:]...)
   279  		cmd.Dir = ctx.SourceDir
   280  		cmd.Env = env
   281  		if _, err := osutil.Run(10*time.Minute, cmd); err != nil {
   282  			return err
   283  		}
   284  	}
   285  	return nil
   286  }
   287  
   288  func (ctx *Context) applyConfigs(cf *kconfig.ConfigFile) {
   289  	for _, cfg := range ctx.Inst.Configs {
   290  		if !ctx.Inst.Features.Match(cfg.Constraints) {
   291  			continue
   292  		}
   293  		if cfg.Value != kconfig.No {
   294  			// If this is a choice, first unset all other options.
   295  			// If we leave 2 choice options enabled, the last one will win.
   296  			// It can make sense to move this code to kconfig in some form,
   297  			// it's needed everywhere configs are changed.
   298  			if m := ctx.Kconf.Configs[cfg.Name]; m != nil && m.Parent.Kind == kconfig.MenuChoice {
   299  				for _, choice := range m.Parent.Elems {
   300  					cf.Unset(choice.Name)
   301  				}
   302  			}
   303  		}
   304  		cf.Set(cfg.Name, cfg.Value)
   305  	}
   306  }
   307  
   308  func (ctx *Context) verifyConfigs(cf *kconfig.ConfigFile) error {
   309  	errs := new(Errors)
   310  	for _, cfg := range ctx.Inst.Configs {
   311  		act := cf.Value(cfg.Name)
   312  		if act == cfg.Value || cfg.Optional || !ctx.Inst.Features.Match(cfg.Constraints) {
   313  			continue
   314  		}
   315  		if act == kconfig.No {
   316  			errs.push("%v:%v: %v is not present in the final config", cfg.File, cfg.Line, cfg.Name)
   317  		} else if cfg.Value == kconfig.No {
   318  			var selectedBy []string
   319  			for name := range ctx.Kconf.SelectedBy(cfg.Name) {
   320  				if cf.Value(name) == kconfig.Yes {
   321  					selectedBy = append(selectedBy, name)
   322  				}
   323  			}
   324  			selectedByStr := ""
   325  			if len(selectedBy) > 0 {
   326  				selectedByStr = fmt.Sprintf(", possibly selected by %q", selectedBy)
   327  			}
   328  			errs.push("%v:%v: %v is present in the final config%s", cfg.File, cfg.Line, cfg.Name, selectedByStr)
   329  		} else {
   330  			errs.push("%v:%v: %v does not match final config %v vs %v",
   331  				cfg.File, cfg.Line, cfg.Name, cfg.Value, act)
   332  		}
   333  	}
   334  	return errs.err()
   335  }
   336  
   337  func (ctx *Context) addUSBConfigs(cf *kconfig.ConfigFile) error {
   338  	prefix := ""
   339  	switch {
   340  	case ctx.Inst.Features[featAndroid]:
   341  		prefix = "android"
   342  	case ctx.Inst.Features[featChromeos]:
   343  		prefix = "chromeos"
   344  	}
   345  	distroConfig := filepath.Join(ctx.ConfigDir, "distros", prefix+"*")
   346  	// Some USB drivers don't depend on any USB related symbols, but rather on a generic symbol
   347  	// for some input subsystem (e.g. HID), so include it as well.
   348  	return ctx.addDependentConfigs(cf, []string{"USB_SUPPORT", "HID"}, distroConfig)
   349  }
   350  
   351  func (ctx *Context) addDependentConfigs(dst *kconfig.ConfigFile, include []string, configGlob string) error {
   352  	configFiles, err := filepath.Glob(configGlob)
   353  	if err != nil {
   354  		return err
   355  	}
   356  	includes := func(a []string, b map[string]bool) bool {
   357  		for _, x := range a {
   358  			if b[x] {
   359  				return true
   360  			}
   361  		}
   362  		return false
   363  	}
   364  	selected := make(map[string]bool)
   365  	for _, cfg := range ctx.Kconf.Configs {
   366  		deps := cfg.DependsOn()
   367  		if !includes(include, deps) {
   368  			continue
   369  		}
   370  		selected[cfg.Name] = true
   371  		for dep := range deps {
   372  			selected[dep] = true
   373  		}
   374  	}
   375  	dedup := make(map[string]bool)
   376  	for _, file := range configFiles {
   377  		cf, err := kconfig.ParseConfig(file)
   378  		if err != nil {
   379  			return err
   380  		}
   381  		for _, cfg := range cf.Configs {
   382  			if cfg.Value == kconfig.No || dedup[cfg.Name] || !selected[cfg.Name] {
   383  				continue
   384  			}
   385  			dedup[cfg.Name] = true
   386  			dst.Set(cfg.Name, cfg.Value)
   387  		}
   388  	}
   389  	return nil
   390  }
   391  
   392  func (ctx *Context) setTarget() error {
   393  	for _, target := range targets.List[targets.Linux] {
   394  		if ctx.Inst.Features[target.KernelArch] {
   395  			if ctx.Target != nil {
   396  				return fmt.Errorf("arch is set twice")
   397  			}
   398  			ctx.Target = targets.GetEx(targets.Linux, target.Arch, ctx.Inst.Features[featClang])
   399  		}
   400  	}
   401  	if ctx.Target == nil {
   402  		return fmt.Errorf("no arch feature")
   403  	}
   404  	return nil
   405  }
   406  
   407  func (ctx *Context) setReleaseFeatures() error {
   408  	tag := ctx.ReleaseTag
   409  	match := releaseRe.FindStringSubmatch(tag)
   410  	if match == nil {
   411  		return fmt.Errorf("bad release tag %q", tag)
   412  	}
   413  	major, err := strconv.ParseInt(match[1], 10, 32)
   414  	if err != nil {
   415  		return fmt.Errorf("bad release tag %q: %w", tag, err)
   416  	}
   417  	minor, err := strconv.ParseInt(match[2], 10, 32)
   418  	if err != nil {
   419  		return fmt.Errorf("bad release tag %q: %w", tag, err)
   420  	}
   421  	for ; major >= 2; major-- {
   422  		for ; minor >= 0; minor-- {
   423  			ctx.Inst.Features[fmt.Sprintf("v%v.%v", major, minor)] = true
   424  		}
   425  		minor = 99
   426  	}
   427  	return nil
   428  }
   429  
   430  var releaseRe = regexp.MustCompile(`^v([0-9]+)\.([0-9]+)(?:-rc([0-9]+))?(?:\.([0-9]+))?$`)
   431  
   432  func (ctx *Context) mrProper() error {
   433  	// Run 'make mrproper', otherwise out-of-tree build fails.
   434  	// However, it takes unreasonable amount of time,
   435  	// so first check few files and if they are missing hope for best.
   436  	files := []string{
   437  		".config",
   438  		"init/main.o",
   439  		"include/config",
   440  		"include/generated/compile.h",
   441  		"arch/" + ctx.Target.KernelArch + "/include/generated",
   442  	}
   443  	for _, file := range files {
   444  		if osutil.IsExist(filepath.Join(ctx.SourceDir, filepath.FromSlash(file))) {
   445  			goto clean
   446  		}
   447  	}
   448  	return nil
   449  clean:
   450  	buildDir := ctx.BuildDir
   451  	ctx.BuildDir = ctx.SourceDir
   452  	err := ctx.Make("mrproper")
   453  	ctx.BuildDir = buildDir
   454  	return err
   455  }
   456  
   457  func (ctx *Context) Make(args ...string) error {
   458  	compiler, linker := "", ""
   459  	if ctx.Inst.Compiler != "" {
   460  		compiler = ctx.replaceVars(ctx.Inst.Compiler)
   461  	}
   462  	if ctx.Inst.Linker != "" {
   463  		linker = ctx.replaceVars(ctx.Inst.Linker)
   464  	}
   465  	args = append(args, build.LinuxMakeArgs(
   466  		ctx.Target,
   467  		compiler,
   468  		linker,
   469  		"", // ccache
   470  		ctx.BuildDir,
   471  		runtime.NumCPU(),
   472  	)...)
   473  	_, err := osutil.RunCmd(10*time.Minute, ctx.SourceDir, "make", args...)
   474  	return err
   475  }
   476  
   477  func (ctx *Context) replaceVars(str string) string {
   478  	str = strings.ReplaceAll(str, "${SOURCEDIR}", ctx.SourceDir)
   479  	str = strings.ReplaceAll(str, "${BUILDDIR}", ctx.BuildDir)
   480  	str = strings.ReplaceAll(str, "${ARCH}", ctx.Target.KernelArch)
   481  	return str
   482  }
   483  
   484  func releaseTag(dir string) (string, error) {
   485  	data, err := os.ReadFile(filepath.Join(dir, "Makefile"))
   486  	if err != nil {
   487  		return "", err
   488  	}
   489  	return releaseTagImpl(data)
   490  }
   491  
   492  func releaseTagImpl(data []byte) (string, error) {
   493  	match := makefileReleaseRe.FindSubmatch(data)
   494  	if match == nil {
   495  		return "", fmt.Errorf("did not find VERSION/PATCHLEVEL in the kernel Makefile")
   496  	}
   497  	return fmt.Sprintf("v%s.%s", match[1], match[2]), nil
   498  }
   499  
   500  var makefileReleaseRe = regexp.MustCompile(`\nVERSION = ([0-9]+)(?:\n.*)*\nPATCHLEVEL = ([0-9]+)\n`)