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  }