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 }