github.com/insomniacslk/u-root@v0.0.0-20200717035308-96b791510d76/pkg/boot/esxi/esxi.go (about)

     1  // Copyright 2019 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 esxi contains an ESXi boot config parser for disks and CDROMs.
     6  //
     7  // For CDROMs, it parses the boot.cfg found in the root directory and tries to
     8  // boot from it.
     9  //
    10  // For disks, there may be multiple boot partitions:
    11  //
    12  // - Locates both <device>5/boot.cfg and <device>6/boot.cfg.
    13  //
    14  // - If parsable, chooses partition with bootstate=(0|2|empty) and greater
    15  // updated=N.
    16  //
    17  // Sometimes, an ESXi partition can contain a valid boot.cfg, but not actually
    18  // any of the named modules. Hence it is important to try fully loading ESXi
    19  // into memory, and only then falling back to the other partition.
    20  //
    21  // Only boots partitions with bootstate=0, bootstate=2, bootstate=(empty) will
    22  // boot at all.
    23  //
    24  // Most of the parsing logic in this package comes from
    25  // https://github.com/vmware/esx-boot/blob/master/safeboot/bootbank.c
    26  package esxi
    27  
    28  import (
    29  	"bufio"
    30  	"encoding/hex"
    31  	"fmt"
    32  	"io"
    33  	"io/ioutil"
    34  	"os"
    35  	"path/filepath"
    36  	"strconv"
    37  	"strings"
    38  
    39  	"golang.org/x/sys/unix"
    40  
    41  	"github.com/u-root/u-root/pkg/boot"
    42  	"github.com/u-root/u-root/pkg/boot/multiboot"
    43  	"github.com/u-root/u-root/pkg/mount"
    44  	"github.com/u-root/u-root/pkg/mount/gpt"
    45  	"github.com/u-root/u-root/pkg/uio"
    46  )
    47  
    48  // LoadDisk loads the right ESXi multiboot kernel from partitions 5 or 6 of the
    49  // given device.
    50  //
    51  // The kernels are returned in the priority order according to the bootstate
    52  // and updated values in their boot configurations.
    53  //
    54  // The caller should try loading all returned images in order, as some of them
    55  // may not be valid.
    56  //
    57  // device5 and device6 will be mounted at temporary directories.
    58  func LoadDisk(device string) ([]*boot.MultibootImage, []*mount.MountPoint, error) {
    59  	opts5, mp5, err5 := mountPartition(fmt.Sprintf("%s5", device))
    60  	opts6, mp6, err6 := mountPartition(fmt.Sprintf("%s6", device))
    61  	if err5 != nil && err6 != nil {
    62  		return nil, nil, fmt.Errorf("could not mount or read either partition 5 (%v) or partition 6 (%v)", err5, err6)
    63  	}
    64  	var mps []*mount.MountPoint
    65  	if mp5 != nil {
    66  		mps = append(mps, mp5)
    67  	}
    68  	if mp6 != nil {
    69  		mps = append(mps, mp6)
    70  	}
    71  
    72  	imgs, err := getImages(device, opts5, opts6)
    73  	if err != nil {
    74  		for _, mp := range mps {
    75  			mp.Unmount(mount.MNT_DETACH)
    76  		}
    77  		return nil, nil, err
    78  	}
    79  	return imgs, mps, nil
    80  }
    81  
    82  func getImages(device string, opts5, opts6 *options) ([]*boot.MultibootImage, error) {
    83  	var (
    84  		img5, img6 *boot.MultibootImage
    85  		err5, err6 error
    86  	)
    87  	if opts5 != nil {
    88  		img5, err5 = getBootImage(*opts5, device, 5, fmt.Sprintf("%s%d", device, 5))
    89  	}
    90  	if opts6 != nil {
    91  		img6, err6 = getBootImage(*opts6, device, 6, fmt.Sprintf("%s%d", device, 6))
    92  	}
    93  	if img5 == nil && img6 == nil {
    94  		return nil, fmt.Errorf("could not read boot configs on partition 5 (%v) or partition 6 (%v)", err5, err6)
    95  	}
    96  
    97  	if img5 != nil && img6 != nil {
    98  		if opts6.updated > opts5.updated {
    99  			return []*boot.MultibootImage{img6, img5}, nil
   100  		}
   101  		return []*boot.MultibootImage{img5, img6}, nil
   102  	} else if img5 != nil {
   103  		return []*boot.MultibootImage{img5}, nil
   104  	}
   105  	return []*boot.MultibootImage{img6}, nil
   106  }
   107  
   108  // LoadCDROM loads an ESXi multiboot kernel from a CDROM at device.
   109  //
   110  // device will be mounted at mountPoint.
   111  func LoadCDROM(device string) (*boot.MultibootImage, *mount.MountPoint, error) {
   112  	mountPoint, err := ioutil.TempDir("", "esxi-mount-")
   113  	if err != nil {
   114  		return nil, nil, err
   115  	}
   116  	mp, err := mount.Mount(device, mountPoint, "iso9660", "", unix.MS_RDONLY|unix.MS_NOATIME)
   117  	if err != nil {
   118  		os.RemoveAll(mountPoint)
   119  		return nil, nil, err
   120  	}
   121  
   122  	opts, err := parse(filepath.Join(mountPoint, "boot.cfg"))
   123  	if err != nil {
   124  		mp.Unmount(mount.MNT_DETACH)
   125  		os.RemoveAll(mountPoint)
   126  		return nil, nil, fmt.Errorf("cannot parse config from %s: %v", device, err)
   127  	}
   128  	img, err := getBootImage(opts, "", 0, device)
   129  	if err != nil {
   130  		mp.Unmount(mount.MNT_DETACH)
   131  		os.RemoveAll(mountPoint)
   132  		return nil, nil, err
   133  	}
   134  	return img, mp, nil
   135  }
   136  
   137  // LoadConfig loads an ESXi configuration from configFile.
   138  func LoadConfig(configFile string) (*boot.MultibootImage, error) {
   139  	opts, err := parse(configFile)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err)
   142  	}
   143  	return getBootImage(opts, "", 0, fmt.Sprintf("config file %s", configFile))
   144  }
   145  
   146  func mountPartition(dev string) (*options, *mount.MountPoint, error) {
   147  	base := filepath.Base(dev)
   148  	mountPoint, err := ioutil.TempDir("", fmt.Sprintf("%s-", base))
   149  	if err != nil {
   150  		return nil, nil, err
   151  	}
   152  	mp, err := mount.Mount(dev, mountPoint, "vfat", "", unix.MS_RDONLY|unix.MS_NOATIME)
   153  	if err != nil {
   154  		os.RemoveAll(mountPoint)
   155  		return nil, nil, err
   156  	}
   157  
   158  	configFile := filepath.Join(mountPoint, "boot.cfg")
   159  	opts, err := parse(configFile)
   160  	if err != nil {
   161  		mp.Unmount(mount.MNT_DETACH)
   162  		os.RemoveAll(mountPoint)
   163  		return nil, nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err)
   164  	}
   165  	return &opts, mp, nil
   166  }
   167  
   168  // lazyOpenModules assigns modules to be opened as files.
   169  //
   170  // Each module is a path followed by optional command-line arguments, e.g.
   171  // []string{"./module arg1 arg2", "./module2 arg3 arg4"}.
   172  func lazyOpenModules(mods []module) multiboot.Modules {
   173  	modules := make([]multiboot.Module, 0, len(mods))
   174  	for _, m := range mods {
   175  		name := strings.Fields(m.cmdline)[0]
   176  		modules = append(modules, multiboot.Module{
   177  			CmdLine: m.cmdline,
   178  			Name:    name,
   179  			Module:  uio.NewLazyFile(m.path),
   180  		})
   181  	}
   182  	return modules
   183  }
   184  
   185  func getBootImage(opts options, device string, partition int, name string) (*boot.MultibootImage, error) {
   186  	// Only valid and upgrading are bootable partitions.
   187  	//
   188  	// We are supposed to support the following two state transitions (only
   189  	// one transition every boot!):
   190  	//
   191  	// upgrading -> dirty
   192  	// dirty -> invalid
   193  	//
   194  	// A validly booted system will set its own bootstate to "valid" from
   195  	// "dirty".
   196  	//
   197  	// We currently don't support writing the state back to disk, which is
   198  	// fine in our manual testing.
   199  	if opts.bootstate != bootValid && opts.bootstate != bootUpgrading {
   200  		return nil, fmt.Errorf("boot state %d invalid", opts.bootstate)
   201  	}
   202  
   203  	if len(device) > 0 {
   204  		if err := opts.addUUID(device, partition); err != nil {
   205  			return nil, fmt.Errorf("cannot add boot uuid of %s: %v", device, err)
   206  		}
   207  	}
   208  
   209  	return &boot.MultibootImage{
   210  		Name:    fmt.Sprintf("%s from %s", opts.title, name),
   211  		Kernel:  uio.NewLazyFile(opts.kernel),
   212  		Cmdline: opts.args,
   213  		Modules: lazyOpenModules(opts.modules),
   214  	}, nil
   215  }
   216  
   217  type module struct {
   218  	path    string
   219  	cmdline string
   220  }
   221  
   222  type options struct {
   223  	title     string
   224  	kernel    string
   225  	args      string
   226  	modules   []module
   227  	updated   int
   228  	bootstate bootstate
   229  }
   230  
   231  type bootstate int
   232  
   233  // From safeboot.c
   234  const (
   235  	bootValid     bootstate = 0
   236  	bootUpgrading bootstate = 1
   237  	bootDirty     bootstate = 2
   238  	bootInvalid   bootstate = 3
   239  )
   240  
   241  // So tests can replace this and don't have to have actual block devices.
   242  var getBlockSize = gpt.GetBlockSize
   243  
   244  func getUUID(device string, partition int) (string, error) {
   245  	device = strings.TrimRight(device, "/")
   246  	blockSize, err := getBlockSize(device)
   247  	if err != nil {
   248  		return "", err
   249  	}
   250  
   251  	f, err := os.Open(fmt.Sprintf("%s%d", device, partition))
   252  	if err != nil {
   253  		return "", err
   254  	}
   255  
   256  	// Boot uuid is stored in the second block of the disk
   257  	// in the following format:
   258  	//
   259  	// VMWARE FAT16    <uuid>
   260  	// <---128 bit----><128 bit>
   261  	data := make([]byte, uuidSize)
   262  	n, err := f.ReadAt(data, int64(blockSize))
   263  	if err != nil {
   264  		return "", err
   265  	}
   266  	if n != uuidSize {
   267  		return "", io.ErrUnexpectedEOF
   268  	}
   269  
   270  	if magic := string(data[:len(uuidMagic)]); magic != uuidMagic {
   271  		return "", fmt.Errorf("bad uuid magic %q, want %q", magic, uuidMagic)
   272  	}
   273  
   274  	uuid := hex.EncodeToString(data[len(uuidMagic):])
   275  	return fmt.Sprintf("bootUUID=%s", uuid), nil
   276  }
   277  
   278  func (o *options) addUUID(device string, partition int) error {
   279  	uuid, err := getUUID(device, partition)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	o.args += " " + uuid
   284  	return nil
   285  }
   286  
   287  const (
   288  	comment = '#'
   289  	sep     = "---"
   290  
   291  	uuidMagic = "VMWARE FAT16    "
   292  	uuidSize  = 32
   293  )
   294  
   295  func parse(configFile string) (options, error) {
   296  	dir := filepath.Dir(configFile)
   297  
   298  	f, err := os.Open(configFile)
   299  	if err != nil {
   300  		return options{}, err
   301  	}
   302  	defer f.Close()
   303  
   304  	// An empty or missing updated value is always 0, so we can let the
   305  	// ints be initialized to 0.
   306  	//
   307  	// see esx-boot/bootlib/parse.c:parse_config_file.
   308  	opt := options{
   309  		title: "VMware ESXi",
   310  		// Default value taken from
   311  		// esx-boot/safeboot/bootbank.c:bank_scan.
   312  		bootstate: bootInvalid,
   313  	}
   314  
   315  	scanner := bufio.NewScanner(f)
   316  	for scanner.Scan() {
   317  		line := scanner.Text()
   318  		line = strings.TrimSpace(line)
   319  
   320  		if len(line) == 0 || line[0] == comment {
   321  			continue
   322  		}
   323  
   324  		tokens := strings.SplitN(line, "=", 2)
   325  		if len(tokens) != 2 {
   326  			return opt, fmt.Errorf("bad line %q", line)
   327  		}
   328  		key := strings.TrimSpace(tokens[0])
   329  		val := strings.TrimSpace(tokens[1])
   330  		switch key {
   331  		case "title":
   332  			opt.title = val
   333  
   334  		case "kernel":
   335  			opt.kernel = filepath.Join(dir, val)
   336  
   337  			// The kernel cmdline is expected to have the filename
   338  			// first, as in cmdlines[0] here:
   339  			// https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L870
   340  			//
   341  			// Note that the kernel is module 0 in the esx-boot
   342  			// code base, but it doesn't get loaded like that into
   343  			// the info structure; see -- so don't panic like I did
   344  			// when you read that!
   345  			// https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L578
   346  			opt.args = val + " " + opt.args
   347  
   348  		case "kernelopt":
   349  			opt.args += val
   350  
   351  		case "updated":
   352  			if len(val) == 0 {
   353  				// Explicitly setting to 0, as in
   354  				// esx-boot/bootlib/parse.c:parse_config_file,
   355  				// in case this value is specified twice.
   356  				opt.updated = 0
   357  			} else {
   358  				n, err := strconv.Atoi(val)
   359  				if err != nil {
   360  					return options{}, err
   361  				}
   362  				opt.updated = n
   363  			}
   364  		case "bootstate":
   365  			if len(val) == 0 {
   366  				// Explicitly setting to valid, as in
   367  				// esx-boot/bootlib/parse.c:parse_config_file,
   368  				// in case this value is specified twice.
   369  				opt.bootstate = bootValid
   370  			} else {
   371  				n, err := strconv.Atoi(val)
   372  				if err != nil {
   373  					return options{}, err
   374  				}
   375  				if n < 0 || n > 3 {
   376  					opt.bootstate = bootInvalid
   377  				} else {
   378  					opt.bootstate = bootstate(n)
   379  				}
   380  			}
   381  		case "modules":
   382  			for _, tok := range strings.Split(val, sep) {
   383  				// Each module is "filename arg0 arg1 arg2" and
   384  				// the filename is relative to the directory
   385  				// the module is in.
   386  				tok = strings.TrimSpace(tok)
   387  				if len(tok) > 0 {
   388  					entry := strings.Fields(tok)
   389  					opt.modules = append(opt.modules, module{
   390  						path:    filepath.Join(dir, entry[0]),
   391  						cmdline: tok,
   392  					})
   393  				}
   394  			}
   395  		}
   396  	}
   397  
   398  	err = scanner.Err()
   399  	return opt, err
   400  }