Skip to content

Commit bb4e798

Browse files
committed
Attach EFI VHD in read-only mode by default
EFI VHDs should always be attached as read-only by default to block UVMs from writing to it and corrupting its contents. A new annotation is added to allow attaching EFI VHDs in writable mode when debugging boot failures and such. When this annotation is included a copy of the EFI VHD is made next to the scratch VHD. This is based on the assumption that generally the scratch of the UVM would be stored in its own snapshot directory so adding another VHD in there shouldn't be a problem. It should get cleaned up when the snapshot is removed. This commit also adds the code to always grant VM group access to the VHDs and guest state files to avoid access denied failures. Signed-off-by: Amit Barve <ambarve@microsoft.com>
1 parent 002341a commit bb4e798

File tree

5 files changed

+91
-11
lines changed

5 files changed

+91
-11
lines changed

cmd/containerd-shim-runhcs-v1/pod.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"sync"
1212

13+
"github.com/Microsoft/hcsshim/internal/copyfile"
1314
"github.com/Microsoft/hcsshim/internal/layers"
1415
"github.com/Microsoft/hcsshim/internal/log"
1516
"github.com/Microsoft/hcsshim/internal/oci"
@@ -58,6 +59,21 @@ func initializeWCOWBootFiles(ctx context.Context, wopts *uvm.OptionsWCOW, rootfs
5859
// we can enable this.
5960
return fmt.Errorf("hyperv isolation is not supported with block CIM layers yet")
6061
}
62+
63+
// writable EFI VHD is a valid config for both confidential and regular hyperv
64+
// isolated WCOW. Override the default value here if required.
65+
if wopts.WritableEFI {
66+
// Make a copy of EFI VHD, we can't risk the UVM accidentally modifying
67+
// the original VHD. make copy next to the scratch VHD, this assumes that
68+
// the scratch is located in the separate directory dedicated for this
69+
// UVM.
70+
writableEFIVHDPath := filepath.Join(filepath.Dir(wopts.BootFiles.BlockCIMFiles.ScratchVHDPath), filepath.Base(wopts.BootFiles.BlockCIMFiles.EFIVHDPath))
71+
if err := copyfile.CopyFile(ctx, wopts.BootFiles.BlockCIMFiles.EFIVHDPath, writableEFIVHDPath, false); err != nil {
72+
return fmt.Errorf("failed to copy EFI VHD at %s: %w", writableEFIVHDPath, err)
73+
}
74+
wopts.BootFiles.BlockCIMFiles.EFIVHDPath = writableEFIVHDPath
75+
}
76+
6177
return nil
6278
}
6379

internal/oci/uvm.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,9 @@ func SpecToUVMCreateOpts(ctx context.Context, s *specs.Spec, id, owner string) (
376376
wopts.AdditionalRegistryKeys = append(wopts.AdditionalRegistryKeys, parseAdditionalRegistryValues(ctx, s.Annotations)...)
377377
handleAnnotationFullyPhysicallyBacked(ctx, s.Annotations, wopts)
378378

379+
// Writable EFI is valid for both confidential and regular Hyper-V isolated WCOW.
380+
wopts.WritableEFI = ParseAnnotationsBool(ctx, s.Annotations, annotations.WCOWWritableEFI, wopts.WritableEFI)
381+
379382
// Handle WCOW security policy settings
380383
if err := handleWCOWSecurityPolicy(ctx, s.Annotations, wopts); err != nil {
381384
return nil, err

internal/oci/uvm_test.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ func Test_SpecToUVMCreateOptions_WCOW_Confidential_Defaults(t *testing.T) {
139139
if !wopts.SecurityPolicyEnabled {
140140
t.Fatal("SecurityPolicyEnabled should be true when WCOWSecurityPolicy is set")
141141
}
142+
// Writable EFI should default to false unless explicitly enabled
143+
if wopts.WritableEFI {
144+
t.Fatal("WritableEFI should default to false when not specified")
145+
}
142146
if wopts.MemorySizeInMB != 2048 {
143147
t.Fatalf("expected MemorySizeInMB to default to 2048, got %d", wopts.MemorySizeInMB)
144148
}
@@ -159,6 +163,47 @@ func Test_SpecToUVMCreateOptions_WCOW_Confidential_Defaults(t *testing.T) {
159163
}
160164
}
161165

166+
func Test_SpecToUVMCreateOptions_WCOW_Confidential_WritableEFI_Enabled(t *testing.T) {
167+
s := &specs.Spec{
168+
Windows: &specs.Windows{HyperV: &specs.WindowsHyperV{}},
169+
Annotations: map[string]string{
170+
annotations.WCOWSecurityPolicy: "test-policy",
171+
annotations.WCOWWritableEFI: "true",
172+
},
173+
}
174+
175+
opts, err := SpecToUVMCreateOpts(context.Background(), s, t.Name(), "")
176+
if err != nil {
177+
t.Fatalf("unexpected error generating UVM opts: %v", err)
178+
}
179+
180+
wopts := (opts).(*uvm.OptionsWCOW)
181+
if !wopts.WritableEFI {
182+
t.Fatal("WritableEFI should be true when WCOWWritableEFI annotation is set to true")
183+
}
184+
}
185+
186+
func Test_SpecToUVMCreateOptions_WCOW_NonConfidential_WritableEFI_Enabled(t *testing.T) {
187+
s := &specs.Spec{
188+
Windows: &specs.Windows{HyperV: &specs.WindowsHyperV{}},
189+
Annotations: map[string]string{
190+
// No WCOWSecurityPolicy means non-confidential path
191+
annotations.WCOWWritableEFI: "true",
192+
},
193+
}
194+
195+
opts, err := SpecToUVMCreateOpts(context.Background(), s, t.Name(), "")
196+
if err != nil {
197+
t.Fatalf("unexpected error generating UVM opts: %v", err)
198+
}
199+
200+
wopts := (opts).(*uvm.OptionsWCOW)
201+
// WritableEFI should be respected for non-confidential as well
202+
if !wopts.WritableEFI {
203+
t.Fatal("WritableEFI should be true for non-confidential when WCOWWritableEFI is set")
204+
}
205+
}
206+
162207
func Test_SpecToUVMCreateOptions_WCOW_Confidential_ErrorOnLowMemory(t *testing.T) {
163208
s := &specs.Spec{
164209
Windows: &specs.Windows{HyperV: &specs.WindowsHyperV{}},
@@ -191,13 +236,13 @@ func Test_SpecToUVMCreateOptions_WCOW_Confidential_Overrides(t *testing.T) {
191236
s := &specs.Spec{
192237
Windows: &specs.Windows{HyperV: &specs.WindowsHyperV{}},
193238
Annotations: map[string]string{
194-
annotations.WCOWSecurityPolicy: "test-policy",
195-
annotations.MemorySizeInMB: "4096",
196-
annotations.AllowOvercommit: "false",
239+
annotations.WCOWSecurityPolicy: "test-policy",
240+
annotations.MemorySizeInMB: "4096",
241+
annotations.AllowOvercommit: "false",
197242
annotations.WCOWSecurityPolicyEnforcer: "rego",
198-
annotations.WCOWDisableSecureBoot: "true",
199-
annotations.WCOWGuestStateFile: "C:\\custom\\cwcow.vmgs",
200-
annotations.WCOWIsolationType: "VirtualizationBasedSecurity",
243+
annotations.WCOWDisableSecureBoot: "true",
244+
annotations.WCOWGuestStateFile: "C:\\custom\\cwcow.vmgs",
245+
annotations.WCOWIsolationType: "VirtualizationBasedSecurity",
201246
},
202247
}
203248

internal/uvm/create_wcow.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ConfidentialWCOWOptions struct {
4141
IsolationType string
4242
DisableSecureBoot bool
4343
FirmwareParameters string
44+
WritableEFI bool
4445
}
4546

4647
// OptionsWCOW are the set of options passed to CreateWCOW() to create a utility vm.
@@ -384,26 +385,37 @@ func prepareSecurityConfigDoc(ctx context.Context, uvm *UtilityVM, opts *Options
384385
Minor: 0,
385386
}
386387

388+
// TODO(ambarve): only scratch VHD is unique per VM, EFI & Boot CIM VHDs are
389+
// shared across UVMs, so we don't need to assign VM group access to them every
390+
// time. It should have been done once while deploying the package.
391+
if err := wclayer.GrantVmAccess(ctx, uvm.id, opts.BootFiles.BlockCIMFiles.EFIVHDPath); err != nil {
392+
return nil, errors.Wrap(err, "failed to grant vm access to EFI VHD")
393+
}
394+
387395
if err := wclayer.GrantVmAccess(ctx, uvm.id, opts.BootFiles.BlockCIMFiles.BootCIMVHDPath); err != nil {
388-
return nil, errors.Wrap(err, "failed to grant vm access to boot CIM VHD")
396+
return nil, errors.Wrap(err, "failed to grant vm access to Boot CIM VHD")
389397
}
390398

391-
if err := wclayer.GrantVmAccess(ctx, uvm.id, opts.BootFiles.BlockCIMFiles.EFIVHDPath); err != nil {
392-
return nil, errors.Wrap(err, "failed to grant vm access to EFI VHD")
399+
if err := wclayer.GrantVmAccess(ctx, uvm.id, opts.GuestStateFilePath); err != nil {
400+
return nil, errors.Wrap(err, "failed to grant vm access to guest state file")
393401
}
394402

395403
if err := wclayer.GrantVmAccess(ctx, uvm.id, opts.BootFiles.BlockCIMFiles.ScratchVHDPath); err != nil {
396404
return nil, errors.Wrap(err, "failed to grant vm access to scratch VHD")
397405
}
398406

407+
// boot depends on scratch being attached at LUN 0, it MUST ALWAYS remain at LUN 0
399408
doc.VirtualMachine.Devices.Scsi[guestrequest.ScsiControllerGuids[0]].Attachments["0"] = hcsschema.Attachment{
400409
Path: opts.BootFiles.BlockCIMFiles.ScratchVHDPath,
401410
Type_: "VirtualDisk",
402411
}
412+
403413
doc.VirtualMachine.Devices.Scsi[guestrequest.ScsiControllerGuids[0]].Attachments["1"] = hcsschema.Attachment{
404-
Path: opts.BootFiles.BlockCIMFiles.EFIVHDPath,
405-
Type_: "VirtualDisk",
414+
Path: opts.BootFiles.BlockCIMFiles.EFIVHDPath,
415+
Type_: "VirtualDisk",
416+
ReadOnly: !opts.WritableEFI,
406417
}
418+
407419
doc.VirtualMachine.Devices.Scsi[guestrequest.ScsiControllerGuids[0]].Attachments["2"] = hcsschema.Attachment{
408420
Path: opts.BootFiles.BlockCIMFiles.BootCIMVHDPath,
409421
Type_: "VirtualDisk",

pkg/annotations/annotations.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ const (
205205
// Allows disabling secure boot for testing and debugging scenarios, secure boot doesn't apply to confidential LCOW so
206206
// this is a WCOW only config
207207
WCOWDisableSecureBoot = "io.microsoft.virtualmachine.wcow.no_secure_boot"
208+
209+
// Attaches the EFI/boot VHD in the writable mode (instead of the default read-only mode). This is usually required
210+
// when debugging boot to capture bootstat traces.
211+
WCOWWritableEFI = "io.microsoft.virtualmachine.wcow.writable_efi"
208212
)
209213

210214
// WCOW host process container annotations.

0 commit comments

Comments
 (0)