github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/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  	"os"
    20  	"path/filepath"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/u-root/u-root/pkg/boot"
    25  	"github.com/u-root/u-root/pkg/ulog"
    26  )
    27  
    28  const (
    29  	blsEntriesDir = "loader/entries"
    30  )
    31  
    32  func cutConf(s string) string {
    33  	if strings.HasSuffix(s, ".conf") {
    34  		return s[:len(s)-6]
    35  	}
    36  	return s
    37  }
    38  
    39  // ScanBLSEntries scans the filesystem root for valid BLS entries.
    40  // This function skips over invalid or unreadable entries in an effort
    41  // to return everything that is bootable.
    42  func ScanBLSEntries(log ulog.Logger, fsRoot string) ([]boot.OSImage, error) {
    43  	entriesDir := filepath.Join(fsRoot, blsEntriesDir)
    44  
    45  	files, err := filepath.Glob(filepath.Join(entriesDir, "*.conf"))
    46  	if err != nil {
    47  		return nil, fmt.Errorf("no BootLoaderSpec entries found: %w", err)
    48  	}
    49  
    50  	// loader.conf is not in the real spec; it's an implementation detail
    51  	// of systemd-boot. It is specified in
    52  	// https://www.freedesktop.org/software/systemd/man/loader.conf.html
    53  	loaderConf, err := parseConf(filepath.Join(fsRoot, "loader", "loader.conf"))
    54  	if err != nil {
    55  		// loader.conf is optional.
    56  		loaderConf = make(map[string]string)
    57  	}
    58  
    59  	// TODO: Rank entries by version or machine-id attribute as suggested
    60  	// in the spec (but not mandated, surprisingly).
    61  	imgs := make(map[string]boot.OSImage)
    62  	for _, f := range files {
    63  		identifier := cutConf(filepath.Base(f))
    64  
    65  		img, err := parseBLSEntry(f, fsRoot)
    66  		if err != nil {
    67  			log.Printf("BootLoaderSpec skipping entry %s: %v", f, err)
    68  			continue
    69  		}
    70  		imgs[identifier] = img
    71  	}
    72  
    73  	return sortImages(loaderConf, imgs), nil
    74  }
    75  
    76  func sortImages(loaderConf map[string]string, imgs map[string]boot.OSImage) []boot.OSImage {
    77  	// rankedImages = sort(default-images) + sort(remaining images)
    78  	var rankedImages []boot.OSImage
    79  
    80  	pattern, ok := loaderConf["default"]
    81  	if !ok {
    82  		// All images are default.
    83  		pattern = "*"
    84  	}
    85  
    86  	var defaultIdents []string
    87  	var otherIdents []string
    88  
    89  	// Find default and non-default identifiers.
    90  	for ident := range imgs {
    91  		ok, err := filepath.Match(pattern, ident)
    92  		if err != nil && ok {
    93  			defaultIdents = append(defaultIdents, ident)
    94  		} else {
    95  			otherIdents = append(otherIdents, ident)
    96  		}
    97  	}
    98  
    99  	// Sort them in the order we want them.
   100  	sort.Sort(sort.Reverse(sort.StringSlice(defaultIdents)))
   101  	sort.Sort(sort.Reverse(sort.StringSlice(otherIdents)))
   102  
   103  	// Add images to rankedImages in that sorted order, defaults first.
   104  	for _, ident := range defaultIdents {
   105  		rankedImages = append(rankedImages, imgs[ident])
   106  	}
   107  	for _, ident := range otherIdents {
   108  		rankedImages = append(rankedImages, imgs[ident])
   109  	}
   110  	return rankedImages
   111  }
   112  
   113  func parseConf(entryPath string) (map[string]string, error) {
   114  	f, err := os.Open(entryPath)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	defer f.Close()
   119  
   120  	vals := make(map[string]string)
   121  
   122  	scanner := bufio.NewScanner(f)
   123  	for scanner.Scan() {
   124  		line := scanner.Text()
   125  		if strings.HasPrefix(line, "#") {
   126  			continue
   127  		}
   128  		line = strings.TrimSpace(line)
   129  
   130  		sline := strings.SplitN(line, " ", 2)
   131  		if len(sline) != 2 {
   132  			continue
   133  		}
   134  		vals[sline[0]] = strings.TrimSpace(sline[1])
   135  	}
   136  	return vals, nil
   137  }
   138  
   139  // The spec says "$BOOT/loader/ is the directory containing all files needed
   140  // for Type #1 entries", but that's bullshit. Relative file names are indeed in
   141  // the $BOOT/loader/ directory, but absolute path names are in $BOOT, as
   142  // evidenced by the entries that kernel-install installs on Fedora 32.
   143  func filePath(fsRoot, value string) string {
   144  	if !filepath.IsAbs(value) {
   145  		return filepath.Join(fsRoot, "loader", value)
   146  	}
   147  	return filepath.Join(fsRoot, value)
   148  }
   149  
   150  func parseLinuxImage(vals map[string]string, fsRoot string) (boot.OSImage, error) {
   151  	linux := &boot.LinuxImage{}
   152  
   153  	var cmdlines []string
   154  	for key, val := range vals {
   155  		switch key {
   156  		case "linux":
   157  			f, err := os.Open(filePath(fsRoot, val))
   158  			if err != nil {
   159  				return nil, err
   160  			}
   161  			linux.Kernel = f
   162  
   163  		// TODO: initrd may be specified more than once.
   164  		case "initrd":
   165  			f, err := os.Open(filePath(fsRoot, val))
   166  			if err != nil {
   167  				return nil, err
   168  			}
   169  			linux.Initrd = f
   170  
   171  		case "devicetree":
   172  			// Explicitly return an error rather than ignore this,
   173  			// because the intended kernel likely won't boot
   174  			// correctly if we silently ignore this attribute.
   175  			return nil, fmt.Errorf("devicetree attribute unsupported for Linux entries")
   176  
   177  		// options may appear more than once.
   178  		case "options":
   179  			cmdlines = append(cmdlines, val)
   180  		}
   181  	}
   182  
   183  	// Spec says kernel is required.
   184  	if linux.Kernel == nil {
   185  		return nil, fmt.Errorf("malformed Linux config: linux keyword missing")
   186  	}
   187  
   188  	var name []string
   189  	if title, ok := vals["title"]; ok && len(title) > 0 {
   190  		name = append(name, title)
   191  	}
   192  	if version, ok := vals["version"]; ok && len(version) > 0 {
   193  		name = append(name, version)
   194  	}
   195  	// If both title and version were empty, so will this.
   196  	linux.Name = strings.Join(name, " ")
   197  	linux.Cmdline = strings.Join(cmdlines, " ")
   198  	return linux, nil
   199  }
   200  
   201  // parseBLSEntry takes a Type #1 BLS entry and the directory of entries, and
   202  // returns a LinuxImage.
   203  // An error is returned if the syntax is wrong or required keys are missing.
   204  func parseBLSEntry(entryPath, fsRoot string) (boot.OSImage, error) {
   205  	vals, err := parseConf(entryPath)
   206  	if err != nil {
   207  		return nil, fmt.Errorf("error parsing config in %s: %w", entryPath, err)
   208  	}
   209  
   210  	var img boot.OSImage
   211  	err = fmt.Errorf("neither linux, efi, nor multiboot present in BootLoaderSpec config")
   212  	if _, ok := vals["linux"]; ok {
   213  		img, err = parseLinuxImage(vals, fsRoot)
   214  	} else if _, ok := vals["multiboot"]; ok {
   215  		err = fmt.Errorf("multiboot not yet supported")
   216  	} else if _, ok := vals["efi"]; ok {
   217  		err = fmt.Errorf("EFI not yet supported")
   218  	}
   219  	if err != nil {
   220  		return nil, fmt.Errorf("error parsing config in %s: %w", entryPath, err)
   221  	}
   222  	return img, nil
   223  }