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  }