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  }