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