Skip to content

Commit 5b3d5b3

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 b24c4ed commit 5b3d5b3

8 files changed

Lines changed: 160 additions & 30 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
@@ -46,6 +46,7 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf
4646
var err error
4747
callbackFuncs := machine.CleanUp()
4848
defer callbackFuncs.CleanIfErr(&err)
49+
callbackFuncs.Add(createErrorLogCallback(&err))
4950
go callbackFuncs.CleanOnSignal()
5051

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

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

@@ -237,6 +252,35 @@ func (h HyperVStubber) canCreate() error {
237252
return ErrHypervUserNotInAdminGroup
238253
}
239254

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

373417
callbackFuncs := machine.CleanUp()
374418
defer callbackFuncs.CleanIfErr(&err)
419+
callbackFuncs.Add(createErrorLogCallback(&err))
375420
go callbackFuncs.CleanOnSignal()
376421

377422
if mc.IsFirstBoot() {
@@ -547,6 +592,11 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, _ *ignition.
547592
readySock, err := vsock.LoadHVSockRegistryEntryByPurpose(vsock.Events)
548593
if err != nil {
549594
if !windows.HasAdminRights() {
595+
if !windows.IsReExecuting() && windows.IsInAdministratorsGroup() {
596+
message := "Initializing the first Hyper-V machine requires admin rights to set up the Windows Registry.\n\n" +
597+
windows.UACConfirmationPrompt
598+
return nil, launchElevate(message)
599+
}
550600
return nil, ErrHypervRegistryInitRequiresElevation
551601
}
552602
readySock, err = vsock.NewHVSockRegistryEntry(vsock.Events)

pkg/machine/shim/host.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
7575
)
7676

7777
callbackFuncs := machine.CleanUp()
78-
defer callbackFuncs.CleanIfErr(&err)
78+
defer func() {
79+
// Do not clean up on relaunch: the elevated child process
80+
// completed init successfully and is using the resources
81+
// (e.g. the disk image) that cleanup would remove.
82+
if !errors.Is(err, machineDefine.ErrRelaunchAttempt) {
83+
callbackFuncs.CleanIfErr(&err)
84+
}
85+
}()
7986
go callbackFuncs.CleanOnSignal()
8087

8188
dirs, err := env.GetMachineDirs(mp.VMType())
@@ -159,17 +166,20 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
159166
}
160167
mc.ImagePath = imagePath
161168

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

175185
callbackFuncs.Add(mc.ImagePath.Delete)
@@ -460,8 +470,10 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St
460470
if err != nil {
461471
return err
462472
}
463-
mc.Lock()
464-
defer mc.Unlock()
473+
if !opts.ReExec {
474+
mc.Lock()
475+
defer mc.Unlock()
476+
}
465477
if err := mc.Refresh(); err != nil {
466478
return fmt.Errorf("reload config: %w", err)
467479
}
@@ -732,8 +744,10 @@ func Remove(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.R
732744
if err != nil {
733745
return err
734746
}
735-
mc.Lock()
736-
defer mc.Unlock()
747+
if !opts.ReExec {
748+
mc.Lock()
749+
defer mc.Unlock()
750+
}
737751
if err := mc.Refresh(); err != nil {
738752
return fmt.Errorf("reload config: %w", err)
739753
}

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)