Skip to content

Commit 958a6e9

Browse files
committed
propose running init/rm command on hyperv machine in elevated mode
This commit adds automatic UAC elevation prompts for HyperV machine init/rm actions when administrator privileges are required. Previously, users had to manually run Podman as administrator when creating the first machine or removing the last machine, which requires Windows Registry modifications. When the HyperV command gets relaunched as elevated, the error of the elevated process is saved on a file to be displayed by the caller. The implementation is the same as that used by WSL. Signed-off-by: lstocchi <lstocchi@redhat.com>
1 parent e30d693 commit 958a6e9

8 files changed

Lines changed: 152 additions & 29 deletions

File tree

cmd/podman/machine/init.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,13 @@ func initMachine(cmd *cobra.Command, args []string) error {
287287
// Examples:
288288
// - a user has chosen to perform their own reboot
289289
// - reexec for limited admin operations, returning to parent
290-
if errors.Is(err, define.ErrInitRelaunchAttempt) {
290+
if errors.Is(err, define.ErrRelaunchAttempt) {
291+
fmt.Println("Machine init complete")
292+
if now {
293+
fmt.Printf("Machine %q started successfully\n", initOpts.Name)
294+
return nil
295+
}
296+
printStartCommand(initOpts.Name)
291297
return nil
292298
}
293299
return err
@@ -297,15 +303,21 @@ func initMachine(cmd *cobra.Command, args []string) error {
297303
fmt.Println("Machine init complete")
298304

299305
if now {
306+
// Pass reexec flag from init to start
307+
startOpts.ReExec = initOpts.ReExec
300308
return start(cmd, args)
301309
}
302310

311+
printStartCommand(initOpts.Name)
312+
return err
313+
}
314+
315+
func printStartCommand(machineName string) {
303316
extra := ""
304-
if initOpts.Name != defaultMachineName {
305-
extra = " " + initOpts.Name
317+
if machineName != defaultMachineName {
318+
extra = " " + machineName
306319
}
307320
fmt.Printf("To start your machine run:\n\n\tpodman machine start%s\n\n", extra)
308-
return err
309321
}
310322

311323
// checkMaxMemory gets the total system memory and compares it to the variable. if the variable

cmd/podman/machine/rm.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
package machine
44

55
import (
6+
"errors"
7+
68
"github.com/containers/podman/v6/cmd/podman/registry"
79
"github.com/containers/podman/v6/libpod/events"
810
"github.com/containers/podman/v6/pkg/machine"
11+
"github.com/containers/podman/v6/pkg/machine/define"
912
"github.com/containers/podman/v6/pkg/machine/shim"
1013
"github.com/spf13/cobra"
1114
)
@@ -38,6 +41,13 @@ func init() {
3841

3942
imageFlagName := "save-image"
4043
flags.BoolVar(&destroyOptions.SaveImage, imageFlagName, false, "Do not delete the image file")
44+
45+
flags.BoolVar(
46+
&destroyOptions.ReExec,
47+
"reexec", false,
48+
"process was rexeced",
49+
)
50+
_ = flags.MarkHidden("reexec")
4151
}
4252

4353
func rm(_ *cobra.Command, args []string) error {
@@ -53,6 +63,13 @@ func rm(_ *cobra.Command, args []string) error {
5363
}
5464

5565
if err := shim.Remove(mc, vmProvider, destroyOptions); err != nil {
66+
// The removal is partially complete and podman should
67+
// exit gracefully with no error and no success message.
68+
// Examples:
69+
// - reexec for limited admin operations, returning to parent
70+
if errors.Is(err, define.ErrRelaunchAttempt) {
71+
return nil
72+
}
5673
return err
5774
}
5875
newMachineEvent(events.Remove, events.Event{Name: vmName})

pkg/machine/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type SSHOptions struct {
6666
}
6767

6868
type StartOptions struct {
69+
ReExec bool
6970
NoInfo bool
7071
Quiet bool
7172
Rosetta bool
@@ -74,6 +75,7 @@ type StartOptions struct {
7475
type StopOptions struct{}
7576

7677
type RemoveOptions struct {
78+
ReExec bool
7779
Force bool
7880
SaveImage bool
7981
SaveIgnition bool

pkg/machine/define/errors.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
)
77

88
var (
9-
ErrWrongState = errors.New("VM in wrong state to perform action")
10-
ErrNotImplemented = errors.New("functionality not implemented")
11-
ErrInitRelaunchAttempt = errors.New("stopping execution: 'init' relaunched with --reexec flag to reinitialize the VM")
12-
ErrRebootInitiated = errors.New("system reboot initiated")
9+
ErrWrongState = errors.New("VM in wrong state to perform action")
10+
ErrNotImplemented = errors.New("functionality not implemented")
11+
ErrRelaunchAttempt = errors.New("stopping execution: command relaunched with --reexec flag for elevated privileges")
12+
ErrRebootInitiated = errors.New("system reboot initiated")
1313
)
1414

1515
type ErrVMAlreadyExists struct {

pkg/machine/hyperv/stubber.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf
4545
var err error
4646
callbackFuncs := machine.CleanUp()
4747
defer callbackFuncs.CleanIfErr(&err)
48+
callbackFuncs.Add(createErrorLogCallback(&err))
4849
go callbackFuncs.CleanOnSignal()
4950

5051
hwConfig := hypervctl.HardwareConfig{
@@ -62,6 +63,13 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf
6263
// This is to prevent a non-admin user from creating the first machine
6364
// which would require adding vsock entries into the Windows Registry.
6465
if err := h.canCreate(); err != nil {
66+
// If it returns ErrHypervRegistryInitRequiresElevation and we're not already re-executing,
67+
// offer to elevate automatically if user is in admin group
68+
if err == ErrHypervRegistryInitRequiresElevation && !windows.IsReExecuting() && windows.IsInAdministratorsGroup() {
69+
message := "To initialize the first Hyper-V machine, Podman requires admin rights to set up the Windows Registry.\n\n" +
70+
windows.UACConfirmationPrompt
71+
return launchElevate(message)
72+
}
6573
return err
6674
}
6775

@@ -178,6 +186,13 @@ func (h HyperVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() err
178186
// This is to prevent a non-admin user from deleting the last machine
179187
// which would require removal of vsock entries from the Windows Registry.
180188
if err := h.canRemove(mc); err != nil {
189+
// If we get ErrHypervRegistryRemoveRequiresElevation and we're not already re-executing,
190+
// and the user has admin rights (is in admin group), offer to elevate automatically
191+
if err == ErrHypervRegistryRemoveRequiresElevation && !windows.IsReExecuting() && windows.IsInAdministratorsGroup() {
192+
message := "Removing this Hyper-V machine requires admin rights to clean up the Windows Registry.\n\n" +
193+
windows.UACConfirmationPrompt
194+
return nil, nil, launchElevate(message)
195+
}
181196
return nil, nil, err
182197
}
183198

@@ -236,6 +251,35 @@ func (h HyperVStubber) canCreate() error {
236251
return ErrHypervUserNotInAdminGroup
237252
}
238253

254+
// launchElevate attempts to automatically re-run the command as administrator
255+
// This is similar to how WSL handles elevation.
256+
func launchElevate(message string) error {
257+
if windows.MessageBox(message, "Podman Machine", false) != 1 {
258+
return errors.New("elevation process cancelled by user. Please rerun the command as administrator")
259+
}
260+
err := windows.CreateOrTruncateElevatedOutputFile()
261+
if err != nil {
262+
return err
263+
}
264+
265+
err = windows.RelaunchElevatedWait()
266+
if err != nil {
267+
windows.DumpOutputFile()
268+
return fmt.Errorf("elevated process failed with error: %w", err)
269+
}
270+
return define.ErrRelaunchAttempt
271+
}
272+
273+
// createErrorLogCallback creates a callback function that logs errors to file when --reexec is detected
274+
func createErrorLogCallback(err *error) func() error {
275+
return func() error {
276+
if *err != nil && windows.IsReExecuting() {
277+
windows.LogErrorToFile(*err)
278+
}
279+
return nil
280+
}
281+
}
282+
239283
func isLegacyMachine(mc *vmconfigs.MachineConfig) bool {
240284
return mc.HyperVHypervisor != nil && mc.HyperVHypervisor.ReadyVsock.MachineName != ""
241285
}
@@ -371,6 +415,7 @@ func (h HyperVStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func(
371415

372416
callbackFuncs := machine.CleanUp()
373417
defer callbackFuncs.CleanIfErr(&err)
418+
callbackFuncs.Add(createErrorLogCallback(&err))
374419
go callbackFuncs.CleanOnSignal()
375420

376421
if mc.IsFirstBoot() {
@@ -553,6 +598,11 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, _ *ignition.
553598
readySock, err := vsock.LoadHVSockRegistryEntryByPurpose(vsock.Events)
554599
if err != nil {
555600
if !windows.HasAdminRights() {
601+
if !windows.IsReExecuting() && windows.IsInAdministratorsGroup() {
602+
message := "Initializing the first Hyper-V machine requires admin rights to set up the Windows Registry.\n\n" +
603+
windows.UACConfirmationPrompt
604+
return nil, launchElevate(message)
605+
}
556606
return nil, ErrHypervRegistryInitRequiresElevation
557607
}
558608
readySock, err = vsock.NewHVSockRegistryEntry(vsock.Events)

pkg/machine/shim/host.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,20 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
159159
}
160160
mc.ImagePath = imagePath
161161

162-
// TODO The following stanzas should be re-written in a differeent place. It should have a custom
163-
// parser for our image pulling. It would be nice if init just got an error and mydisk back.
164-
//
165-
// Eventual valid input:
166-
// "" <- means take the default
167-
// "http|https://path"
168-
// "/path
169-
// "docker://quay.io/something/someManifest
170-
171-
if err := diskpull.GetDisk(opts.Image, dirs, mc.ImagePath, mp.VMType(), mc.Name, opts.SkipTlsVerify); err != nil {
172-
return err
162+
// If the process was re-executed with elevation, the image has already been pulled
163+
// in the parent process, so skip disk pulling here.
164+
if !opts.ReExec {
165+
// TODO The following stanzas should be re-written in a differeent place. It should have a custom
166+
// parser for our image pulling. It would be nice if init just got an error and mydisk back.
167+
//
168+
// Eventual valid input:
169+
// "" <- means take the default
170+
// "http|https://path"
171+
// "/path
172+
// "docker://quay.io/something/someManifest
173+
if err := diskpull.GetDisk(opts.Image, dirs, mc.ImagePath, mp.VMType(), mc.Name, opts.SkipTlsVerify); err != nil {
174+
return err
175+
}
173176
}
174177

175178
callbackFuncs.Add(mc.ImagePath.Delete)
@@ -460,8 +463,10 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St
460463
if err != nil {
461464
return err
462465
}
463-
mc.Lock()
464-
defer mc.Unlock()
466+
if !opts.ReExec {
467+
mc.Lock()
468+
defer mc.Unlock()
469+
}
465470
if err := mc.Refresh(); err != nil {
466471
return fmt.Errorf("reload config: %w", err)
467472
}
@@ -732,8 +737,10 @@ func Remove(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.R
732737
if err != nil {
733738
return err
734739
}
735-
mc.Lock()
736-
defer mc.Unlock()
740+
if !opts.ReExec {
741+
mc.Lock()
742+
defer mc.Unlock()
743+
}
737744
if err := mc.Refresh(); err != nil {
738745
return fmt.Errorf("reload config: %w", err)
739746
}

pkg/machine/windows/util_windows.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"os/user"
89
"path/filepath"
10+
"slices"
911
"strings"
1012
"syscall"
1113
"unsafe"
@@ -15,6 +17,27 @@ import (
1517
"golang.org/x/sys/windows"
1618
)
1719

20+
const UACConfirmationPrompt = "Since you are not running as admin, a new window will open and " +
21+
"require you to approve administrator privileges.\n\n"
22+
23+
// IsInAdministratorsGroup checks if the current user is a member of the Administrators group,
24+
// regardless of whether the current process is elevated. This can be used to determine
25+
// if the user can elevate privileges via UAC.
26+
func IsInAdministratorsGroup() bool {
27+
u, err := user.Current()
28+
if nil != err {
29+
return false
30+
}
31+
ids, err := u.GroupIds()
32+
if nil != err {
33+
return false
34+
}
35+
// S-1-5-32-544 is the SID for the Administrators group
36+
// see: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
37+
return slices.Contains(ids, "S-1-5-32-544")
38+
}
39+
40+
// HasAdminRights checks if the current process has administrator privileges.
1841
func HasAdminRights() bool {
1942
var sid *windows.SID
2043

@@ -249,3 +272,16 @@ func DumpOutputFile() {
249272
defer file.Close()
250273
_, _ = io.Copy(os.Stdout, file)
251274
}
275+
276+
// LogErrorToFile logs an error message to the elevated output log file if --reexec is detected
277+
func LogErrorToFile(err error) {
278+
if err == nil {
279+
return
280+
}
281+
file, fileErr := GetElevatedOutputFileWrite()
282+
if fileErr != nil {
283+
return
284+
}
285+
defer file.Close()
286+
_, _ = fmt.Fprintf(file, "Error: %v\n", err)
287+
}

pkg/machine/wsl/machine.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,15 +316,14 @@ func attemptFeatureInstall(reExec, admin bool) error {
316316
message := "WSL is not installed on this system, installing it.\n\n"
317317

318318
if !admin {
319-
message += "Since you are not running as admin, a new window will open and " +
320-
"require you to approve administrator privileges.\n\n"
319+
message += winutil.UACConfirmationPrompt
321320
}
322321

323322
message += "NOTE: A system reboot will be required as part of this process. " +
324323
"If you prefer, you may abort now, and perform a manual installation using the \"wsl --install\" command."
325324

326325
if !reExec && winutil.MessageBox(message, "Podman Machine", false) != 1 {
327-
return fmt.Errorf("the WSL installation aborted: %w", define.ErrInitRelaunchAttempt)
326+
return fmt.Errorf("the WSL installation aborted: %w", define.ErrRelaunchAttempt)
328327
}
329328

330329
if !reExec && !admin {
@@ -342,16 +341,16 @@ func launchElevate(operation string) error {
342341
if eerr, ok := err.(*winutil.ExitCodeError); ok {
343342
if eerr.Code == ErrorSuccessRebootRequired {
344343
fmt.Println("Reboot is required to continue installation, please reboot at your convenience")
345-
return define.ErrInitRelaunchAttempt
344+
return define.ErrRelaunchAttempt
346345
}
347346
}
348347

349348
fmt.Fprintf(os.Stderr, "Elevated process failed with error: %v\n\n", err)
350349
winutil.DumpOutputFile()
351350
fmt.Fprintf(os.Stderr, wslInstallError, operation)
352-
return fmt.Errorf("%w: %w", err, define.ErrInitRelaunchAttempt)
351+
return fmt.Errorf("%w: %w", err, define.ErrRelaunchAttempt)
353352
}
354-
return define.ErrInitRelaunchAttempt
353+
return define.ErrRelaunchAttempt
355354
}
356355

357356
func installWsl() error {

0 commit comments

Comments
 (0)