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  }