github.com/oweisse/u-root@v0.0.0-20181109060735-d005ad25fef1/cmds/boot/boot.go (about) 1 // Copyright 2012-2017 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 // boot allows to handover a system running linuxboot/u-root 6 // to a legacy preinstalled operating system by replacing the traditional 7 // bootloader path 8 9 // 10 // Synopsis: 11 // boot [-dev][-v][-dryrun] 12 // 13 // Description: 14 // If returns to u-root shell, the code didn't found a local bootable option 15 // -dev glob to use; default is /sys/class/block/* 16 // -v prints messages 17 // -dryrun doesn't really boot 18 // 19 // Notes: 20 // The code is looking for boot/grub/grub.cfg file as to identify the 21 // boot option. 22 // The first bootable device found in the block device tree is the one used 23 // Windows is not supported (that is a work in progress) 24 // 25 // Example: 26 // boot -v - Start the script in verbose mode for debugging purpose 27 28 package main 29 30 import ( 31 "encoding/binary" 32 "flag" 33 "fmt" 34 "github.com/u-root/u-root/pkg/kexec" 35 "io" 36 "io/ioutil" 37 "log" 38 "os" 39 "path/filepath" 40 "strconv" 41 "strings" 42 "syscall" 43 ) 44 45 const ( 46 bootableMBR = 0xaa55 47 signatureOffset = 510 48 ) 49 50 type bootEntry struct { 51 kernel string 52 initrd string 53 cmdline string 54 } 55 56 var ( 57 devGlob = flag.String("dev", "/sys/block/*", "Glob for devices") 58 v = flag.Bool("v", false, "Print debug messages") 59 verbose = func(string, ...interface{}) {} 60 dryrun = flag.Bool("dryrun", true, "Boot") 61 defaultBoot = flag.String("boot", "default", "Default entry to boot") 62 uroot string 63 ) 64 65 // checkForBootableMBR is looking for bootable MBR signature 66 // Current support is limited to Hard disk devices and USB devices 67 func checkForBootableMBR(path string) error { 68 var sig uint16 69 f, err := os.Open(path) 70 if err != nil { 71 return err 72 } 73 if err := binary.Read(io.NewSectionReader(f, signatureOffset, 2), binary.LittleEndian, &sig); err != nil { 74 return err 75 } 76 if sig != bootableMBR { 77 err := fmt.Errorf("%v is not a bootable device", path) 78 return err 79 } 80 return nil 81 } 82 83 // getSupportedFilesystem returns all block file system supported by the linuxboot kernel 84 func getSupportedFilesystem() ([]string, error) { 85 var err error 86 fs, err := ioutil.ReadFile("/proc/filesystems") 87 if err != nil { 88 return nil, err 89 } 90 var returnValue []string 91 for _, f := range strings.Split(string(fs), "\n") { 92 n := strings.Fields(f) 93 if len(n) != 1 { 94 continue 95 } 96 returnValue = append(returnValue, n[0]) 97 } 98 return returnValue, err 99 100 } 101 102 // mountEntry tries to mount a specific block device using a list of 103 // supported file systems. We have to try to mount the device 104 // itself, since devices can be filesystem formatted but not 105 // partitioned; and all its partitions. 106 func mountEntry(d string, supportedFilesystem []string) error { 107 var err error 108 verbose("Try to mount %v", d) 109 110 // find or create the mountpoint. 111 m := filepath.Join(uroot, d) 112 if _, err = os.Stat(m); err != nil && os.IsNotExist(err) { 113 err = os.MkdirAll(m, 0777) 114 } 115 if err != nil { 116 verbose("Can't make %v", m) 117 return err 118 } 119 for _, filesystem := range supportedFilesystem { 120 var flags = uintptr(syscall.MS_RDONLY) 121 verbose("\twith %v", filesystem) 122 if err := syscall.Mount(d, m, filesystem, flags, ""); err == nil { 123 return err 124 } 125 } 126 verbose("No mount succeeded") 127 return fmt.Errorf("Unable to mount any partition on %v", d) 128 } 129 130 func umountEntry(n string) error { 131 return syscall.Unmount(n, syscall.MNT_DETACH) 132 } 133 134 // loadISOLinux reads an isolinux.cfg file. It further needs to 135 // process include directives. 136 // The include files are correctly placed in line at the place they 137 // were included. Include files can include other files, i.e. this 138 // function can recurse. 139 // It returns a []string of all the files, the path to the directory 140 // contain the boot images (bzImage, initrd, etc.) and the mount point. 141 // For now, these are the same, but in, e.g., grub, they are different, 142 // and they might in future change here too. 143 func loadISOLinux(dir, base string) ([]string, string, string, error) { 144 sol := filepath.Join(dir, base) 145 isolinux, err := ioutil.ReadFile(sol) 146 if err != nil { 147 return nil, "", "", err 148 } 149 150 // it's easier we think to do include processing here. 151 lines := strings.Split(string(isolinux), "\n") 152 var result []string 153 for _, l := range lines { 154 f := strings.Fields(l) 155 if len(f) == 0 { 156 continue 157 } 158 switch f[0] { 159 default: 160 result = append(result, l) 161 case "include": 162 i, _, _, err := loadISOLinux(dir, f[1]) 163 if err != nil { 164 return nil, "", "", err 165 } 166 result = append(result, i...) 167 // If the string contain include 168 // we must call back as to read the content of the file 169 } 170 } 171 return result, dir, dir, nil 172 } 173 174 // checkBootEntry is looking for grub.cfg file 175 // and return absolute path to it. It returns a []string with all the commands, 176 // the path to the direction containing files to load for kexec, and the mountpoint. 177 func checkBootEntry(mountPoint string) ([]string, string, string, error) { 178 grub, err := ioutil.ReadFile(filepath.Join(mountPoint, "boot/grub/grub.cfg")) 179 if err == nil { 180 return strings.Split(string(grub), "\n"), filepath.Join(mountPoint, "/boot"), mountPoint, nil 181 } 182 return loadISOLinux(filepath.Join(mountPoint, "isolinux"), "isolinux.cfg") 183 } 184 185 // getFileMenuContent is parsing a grub.cfg file 186 // output: bootEntries 187 // grub parsing is a good deal messier than it looks. 188 // This is a simple parser will fail on anything tricky. 189 func getBootEntries(lines []string) (map[string]*bootEntry, map[string]string, error) { 190 // There are two ways to reference a bootEntry: its order in the file and its 191 // name. 192 var ( 193 lineno int 194 line string 195 err error 196 curEntry string 197 numEntry int 198 bootEntries = make(map[string]*bootEntry) 199 bootVars = make(map[string]string) 200 f []string 201 be *bootEntry 202 ) 203 defer func() { 204 switch err := recover().(type) { 205 case nil: 206 case error: 207 log.Fatalf("Bummer: %v, line #%d, line %q, fields %q", err, lineno, line, f) 208 default: 209 log.Fatalf("unexpected panic value: %T(%v)", err, err) 210 } 211 }() 212 213 verbose("getBootEntries: %s", lines) 214 curLabel := "" 215 for _, line = range lines { 216 lineno++ 217 f = strings.Fields(line) 218 verbose("%d: %q, %q", lineno, line, f) 219 if len(f) == 0 { 220 continue 221 } 222 switch f[0] { 223 default: 224 case "#": 225 case "default": 226 bootVars["default"] = f[1] 227 case "set": 228 vals := strings.SplitN(f[1], "=", 2) 229 if len(vals) == 2 { 230 bootVars[vals[0]] = vals[1] 231 } 232 case "menuentry", "label": 233 verbose("Menuentry %v", line) 234 // nasty, but the alternatives are not much better. 235 // grub config language is kind of arbitrary 236 curEntry = fmt.Sprintf("\"%s\"", strconv.Itoa(numEntry)) 237 be = &bootEntry{} 238 curLabel = f[1] 239 bootEntries[f[1]] = be 240 bootEntries[curEntry] = be 241 numEntry++ 242 case "menu": 243 // keyword default marks this as default 244 if f[1] != "default" { 245 continue 246 } 247 bootVars["default"] = curLabel 248 case "linux", "kernel": 249 verbose("linux %v", line) 250 be.kernel = f[1] 251 be.cmdline = strings.Join(f[2:], " ") 252 case "initrd": 253 verbose("initrd %v", line) 254 be.initrd = f[1] 255 case "append": 256 // Format is a little bit strange 257 // append MENU=/bin/cdrom-checker-menu vga=788 initrd=/install/initrd.gz quiet -- 258 var current_parameter int 259 for _, parameter := range f { 260 if current_parameter > 0 { 261 if strings.HasPrefix(parameter, "initrd") { 262 initrd := strings.Split(parameter, "=") 263 bootEntries[curEntry].initrd = bootEntries[curEntry].initrd + initrd[1] 264 } else { 265 if parameter != "--" { 266 bootEntries[curEntry].cmdline = bootEntries[curEntry].cmdline + " " + parameter 267 } 268 } 269 } 270 current_parameter++ 271 } 272 } 273 } 274 verbose("grub config menu decoded to [%q, %q, %v]", bootEntries, bootVars, err) 275 return bootEntries, bootVars, err 276 277 } 278 279 func copyLocal(path string) (string, error) { 280 var dest string 281 var err error 282 result := strings.Split(path, "/") 283 for _, entry := range result { 284 dest = entry 285 } 286 dest = "/tmp/" + dest 287 srcFile, err := os.Open(path) 288 if err != nil { 289 return dest, err 290 } 291 292 destFile, err := os.Create(dest) // creates if file doesn't exist 293 if err != nil { 294 return dest, err 295 } 296 297 _, err = io.Copy(destFile, srcFile) // check first var for number of bytes copied 298 if err != nil { 299 return dest, err 300 } 301 302 err = destFile.Sync() 303 if err != nil { 304 return dest, err 305 } 306 err = destFile.Close() 307 if err != nil { 308 return dest, err 309 } 310 err = srcFile.Close() 311 if err != nil { 312 return dest, err 313 } 314 return dest, nil 315 } 316 317 // kexecLoad Loads a new kernel and initrd. 318 func kexecLoad(grubConfPath string, grub []string, mountPoint string) error { 319 verbose("kexecEntry: boot from %v", grubConfPath) 320 b, v, err := getBootEntries(grub) 321 if err != nil { 322 return err 323 } 324 325 verbose("Boot Entries: %q", b) 326 entry, ok := v[*defaultBoot] 327 if !ok { 328 return fmt.Errorf("Entry %v not found in config file", *defaultBoot) 329 } 330 be, ok := b[entry] 331 if !ok { 332 return fmt.Errorf("Entry %v not found in boot entries file", entry) 333 } 334 335 verbose("Boot params: %q", be) 336 localKernelPath, err := copyLocal(filepath.Join(mountPoint, be.kernel)) 337 if err != nil { 338 verbose("copyLocal(%v, %v): %v", filepath.Join(mountPoint, be.kernel), err) 339 return err 340 } 341 localInitrdPath, err := copyLocal(filepath.Join(mountPoint, be.initrd)) 342 if err != nil { 343 verbose("copyLocal(%v, %v): %v", filepath.Join(mountPoint, be.initrd), err) 344 return err 345 } 346 verbose(localKernelPath) 347 348 // We can kexec the kernel with localKernelPath as kernel entry, kernelParameter as parameter and initrd as initrd ! 349 log.Printf("Loading %s for kernel\n", localKernelPath) 350 351 kernelDesc, err := os.OpenFile(localKernelPath, os.O_RDONLY, 0) 352 if err != nil { 353 verbose("%v", err) 354 return err 355 } 356 // defer kernelDesc.Close() 357 358 log.Printf("Loading %s for initramfs", localInitrdPath) 359 ramfs, err := os.OpenFile(localInitrdPath, os.O_RDONLY, 0) 360 if err != nil { 361 verbose("%v", err) 362 return err 363 } 364 // defer ramfs.Close() 365 366 // if /tmp/rsdp file exist we must get the value and add it to the be.cmdline parameter 367 // this will allow the kernel to properly read the ACPI table 368 369 rsdp, err := ioutil.ReadFile("/tmp/rsdp") 370 if err == nil { 371 be.cmdline = be.cmdline + string(rsdp) 372 } 373 374 log.Printf("Kernel cmdline %s", be.cmdline) 375 376 if err := kexec.FileLoad(kernelDesc, ramfs, be.cmdline); err != nil { 377 verbose("%v", err) 378 return err 379 } 380 if err = ramfs.Close(); err != nil { 381 verbose("%v", err) 382 return err 383 } 384 if err = kernelDesc.Close(); err != nil { 385 verbose("%v", err) 386 return err 387 } 388 return err 389 390 } 391 392 func main() { 393 flag.Parse() 394 395 if *v { 396 verbose = log.Printf 397 } 398 fs, err := getSupportedFilesystem() 399 if err != nil { 400 log.Panic("No filesystem support found") 401 } 402 verbose("Supported filesystems: %v", fs) 403 sysList, err := filepath.Glob(*devGlob) 404 if err != nil { 405 log.Panic("No available block devices to boot from") 406 } 407 // The Linux /sys file system is a bit, er, awkward. You can't find 408 // the device special in there; just everything else. 409 var blkList []string 410 for _, b := range sysList { 411 blkList = append(blkList, filepath.Join("/dev", filepath.Base(b))) 412 } 413 414 // We must validate if the MBR is bootable or not and keep the 415 // devices which do have such support drive are easy to 416 // detect. This whole loop is pretty bogus at present, it 417 // assumes the first partiton we find with grub.cfg is the one 418 // we want. It works for now but ... 419 var allparts []string 420 for _, d := range blkList { 421 err := checkForBootableMBR(d) 422 if err != nil { 423 // Not sure it matters; there can be many bogus entries? 424 log.Printf("MBR for %s failed: %v", d, err) 425 continue 426 } 427 verbose("Bootable device %v found", d) 428 // You can't just look for numbers to match. Consider names like 429 // mmcblk0, where has parts like mmcblk0p1. Just glob. 430 g := d + "*" 431 all, err := filepath.Glob(g) 432 if err != nil { 433 log.Printf("Glob for all partitions of %s failed: %v", g, err) 434 } 435 allparts = append(allparts, all...) 436 } 437 uroot, err = ioutil.TempDir("", "u-root-boot") 438 if err != nil { 439 log.Fatalf("Can't create tmpdir: %v", err) 440 } 441 verbose("Trying to boot from %v", allparts) 442 for _, d := range allparts { 443 if err := mountEntry(d, fs); err != nil { 444 continue 445 } 446 verbose("mount succeed") 447 u := filepath.Join(uroot, d) 448 config, fileDir, root, err := checkBootEntry(u) 449 if err != nil { 450 verbose("d: %v", d, err) 451 if err := umountEntry(u); err != nil { 452 log.Printf("Can't unmount %v: %v", u, err) 453 } 454 continue 455 } 456 verbose("calling basic kexec: content %v, path %v", config, fileDir) 457 if err = kexecLoad(fileDir, config, root); err != nil { 458 log.Printf("kexec on %v failed: %v", u, err) 459 } 460 verbose("kexecLoad succeeded") 461 462 if err := umountEntry(u); err != nil { 463 log.Printf("Can't unmount %v: %v", u, err) 464 } 465 if *dryrun { 466 continue 467 } 468 if err := kexec.Reboot(); err != nil { 469 log.Printf("Kexec Reboot %v failed, %v. Sorry", u, err) 470 } 471 } 472 log.Fatalf("Sorry no bootable device found") 473 }