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 }