github.hscsec.cn/u-root/u-root@v7.0.0+incompatible/pkg/boot/grub/grub.go (about) 1 // Copyright 2017-2020 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 grub implements a grub config file parser. 6 // 7 // See the grub manual https://www.gnu.org/software/grub/manual/grub/ for 8 // a reference of the configuration format 9 // In particular the following pages: 10 // - https://www.gnu.org/software/grub/manual/grub/html_node/Shell_002dlike-scripting.html 11 // - https://www.gnu.org/software/grub/manual/grub/html_node/Commands.html 12 // 13 // Currently, only the linux[16|efi], initrd[16|efi], menuentry and set 14 // directives are partially supported. 15 package grub 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "log" 22 "net/url" 23 "path/filepath" 24 "strconv" 25 "strings" 26 27 "github.com/u-root/u-root/pkg/boot" 28 "github.com/u-root/u-root/pkg/boot/multiboot" 29 "github.com/u-root/u-root/pkg/curl" 30 "github.com/u-root/u-root/pkg/shlex" 31 "github.com/u-root/u-root/pkg/uio" 32 ) 33 34 var probeGrubFiles = []string{ 35 "boot/grub/grub.cfg", 36 "grub/grub.cfg", 37 "grub2/grub.cfg", 38 "boot/grub2/grub.cfg", 39 } 40 41 // ParseLocalConfig looks for a GRUB config in the disk partition mounted at 42 // diskDir and parses out OSes to boot. 43 // 44 // This... is at best crude, at worst totally wrong, since we fundamentally 45 // assume that the kernels we boot are only on this one partition. But so is 46 // this whole parser. 47 func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) { 48 wd := &url.URL{ 49 Scheme: "file", 50 Path: diskDir, 51 } 52 53 // This is a hack. GRUB should stop caring about URLs at least in the 54 // way we use them today, because GRUB has additional path encoding 55 // methods. Sorry. 56 // 57 // Normally, stuff like this will be in EFI/BOOT/grub.cfg, but some 58 // distro's have their own directory in this EFI namespace. Just check 59 // 'em all. 60 files, err := filepath.Glob(filepath.Join(diskDir, "EFI", "*", "grub.cfg")) 61 if err != nil { 62 log.Printf("[grub] Could not glob for %s/EFI/*/grub.cfg: %v", diskDir, err) 63 } 64 var relNames []string 65 for _, file := range files { 66 base, err := filepath.Rel(diskDir, file) 67 if err == nil { 68 relNames = append(relNames, base) 69 } 70 } 71 72 for _, relname := range append(relNames, probeGrubFiles...) { 73 c, err := ParseConfigFile(ctx, curl.DefaultSchemes, relname, wd) 74 if curl.IsURLError(err) { 75 continue 76 } 77 return c, err 78 } 79 return nil, fmt.Errorf("no valid grub config found") 80 } 81 82 // ParseConfigFile parses a grub configuration as specified in 83 // https://www.gnu.org/software/grub/manual/grub/ 84 // 85 // Currently, only the linux[16|efi], initrd[16|efi], menuentry and set 86 // directives are partially supported. 87 // 88 // `wd` is the default scheme, host, and path for any files named as a 89 // relative path - e.g. kernel, include, and initramfs paths are requested 90 // relative to the wd. 91 func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, wd *url.URL) ([]boot.OSImage, error) { 92 p := newParser(wd, s) 93 if err := p.appendFile(ctx, configFile); err != nil { 94 return nil, err 95 } 96 97 // Don't add entries twice. 98 // 99 // Multiple labels can refer to the same image, so we have to dedup by pointer. 100 seenLinux := make(map[*boot.LinuxImage]struct{}) 101 seenMB := make(map[*boot.MultibootImage]struct{}) 102 103 if len(p.defaultEntry) > 0 { 104 p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...) 105 } 106 107 var images []boot.OSImage 108 for _, label := range p.labelOrder { 109 if img, ok := p.linuxEntries[label]; ok { 110 if _, ok := seenLinux[img]; !ok { 111 images = append(images, img) 112 seenLinux[img] = struct{}{} 113 } 114 } 115 116 if img, ok := p.mbEntries[label]; ok { 117 if _, ok := seenMB[img]; !ok { 118 images = append(images, img) 119 seenMB[img] = struct{}{} 120 } 121 } 122 } 123 return images, nil 124 } 125 126 type parser struct { 127 linuxEntries map[string]*boot.LinuxImage 128 mbEntries map[string]*boot.MultibootImage 129 130 labelOrder []string 131 defaultEntry string 132 133 W io.Writer 134 135 // parser internals. 136 numEntry int 137 138 // curEntry is the current entry number as a string. 139 curEntry string 140 141 // curLabel is the last parsed label from a "menuentry". 142 curLabel string 143 144 wd *url.URL 145 schemes curl.Schemes 146 } 147 148 // newParser returns a new grub parser using working directory `wd` 149 // and schemes `s`. 150 // 151 // If a path encountered in a configuration file is relative instead of a full 152 // URL, `wd` is used as the "working directory" of that relative path; the 153 // resulting URL is roughly `wd.String()/path`. 154 // 155 // `s` is used to get files referred to by URLs. 156 func newParser(wd *url.URL, s curl.Schemes) *parser { 157 return &parser{ 158 linuxEntries: make(map[string]*boot.LinuxImage), 159 mbEntries: make(map[string]*boot.MultibootImage), 160 wd: wd, 161 schemes: s, 162 } 163 } 164 165 func parseURL(surl string, wd *url.URL) (*url.URL, error) { 166 u, err := url.Parse(surl) 167 if err != nil { 168 return nil, fmt.Errorf("could not parse URL %q: %v", surl, err) 169 } 170 171 if len(u.Scheme) == 0 { 172 u.Scheme = wd.Scheme 173 174 if len(u.Host) == 0 { 175 // If this is not there, it was likely just a path. 176 u.Host = wd.Host 177 u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path)) 178 } 179 } 180 return u, nil 181 } 182 183 // getFile parses `url` relative to the config's working directory and returns 184 // an io.Reader for the requested url. 185 // 186 // If url is just a relative path and not a full URL, c.wd is used as the 187 // "working directory" of that relative path; the resulting URL is roughly 188 // path.Join(wd.String(), url). 189 func (c *parser) getFile(url string) (io.ReaderAt, error) { 190 u, err := parseURL(url, c.wd) 191 if err != nil { 192 return nil, err 193 } 194 195 return c.schemes.LazyFetch(u) 196 } 197 198 // appendFile parses the config file downloaded from `url` and adds it to `c`. 199 func (c *parser) appendFile(ctx context.Context, url string) error { 200 u, err := parseURL(url, c.wd) 201 if err != nil { 202 return err 203 } 204 205 r, err := c.schemes.Fetch(ctx, u) 206 if err != nil { 207 return err 208 } 209 210 config, err := uio.ReadAll(r) 211 if err != nil { 212 return err 213 } 214 if len(config) > 500 { 215 // Avoid flooding the console on real systems 216 // TODO: do we want to pass a verbose flag or a logger? 217 log.Printf("[grub] Got config file %s", r) 218 } else { 219 log.Printf("[grub] Got config file %s:\n%s\n", r, string(config)) 220 } 221 return c.append(ctx, string(config)) 222 } 223 224 // CmdlineQuote quotes the command line as grub-core/lib/cmdline.c does 225 func cmdlineQuote(args []string) string { 226 q := make([]string, len(args)) 227 for i, s := range args { 228 s = strings.Replace(s, `\`, `\\`, -1) 229 s = strings.Replace(s, `'`, `\'`, -1) 230 s = strings.Replace(s, `"`, `\"`, -1) 231 if strings.ContainsRune(s, ' ') { 232 s = `"` + s + `"` 233 } 234 q[i] = s 235 } 236 return strings.Join(q, " ") 237 } 238 239 // append parses `config` and adds the respective configuration to `c`. 240 // 241 // NOTE: This parser has outlived its usefulness already, given that it doesn't 242 // even understand the {} scoping in GRUB. But let's get the tests to pass, and 243 // then we can do a rewrite. 244 func (c *parser) append(ctx context.Context, config string) error { 245 // Here's a shitty parser. 246 for _, line := range strings.Split(config, "\n") { 247 kv := shlex.Argv(line) 248 if len(kv) < 1 { 249 continue 250 } 251 directive := strings.ToLower(kv[0]) 252 // Used by tests (allow no parameters here) 253 if c.W != nil && directive == "echo" { 254 fmt.Fprintf(c.W, "echo:%#v\n", kv[1:]) 255 } 256 257 if len(kv) <= 1 { 258 continue 259 } 260 arg := kv[1] 261 262 switch directive { 263 case "set": 264 vals := strings.SplitN(arg, "=", 2) 265 if len(vals) == 2 { 266 //TODO handle vars? bootVars[vals[0]] = vals[1] 267 //log.Printf("grubvar: %s=%s", vals[0], vals[1]) 268 if vals[0] == "default" { 269 c.defaultEntry = vals[1] 270 } 271 } 272 273 case "configfile": 274 // TODO test that 275 if err := c.appendFile(ctx, arg); err != nil { 276 return err 277 } 278 279 case "menuentry": 280 c.curEntry = strconv.Itoa(c.numEntry) 281 c.curLabel = arg 282 c.numEntry++ 283 c.labelOrder = append(c.labelOrder, c.curEntry, c.curLabel) 284 285 case "linux", "linux16", "linuxefi": 286 k, err := c.getFile(arg) 287 if err != nil { 288 return err 289 } 290 // from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry 291 entry := &boot.LinuxImage{ 292 Name: c.curLabel, 293 Kernel: k, 294 Cmdline: cmdlineQuote(kv[2:]), 295 } 296 c.linuxEntries[c.curEntry] = entry 297 c.linuxEntries[c.curLabel] = entry 298 299 case "initrd", "initrd16", "initrdefi": 300 if e, ok := c.linuxEntries[c.curEntry]; ok { 301 i, err := c.getFile(arg) 302 if err != nil { 303 return err 304 } 305 e.Initrd = i 306 } 307 308 case "multiboot": 309 // TODO handle --quirk-* arguments ? (change parsing) 310 k, err := c.getFile(arg) 311 if err != nil { 312 return err 313 } 314 // from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry 315 entry := &boot.MultibootImage{ 316 Name: c.curLabel, 317 Kernel: k, 318 Cmdline: cmdlineQuote(kv[2:]), 319 } 320 c.mbEntries[c.curEntry] = entry 321 c.mbEntries[c.curLabel] = entry 322 323 case "module": 324 // TODO handle --nounzip arguments ? (change parsing) 325 if e, ok := c.mbEntries[c.curEntry]; ok { 326 // The only allowed arg 327 cmdline := kv[1:] 328 if arg == "--nounzip" { 329 arg = kv[2] 330 cmdline = kv[2:] 331 } 332 333 m, err := c.getFile(arg) 334 if err != nil { 335 return err 336 } 337 // TODO: Lasy tryGzipFilter(m) 338 mod := multiboot.Module{ 339 Module: m, 340 Cmdline: cmdlineQuote(cmdline), 341 } 342 e.Modules = append(e.Modules, mod) 343 } 344 } 345 } 346 return nil 347 348 }