github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/boot/bls/bls.go (about)

     1  // Copyright 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 bls parses systemd Boot Loader Spec config files.
     6  //
     7  // See spec at https://systemd.io/BOOT_LOADER_SPECIFICATION. Only Type #1 BLS
     8  // entries are supported at the moment, while Type #2 EFI entries are left
     9  // unimplemented awaiting EFI boot support in u-root/LinuxBoot.
    10  //
    11  // This package also supports the systemd-boot loader.conf as described in
    12  // https://www.freedesktop.org/software/systemd/man/loader.conf.html. Only the
    13  // "default" keyword is implemented.
    14  package bls
    15  
    16  import (
    17  	"bufio"
    18  	"fmt"
    19  	"log"
    20  	"os"
    21  	"path/filepath"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/mvdan/u-root-coreutils/pkg/boot"
    27  	"github.com/mvdan/u-root-coreutils/pkg/ulog"
    28  )
    29  
    30  const (
    31  	blsEntriesDir  = "loader/entries"
    32  	blsEntriesDir2 = "boot/loader/entries"
    33  	// Set a higher default rank for BLS. It should be booted prior to the
    34  	// other local images.
    35  	blsDefaultRank = 1
    36  )
    37  
    38  func cutConf(s string) string {
    39  	if strings.HasSuffix(s, ".conf") {
    40  		return s[:len(s)-6]
    41  	}
    42  	return s
    43  }
    44  
    45  // ScanBLSEntries scans the filesystem root for valid BLS entries.
    46  // This function skips over invalid or unreadable entries in an effort
    47  // to return everything that is bootable. map variables is the parsed result
    48  // from Grub parser that should be used by BLS parser, pass nil if there's none.
    49  func ScanBLSEntries(log ulog.Logger, fsRoot string, variables map[string]string) ([]boot.OSImage, error) {
    50  	entriesDir := filepath.Join(fsRoot, blsEntriesDir)
    51  
    52  	files, err := filepath.Glob(filepath.Join(entriesDir, "*.conf"))
    53  	if err != nil || len(files) == 0 {
    54  		// Try blsEntriesDir2
    55  		entriesDir = filepath.Join(fsRoot, blsEntriesDir2)
    56  		files, err = filepath.Glob(filepath.Join(entriesDir, "*.conf"))
    57  		if err != nil || len(files) == 0 {
    58  			return nil, fmt.Errorf("no BootLoaderSpec entries found: %w", err)
    59  		}
    60  	}
    61  
    62  	// loader.conf is not in the real spec; it's an implementation detail
    63  	// of systemd-boot. It is specified in
    64  	// https://www.freedesktop.org/software/systemd/man/loader.conf.html
    65  	loaderConf, err := parseConf(filepath.Join(fsRoot, "loader", "loader.conf"))
    66  	if err != nil {
    67  		// loader.conf is optional.
    68  		loaderConf = make(map[string]string)
    69  	}
    70  
    71  	// TODO: Rank entries by version or machine-id attribute as suggested
    72  	// in the spec (but not mandated, surprisingly).
    73  	imgs := make(map[string]boot.OSImage)
    74  	for _, f := range files {
    75  		identifier := cutConf(filepath.Base(f))
    76  
    77  		img, err := parseBLSEntry(f, fsRoot, variables)
    78  		if err != nil {
    79  			log.Printf("BootLoaderSpec skipping entry %s: %v", f, err)
    80  			continue
    81  		}
    82  		imgs[identifier] = img
    83  	}
    84  
    85  	return sortImages(loaderConf, imgs), nil
    86  }
    87  
    88  func sortImages(loaderConf map[string]string, imgs map[string]boot.OSImage) []boot.OSImage {
    89  	// rankedImages = sort(default-images) + sort(remaining images)
    90  	var rankedImages []boot.OSImage
    91  
    92  	pattern, ok := loaderConf["default"]
    93  	if !ok {
    94  		// All images are default.
    95  		pattern = "*"
    96  	}
    97  
    98  	var defaultIdents []string
    99  	var otherIdents []string
   100  
   101  	// Find default and non-default identifiers.
   102  	for ident := range imgs {
   103  		ok, err := filepath.Match(pattern, ident)
   104  		if err != nil && ok {
   105  			defaultIdents = append(defaultIdents, ident)
   106  		} else {
   107  			otherIdents = append(otherIdents, ident)
   108  		}
   109  	}
   110  
   111  	// Sort them in the order we want them.
   112  	sort.Sort(sort.Reverse(sort.StringSlice(defaultIdents)))
   113  	sort.Sort(sort.Reverse(sort.StringSlice(otherIdents)))
   114  
   115  	// Add images to rankedImages in that sorted order, defaults first.
   116  	for _, ident := range defaultIdents {
   117  		rankedImages = append(rankedImages, imgs[ident])
   118  	}
   119  	for _, ident := range otherIdents {
   120  		rankedImages = append(rankedImages, imgs[ident])
   121  	}
   122  	return rankedImages
   123  }
   124  
   125  func parseConf(entryPath string) (map[string]string, error) {
   126  	f, err := os.Open(entryPath)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	defer f.Close()
   131  
   132  	vals := make(map[string]string)
   133  
   134  	scanner := bufio.NewScanner(f)
   135  	for scanner.Scan() {
   136  		line := scanner.Text()
   137  		if strings.HasPrefix(line, "#") {
   138  			continue
   139  		}
   140  		line = strings.TrimSpace(line)
   141  
   142  		sline := strings.SplitN(line, " ", 2)
   143  		if len(sline) != 2 {
   144  			continue
   145  		}
   146  		vals[sline[0]] = strings.TrimSpace(sline[1])
   147  	}
   148  	return vals, nil
   149  }
   150  
   151  // The spec says "$BOOT/loader/ is the directory containing all files needed
   152  // for Type #1 entries", but that's bullshit. Relative file names are indeed in
   153  // the $BOOT/loader/ directory, but absolute path names are in $BOOT, as
   154  // evidenced by the entries that kernel-install installs on Fedora 32.
   155  func filePath(fsRoot, value string) string {
   156  	if !filepath.IsAbs(value) {
   157  		return filepath.Join(fsRoot, "loader", value)
   158  	}
   159  	return filepath.Join(fsRoot, value)
   160  }
   161  
   162  func getGrubvalue(variables map[string]string, key string) (string, error) {
   163  	if variables == nil {
   164  		// Only return error for nil variables map.
   165  		return "", fmt.Errorf("variables map is nil")
   166  	}
   167  	if val, ok := variables[key]; ok && len(val) > 0 {
   168  		return val, nil
   169  	}
   170  	return "", nil
   171  }
   172  
   173  func parseLinuxImage(vals map[string]string, fsRoot string, variables map[string]string) (boot.OSImage, error) {
   174  	linux := &boot.LinuxImage{}
   175  
   176  	var cmdlines []string
   177  	var tokens []string
   178  	var value string
   179  	for key, val := range vals {
   180  		switch key {
   181  		case "linux":
   182  			f, err := os.Open(filePath(fsRoot, val))
   183  			if err != nil {
   184  				return nil, err
   185  			}
   186  			linux.Kernel = f
   187  
   188  		// TODO: initrd may be specified more than once.
   189  		// TODO: For now only process the first token, the rest are ignored, e.g. '$tuned_initrd'.
   190  		case "initrd":
   191  			tokens = strings.Split(val, " ")
   192  			f, err := os.Open(filePath(fsRoot, tokens[0]))
   193  			if err != nil {
   194  				return nil, err
   195  			}
   196  			linux.Initrd = f
   197  
   198  		case "devicetree":
   199  			// Explicitly return an error rather than ignore this,
   200  			// because the intended kernel likely won't boot
   201  			// correctly if we silently ignore this attribute.
   202  			return nil, fmt.Errorf("devicetree attribute unsupported for Linux entries")
   203  
   204  		// options may appear more than once.
   205  		case "options":
   206  			tokens = strings.Split(val, " ")
   207  			var err error
   208  			for _, w := range tokens {
   209  				switch w {
   210  				// TODO: GRUB/BLS parser should also get kernelopts from grubenv file
   211  				case "$kernelopts":
   212  					if value, err = getGrubvalue(variables, "kernelopts"); err != nil {
   213  						return nil, fmt.Errorf("variables map is nil for $kernelopts")
   214  					}
   215  					if value == "" {
   216  						// If it's not found, fallback to look for default_kernelopts
   217  						log.Printf("kernelopts is empty, look for default_kernelopts\n")
   218  						if value, err = getGrubvalue(variables, "default_kernelopts"); value == "" {
   219  							return nil, fmt.Errorf("No valid kernelopts is found")
   220  						}
   221  					}
   222  					cmdlines = append(cmdlines, value)
   223  					break
   224  				case "$tuned_params":
   225  					if value, err = getGrubvalue(variables, "tuned_params"); err != nil {
   226  						return nil, fmt.Errorf("variables map is nil for $tuned_params")
   227  					}
   228  					cmdlines = append(cmdlines, value)
   229  					break
   230  				default:
   231  					cmdlines = append(cmdlines, w)
   232  				}
   233  			}
   234  		}
   235  	}
   236  
   237  	// Spec says kernel is required.
   238  	if linux.Kernel == nil {
   239  		return nil, fmt.Errorf("malformed Linux config: linux keyword missing")
   240  	}
   241  
   242  	var name []string
   243  	if title, ok := vals["title"]; ok && len(title) > 0 {
   244  		name = append(name, title)
   245  	}
   246  	if version, ok := vals["version"]; ok && len(version) > 0 {
   247  		name = append(name, version)
   248  	}
   249  	// If both title and version were empty, so will this.
   250  	linux.Name = strings.Join(name, " ")
   251  	linux.Cmdline = strings.Join(cmdlines, " ")
   252  	linux.BootRank = blsDefaultRank
   253  	if val, exist := os.LookupEnv("BLS_BOOT_RANK"); exist {
   254  		if rank, err := strconv.Atoi(val); err == nil {
   255  			linux.BootRank = rank
   256  		}
   257  	}
   258  
   259  	return linux, nil
   260  }
   261  
   262  // parseBLSEntry takes a Type #1 BLS entry and the directory of entries, and
   263  // returns a LinuxImage.
   264  // An error is returned if the syntax is wrong or required keys are missing.
   265  func parseBLSEntry(entryPath, fsRoot string, variables map[string]string) (boot.OSImage, error) {
   266  	vals, err := parseConf(entryPath)
   267  	if err != nil {
   268  		return nil, fmt.Errorf("error parsing config in %s: %w", entryPath, err)
   269  	}
   270  
   271  	var img boot.OSImage
   272  	err = fmt.Errorf("neither linux, efi, nor multiboot present in BootLoaderSpec config")
   273  	if _, ok := vals["linux"]; ok {
   274  		img, err = parseLinuxImage(vals, fsRoot, variables)
   275  	} else if _, ok := vals["multiboot"]; ok {
   276  		err = fmt.Errorf("multiboot not yet supported")
   277  	} else if _, ok := vals["efi"]; ok {
   278  		err = fmt.Errorf("EFI not yet supported")
   279  	}
   280  	if err != nil {
   281  		return nil, fmt.Errorf("error parsing config in %s: %w", entryPath, err)
   282  	}
   283  	return img, nil
   284  }