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 }