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