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 }