github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/cmds/boot/localboot/grub.go (about) 1 // Copyright 2017-2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "encoding/hex" 9 "log" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14 "unicode" 15 16 "golang.org/x/text/transform" 17 "golang.org/x/text/unicode/norm" 18 19 "github.com/mvdan/u-root-coreutils/pkg/boot/jsonboot" 20 "github.com/mvdan/u-root-coreutils/pkg/mount/block" 21 ) 22 23 // List of directories where to recursively look for grub config files. The root dorectory 24 // of each mountpoint, these folders inside the mountpoint and all subfolders 25 // of these folders are searched 26 var ( 27 GrubSearchDirectories = []string{ 28 "boot", 29 "EFI", 30 "efi", 31 "grub", 32 "grub2", 33 } 34 ) 35 36 // Limits rekursive search of grub files. It is the maximum directory depth 37 // that is searched through. Since on efi partitions grub files reside usually 38 // at /boot/efi/EFI/distro/ , 4 might be a good choice. 39 const searchDepth = 4 40 41 type grubVersion int 42 43 var ( 44 grubV1 grubVersion = 1 45 grubV2 grubVersion = 2 46 ) 47 48 func isGrubSearchDir(dirname string) bool { 49 for _, dir := range GrubSearchDirectories { 50 if dirname == dir { 51 return true 52 } 53 } 54 return false 55 } 56 57 // ParseGrubCfg parses the content of a grub.cfg and returns a list of 58 // BootConfig structures, one for each menuentry, in the same order as they 59 // appear in grub.cfg. All opened kernel and initrd files are relative to 60 // basedir. 61 func ParseGrubCfg(ver grubVersion, devices block.BlockDevices, grubcfg string, basedir string) []jsonboot.BootConfig { 62 // This parser sucks. It's not even a parser, it just looks for lines 63 // starting with menuentry, linux or initrd. 64 // TODO use a parser, e.g. https://github.com/alecthomas/participle 65 if ver != grubV1 && ver != grubV2 { 66 log.Printf("Warning: invalid GRUB version: %d", ver) 67 return nil 68 } 69 kernelBasedir := basedir 70 bootconfigs := make([]jsonboot.BootConfig, 0) 71 inMenuEntry := false 72 var cfg *jsonboot.BootConfig 73 for _, line := range strings.Split(grubcfg, "\n") { 74 // remove all leading spaces as they are not relevant for the config 75 // line 76 line = strings.TrimLeft(line, " ") 77 sline := strings.Fields(line) 78 if len(sline) == 0 { 79 continue 80 } 81 if sline[0] == "menuentry" { 82 // if a "menuentry", start a new boot config 83 if cfg != nil { 84 // save the previous boot config, if any 85 if cfg.IsValid() { 86 // only consider valid boot configs, i.e. the ones that have 87 // both kernel and initramfs 88 bootconfigs = append(bootconfigs, *cfg) 89 } 90 // reset kernelBaseDir 91 kernelBasedir = basedir 92 } 93 inMenuEntry = true 94 cfg = new(jsonboot.BootConfig) 95 name := "" 96 if len(sline) > 1 { 97 name = strings.Join(sline[1:], " ") 98 name = unquoteGrubString(name) 99 name = strings.Split(name, "--")[0] 100 } 101 cfg.Name = name 102 } else if inMenuEntry { 103 // check if location of kernel is at an other partition 104 // see https://www.gnu.org/software/grub/manual/grub/html_node/search.html 105 if sline[0] == "search" { 106 for _, str1 := range sline { 107 if str1 == "--set=root" { 108 log.Printf("Kernel seems to be on an other partitioin then the grub.cfg file") 109 for _, str2 := range sline { 110 if isValidFsUUID(str2) { 111 kernelFsUUID := str2 112 log.Printf("fs-uuid: %s", kernelFsUUID) 113 partitions := devices.FilterFSUUID(kernelFsUUID) 114 if len(partitions) == 0 { 115 log.Printf("WARNING: No partition found with filesystem UUID:'%s' to load kernel from!", kernelFsUUID) // TODO throw error ? 116 continue 117 } 118 if len(partitions) > 1 { 119 log.Printf("WARNING: more than one partition found with the given filesystem UUID. Using the first one") 120 } 121 dev := partitions[0] 122 kernelBasedir = path.Dir(kernelBasedir) 123 kernelBasedir = path.Join(kernelBasedir, dev.Name) 124 log.Printf("Kernel is on: %s", dev.Name) 125 } 126 } 127 } 128 } 129 } 130 // otherwise look for kernel and initramfs configuration 131 if len(sline) < 2 { 132 // surely not a valid linux or initrd directive, skip it 133 continue 134 } 135 if sline[0] == "linux" || sline[0] == "linux16" || sline[0] == "linuxefi" { 136 kernel := sline[1] 137 cmdline := strings.Join(sline[2:], " ") 138 cmdline = unquoteGrubString(cmdline) 139 cfg.Kernel = path.Join(kernelBasedir, kernel) 140 cfg.KernelArgs = cmdline 141 } else if sline[0] == "initrd" || sline[0] == "initrd16" || sline[0] == "initrdefi" { 142 initrd := sline[1] 143 cfg.Initramfs = path.Join(kernelBasedir, initrd) 144 } else if sline[0] == "multiboot" || sline[0] == "multiboot2" { 145 multiboot := sline[1] 146 cmdline := strings.Join(sline[2:], " ") 147 cmdline = unquoteGrubString(cmdline) 148 cfg.Multiboot = path.Join(kernelBasedir, multiboot) 149 cfg.MultibootArgs = cmdline 150 } else if sline[0] == "module" || sline[0] == "module2" { 151 module := sline[1] 152 cmdline := strings.Join(sline[2:], " ") 153 cmdline = unquoteGrubString(cmdline) 154 module = path.Join(kernelBasedir, module) 155 if cmdline != "" { 156 module = module + " " + cmdline 157 } 158 cfg.Modules = append(cfg.Modules, module) 159 } 160 } 161 } 162 163 // append last kernel config if it wasn't already 164 if inMenuEntry && cfg.IsValid() { 165 bootconfigs = append(bootconfigs, *cfg) 166 } 167 return bootconfigs 168 } 169 170 func isValidFsUUID(uuid string) bool { 171 for _, h := range strings.Split(uuid, "-") { 172 if _, err := hex.DecodeString(h); err != nil { 173 return false 174 } 175 } 176 return true 177 } 178 179 func unquoteGrubString(text string) string { 180 // unquote the string to prevent special characters used by GRUB 181 // from being passed thru kexec 182 // https://www.gnu.org/software/grub/manual/grub/grub.html#Quoting 183 // TODO unquote everything, not just \$ 184 return strings.Replace(text, `\$`, "$", -1) 185 } 186 187 func isMn(r rune) bool { 188 return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks 189 } 190 191 // ScanGrubConfigs looks for grub2 and grub legacy config files in the known 192 // locations and returns a list of boot configurations. 193 func ScanGrubConfigs(devices block.BlockDevices, basedir string) []jsonboot.BootConfig { 194 bootconfigs := make([]jsonboot.BootConfig, 0) 195 err := filepath.Walk(basedir, func(currentPath string, info os.FileInfo, err error) error { 196 if err != nil { 197 return err 198 } 199 t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) 200 currentPath, _, _ = transform.String(t, currentPath) 201 if info.IsDir() { 202 if path.Dir(currentPath) == basedir && !isGrubSearchDir(path.Base(currentPath)) { 203 debug("Skip %s: not significant", currentPath) 204 // skip irrelevant toplevel directories 205 return filepath.SkipDir 206 } 207 p, err := filepath.Rel(basedir, currentPath) 208 if err != nil { 209 return err 210 } 211 depth := len(strings.Split(p, string(os.PathSeparator))) 212 if depth > searchDepth { 213 debug("Skip %s, depth limit", currentPath) 214 // skip 215 return filepath.SkipDir 216 } 217 debug("Step into %s", currentPath) 218 // continue 219 return nil 220 } 221 cfgname := info.Name() 222 var ver grubVersion 223 switch cfgname { 224 case "grub.cfg": 225 ver = grubV1 226 case "grub2.cfg": 227 ver = grubV2 228 default: 229 return nil 230 } 231 log.Printf("Parsing %s", currentPath) 232 data, err := os.ReadFile(currentPath) 233 if err != nil { 234 return err 235 } 236 cfgs := ParseGrubCfg(ver, devices, string(data), basedir) 237 bootconfigs = append(bootconfigs, cfgs...) 238 return nil 239 }) 240 if err != nil { 241 log.Printf("filepath.Walk error: %v", err) 242 } 243 return bootconfigs 244 }