github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/kconfig/kconfig.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  // Package kconfig implements parsing of the Linux kernel Kconfig and .config files
     5  // and provides some algorithms to work with these files. For Kconfig reference see:
     6  // https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
     7  package kconfig
     8  
     9  import (
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/google/syzkaller/sys/targets"
    17  )
    18  
    19  // KConfig represents a parsed Kconfig file (including includes).
    20  type KConfig struct {
    21  	Root    *Menu            // mainmenu
    22  	Configs map[string]*Menu // only config/menuconfig entries
    23  }
    24  
    25  // Menu represents a single hierarchical menu or config.
    26  type Menu struct {
    27  	Kind   MenuKind   // config/menu/choice/etc
    28  	Type   ConfigType // tristate/bool/string/etc
    29  	Name   string     // name without CONFIG_
    30  	Elems  []*Menu    // sub-elements for menus
    31  	Parent *Menu      // parent menu, non-nil for everythign except for mainmenu
    32  
    33  	kconf      *KConfig // back-link to the owning KConfig
    34  	prompts    []prompt
    35  	defaults   []defaultVal
    36  	dependsOn  expr
    37  	visibleIf  expr
    38  	deps       map[string]bool
    39  	depsOnce   sync.Once
    40  	selects    []string
    41  	selectedBy []string // filled in in setSelectedBy()
    42  }
    43  
    44  type prompt struct {
    45  	text string
    46  	cond expr
    47  }
    48  
    49  type defaultVal struct {
    50  	val  expr
    51  	cond expr
    52  }
    53  
    54  type (
    55  	MenuKind   int
    56  	ConfigType int
    57  )
    58  
    59  const (
    60  	_ MenuKind = iota
    61  	MenuConfig
    62  	MenuGroup
    63  	MenuChoice
    64  	MenuComment
    65  )
    66  const (
    67  	_ ConfigType = iota
    68  	TypeBool
    69  	TypeTristate
    70  	TypeString
    71  	TypeInt
    72  	TypeHex
    73  )
    74  
    75  // DependsOn returns all transitive configs this config depends on.
    76  func (m *Menu) DependsOn() map[string]bool {
    77  	m.depsOnce.Do(func() {
    78  		m.deps = make(map[string]bool)
    79  		if m.dependsOn != nil {
    80  			m.dependsOn.collectDeps(m.deps)
    81  		}
    82  		if m.visibleIf != nil {
    83  			m.visibleIf.collectDeps(m.deps)
    84  		}
    85  		var indirect []string
    86  		for cfg := range m.deps {
    87  			dep := m.kconf.Configs[cfg]
    88  			if dep == nil {
    89  				delete(m.deps, cfg)
    90  				continue
    91  			}
    92  			for cfg1 := range dep.DependsOn() {
    93  				indirect = append(indirect, cfg1)
    94  			}
    95  		}
    96  		for _, cfg := range indirect {
    97  			m.deps[cfg] = true
    98  		}
    99  	})
   100  	return m.deps
   101  }
   102  
   103  func (m *Menu) Prompt() string {
   104  	// TODO: check prompt conditions, some prompts may be not visible.
   105  	// If all prompts are not visible, then then menu if effectively disabled (at least for user).
   106  	for _, p := range m.prompts {
   107  		return p.text
   108  	}
   109  	return ""
   110  }
   111  
   112  type kconfigParser struct {
   113  	*parser
   114  	target    *targets.Target
   115  	includes  []*parser
   116  	stack     []*Menu
   117  	cur       *Menu
   118  	baseDir   string
   119  	helpIdent int
   120  }
   121  
   122  func Parse(target *targets.Target, file string) (*KConfig, error) {
   123  	data, err := os.ReadFile(file)
   124  	if err != nil {
   125  		return nil, fmt.Errorf("failed to open Kconfig file %v: %w", file, err)
   126  	}
   127  	return ParseData(target, data, file)
   128  }
   129  
   130  func ParseData(target *targets.Target, data []byte, file string) (*KConfig, error) {
   131  	kp := &kconfigParser{
   132  		parser:  newParser(data, file),
   133  		target:  target,
   134  		baseDir: filepath.Dir(file),
   135  	}
   136  	kp.parseFile()
   137  	if kp.err != nil {
   138  		return nil, kp.err
   139  	}
   140  	if len(kp.stack) == 0 {
   141  		return nil, fmt.Errorf("no mainmenu in config")
   142  	}
   143  	root := kp.stack[0]
   144  	kconf := &KConfig{
   145  		Root:    root,
   146  		Configs: make(map[string]*Menu),
   147  	}
   148  	kconf.walk(root, nil, nil)
   149  	kconf.setSelectedBy()
   150  	return kconf, nil
   151  }
   152  
   153  func (kconf *KConfig) walk(m *Menu, dependsOn, visibleIf expr) {
   154  	m.kconf = kconf
   155  	m.dependsOn = exprAnd(dependsOn, m.dependsOn)
   156  	m.visibleIf = exprAnd(visibleIf, m.visibleIf)
   157  	if m.Kind == MenuConfig {
   158  		kconf.Configs[m.Name] = m
   159  	}
   160  	for _, elem := range m.Elems {
   161  		kconf.walk(elem, m.dependsOn, m.visibleIf)
   162  	}
   163  }
   164  
   165  // NOTE: the function is ignoring the "if" part of select/imply.
   166  func (kconf *KConfig) setSelectedBy() {
   167  	for name, cfg := range kconf.Configs {
   168  		for _, selectedName := range cfg.selects {
   169  			selected := kconf.Configs[selectedName]
   170  			if selected == nil {
   171  				continue
   172  			}
   173  			selected.selectedBy = append(selected.selectedBy, name)
   174  		}
   175  	}
   176  }
   177  
   178  // NOTE: the function is ignoring the "if" part of select/imply.
   179  func (kconf *KConfig) SelectedBy(name string) map[string]bool {
   180  	ret := map[string]bool{}
   181  	toVisit := []string{name}
   182  	for len(toVisit) > 0 {
   183  		next := kconf.Configs[toVisit[len(toVisit)-1]]
   184  		toVisit = toVisit[:len(toVisit)-1]
   185  		if next == nil {
   186  			continue
   187  		}
   188  		for _, selectedBy := range next.selectedBy {
   189  			ret[selectedBy] = true
   190  			toVisit = append(toVisit, selectedBy)
   191  		}
   192  	}
   193  	return ret
   194  }
   195  
   196  func (kp *kconfigParser) parseFile() {
   197  	for kp.nextLine() {
   198  		kp.parseLine()
   199  		if kp.TryConsume("#") {
   200  			_ = kp.ConsumeLine()
   201  		}
   202  	}
   203  	kp.endCurrent()
   204  }
   205  
   206  func (kp *kconfigParser) parseLine() {
   207  	if kp.eol() {
   208  		return
   209  	}
   210  	if kp.helpIdent != 0 {
   211  		if kp.identLevel() >= kp.helpIdent {
   212  			_ = kp.ConsumeLine()
   213  			return
   214  		}
   215  		kp.helpIdent = 0
   216  	}
   217  	if kp.TryConsume("#") {
   218  		_ = kp.ConsumeLine()
   219  		return
   220  	}
   221  	if kp.TryConsume("$") {
   222  		_ = kp.Shell()
   223  		return
   224  	}
   225  	ident := kp.Ident()
   226  	if kp.TryConsume("=") || kp.TryConsume(":=") {
   227  		// Macro definition, see:
   228  		// https://www.kernel.org/doc/html/latest/kbuild/kconfig-macro-language.html
   229  		// We don't use this for anything now.
   230  		kp.ConsumeLine()
   231  		return
   232  	}
   233  	kp.parseMenu(ident)
   234  }
   235  
   236  func (kp *kconfigParser) parseMenu(cmd string) {
   237  	switch cmd {
   238  	case "source":
   239  		file, ok := kp.TryQuotedString()
   240  		if !ok {
   241  			file = kp.ConsumeLine()
   242  		}
   243  		kp.includeSource(file)
   244  	case "mainmenu":
   245  		kp.pushCurrent(&Menu{
   246  			Kind:    MenuConfig,
   247  			prompts: []prompt{{text: kp.QuotedString()}},
   248  		})
   249  	case "comment":
   250  		kp.newCurrent(&Menu{
   251  			Kind:    MenuComment,
   252  			prompts: []prompt{{text: kp.QuotedString()}},
   253  		})
   254  	case "menu":
   255  		kp.pushCurrent(&Menu{
   256  			Kind:    MenuGroup,
   257  			prompts: []prompt{{text: kp.QuotedString()}},
   258  		})
   259  	case "if":
   260  		kp.pushCurrent(&Menu{
   261  			Kind:      MenuGroup,
   262  			visibleIf: kp.parseExpr(),
   263  		})
   264  	case "choice":
   265  		kp.pushCurrent(&Menu{
   266  			Kind: MenuChoice,
   267  		})
   268  	case "endmenu", "endif", "endchoice":
   269  		kp.popCurrent()
   270  	case "config", "menuconfig":
   271  		kp.newCurrent(&Menu{
   272  			Kind: MenuConfig,
   273  			Name: kp.Ident(),
   274  		})
   275  	default:
   276  		kp.parseConfigType(cmd)
   277  	}
   278  }
   279  
   280  func (kp *kconfigParser) parseConfigType(typ string) {
   281  	cur := kp.current()
   282  	switch typ {
   283  	case "tristate":
   284  		cur.Type = TypeTristate
   285  		kp.tryParsePrompt()
   286  	case "def_tristate":
   287  		cur.Type = TypeTristate
   288  		kp.parseDefaultValue()
   289  	case "bool":
   290  		cur.Type = TypeBool
   291  		kp.tryParsePrompt()
   292  	case "def_bool":
   293  		cur.Type = TypeBool
   294  		kp.parseDefaultValue()
   295  	case "int":
   296  		cur.Type = TypeInt
   297  		kp.tryParsePrompt()
   298  	case "def_int":
   299  		cur.Type = TypeInt
   300  		kp.parseDefaultValue()
   301  	case "hex":
   302  		cur.Type = TypeHex
   303  		kp.tryParsePrompt()
   304  	case "def_hex":
   305  		cur.Type = TypeHex
   306  		kp.parseDefaultValue()
   307  	case "string":
   308  		cur.Type = TypeString
   309  		kp.tryParsePrompt()
   310  	case "def_string":
   311  		cur.Type = TypeString
   312  		kp.parseDefaultValue()
   313  	default:
   314  		kp.parseProperty(typ)
   315  	}
   316  }
   317  
   318  func (kp *kconfigParser) parseProperty(prop string) {
   319  	cur := kp.current()
   320  	switch prop {
   321  	case "prompt":
   322  		kp.tryParsePrompt()
   323  	case "depends":
   324  		kp.MustConsume("on")
   325  		cur.dependsOn = exprAnd(cur.dependsOn, kp.parseExpr())
   326  	case "visible":
   327  		kp.MustConsume("if")
   328  		cur.visibleIf = exprAnd(cur.visibleIf, kp.parseExpr())
   329  	case "select", "imply":
   330  		name := kp.Ident()
   331  		cur.selects = append(cur.selects, name)
   332  		if kp.TryConsume("if") {
   333  			_ = kp.parseExpr()
   334  		}
   335  	case "option":
   336  		// It can be 'option foo', or 'option bar="BAZ"'.
   337  		kp.ConsumeLine()
   338  	case "modules":
   339  	case "optional":
   340  	// transitional is used for configs backward compatibility.
   341  	// We can ignore them. After such configs are removed from the kernel, we'll see kconf errors.
   342  	// https://www.phoronix.com/news/Linux-6.18-Transitional
   343  	case "transitional":
   344  	case "default":
   345  		kp.parseDefaultValue()
   346  	case "range":
   347  		_, _ = kp.parseExpr(), kp.parseExpr() // from, to
   348  		if kp.TryConsume("if") {
   349  			_ = kp.parseExpr()
   350  		}
   351  	case "help", "---help---":
   352  		// Help rules are tricky: end of help is identified by smaller indentation level
   353  		// as would be rendered on a terminal with 8-column tabs setup, minus empty lines.
   354  		for kp.nextLine() {
   355  			if kp.eol() {
   356  				continue
   357  			}
   358  			kp.helpIdent = kp.identLevel()
   359  			kp.ConsumeLine()
   360  			break
   361  		}
   362  	default:
   363  		kp.failf("unknown line")
   364  	}
   365  }
   366  
   367  func (kp *kconfigParser) includeSource(file string) {
   368  	kp.newCurrent(nil)
   369  	file = kp.expandString(file)
   370  	file = filepath.Join(kp.baseDir, file)
   371  	data, err := os.ReadFile(file)
   372  	if err != nil {
   373  		kp.failf("%v", err)
   374  		return
   375  	}
   376  	kp.includes = append(kp.includes, kp.parser)
   377  	kp.parser = newParser(data, file)
   378  	kp.parseFile()
   379  	err = kp.err
   380  	kp.parser = kp.includes[len(kp.includes)-1]
   381  	kp.includes = kp.includes[:len(kp.includes)-1]
   382  	if kp.err == nil {
   383  		kp.err = err
   384  	}
   385  }
   386  
   387  func (kp *kconfigParser) pushCurrent(m *Menu) {
   388  	kp.endCurrent()
   389  	kp.cur = m
   390  	kp.stack = append(kp.stack, m)
   391  }
   392  
   393  func (kp *kconfigParser) popCurrent() {
   394  	kp.endCurrent()
   395  	if len(kp.stack) < 2 {
   396  		kp.failf("unbalanced endmenu")
   397  		return
   398  	}
   399  	last := kp.stack[len(kp.stack)-1]
   400  	kp.stack = kp.stack[:len(kp.stack)-1]
   401  	top := kp.stack[len(kp.stack)-1]
   402  	last.Parent = top
   403  	top.Elems = append(top.Elems, last)
   404  }
   405  
   406  func (kp *kconfigParser) newCurrent(m *Menu) {
   407  	kp.endCurrent()
   408  	kp.cur = m
   409  }
   410  
   411  func (kp *kconfigParser) current() *Menu {
   412  	if kp.cur == nil {
   413  		kp.failf("config property outside of config")
   414  		return &Menu{}
   415  	}
   416  	return kp.cur
   417  }
   418  
   419  func (kp *kconfigParser) endCurrent() {
   420  	if kp.cur == nil {
   421  		return
   422  	}
   423  	if len(kp.stack) == 0 {
   424  		kp.failf("unbalanced endmenu")
   425  		return
   426  	}
   427  	top := kp.stack[len(kp.stack)-1]
   428  	if top != kp.cur {
   429  		kp.cur.Parent = top
   430  		top.Elems = append(top.Elems, kp.cur)
   431  	}
   432  	kp.cur = nil
   433  }
   434  
   435  func (kp *kconfigParser) tryParsePrompt() {
   436  	if str, ok := kp.TryQuotedString(); ok {
   437  		prompt := prompt{
   438  			text: str,
   439  		}
   440  		if kp.TryConsume("if") {
   441  			prompt.cond = kp.parseExpr()
   442  		}
   443  		kp.current().prompts = append(kp.current().prompts, prompt)
   444  	}
   445  }
   446  
   447  func (kp *kconfigParser) parseDefaultValue() {
   448  	def := defaultVal{val: kp.parseExpr()}
   449  	if kp.TryConsume("if") {
   450  		def.cond = kp.parseExpr()
   451  	}
   452  	kp.current().defaults = append(kp.current().defaults, def)
   453  }
   454  
   455  func (kp *kconfigParser) expandString(str string) string {
   456  	str = strings.ReplaceAll(str, "$(SRCARCH)", kp.target.KernelHeaderArch)
   457  	str = strings.ReplaceAll(str, "$SRCARCH", kp.target.KernelHeaderArch)
   458  	str = strings.ReplaceAll(str, "$(KCONFIG_EXT_PREFIX)", "")
   459  	str = strings.ReplaceAll(str, "$(MALI_KCONFIG_EXT_PREFIX)", "") // ChromeOS.
   460  	return str
   461  }