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