diff --git a/pkg/machine/hyperv/hutil.go b/pkg/machine/hyperv/hutil.go new file mode 100644 index 0000000000..65c6d73c83 --- /dev/null +++ b/pkg/machine/hyperv/hutil.go @@ -0,0 +1,38 @@ +//go:build windows + +package hyperv + +import ( + "errors" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +var ( + ErrHypervUserNotInAdminGroup = errors.New("Hyper-V machines require Hyper-V admin rights to be managed. Please add the current user to the Hyper-V Administrators group or run Podman as an administrator") + ErrHypervRegistryInitRequiresElevation = errors.New("the first time Podman initializes a Hyper-V machine, it requires admin rights. Please run Podman as an administrator") + ErrHypervRegistryRemoveRequiresElevation = errors.New("removing this Hyper-V machine requires admin rights to clean up the Windows Registry. Please run Podman as an administrator") + ErrHypervRegistryUpdateRequiresElevation = errors.New("this machine's configuration requires additional Hyper-V networking (hvsock) entries in the Windows Registry. Please run Podman as an administrator") +) + +func HasHyperVAdminRights() bool { + sid, err := windows.CreateWellKnownSid(windows.WinBuiltinHyperVAdminsSid) + if err != nil { + return false + } + + // From MS docs: + // "If TokenHandle is NULL, CheckTokenMembership uses the impersonation + // token of the calling thread. If the thread is not impersonating, + // the function duplicates the thread's primary token to create an + // impersonation token." + token := windows.Token(0) + member, err := token.IsMember(sid) + if err != nil { + logrus.Warnf("Token Membership Error: %s", err) + return false + } + + return member +} diff --git a/pkg/machine/hyperv/stubber.go b/pkg/machine/hyperv/stubber.go index b0a2a51fad..5d17e19d2f 100644 --- a/pkg/machine/hyperv/stubber.go +++ b/pkg/machine/hyperv/stubber.go @@ -19,6 +19,7 @@ import ( "github.com/containers/podman/v6/pkg/machine/hyperv/vsock" "github.com/containers/podman/v6/pkg/machine/ignition" "github.com/containers/podman/v6/pkg/machine/vmconfigs" + "github.com/containers/podman/v6/pkg/machine/windows" "github.com/containers/podman/v6/pkg/systemd/parser" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/strongunits" @@ -53,11 +54,56 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf Memory: uint64(mc.Resources.Memory), } - networkHVSock, err := vsock.NewHVSockRegistryEntry(mc.Name, vsock.Network) + // Allow creation in these two cases: + // 1. if the user is Admin + // 2. if the user has Hyper-V admin rights and there is at least one *NEW* machine. + // *NEW* machines are those created with the vsock entry having the ToolName field. + // + // This is to prevent a non-admin user from creating the first machine + // which would require adding vsock entries into the Windows Registry. + if err := h.canCreate(); err != nil { + return err + } + + // count number of existing machines, used later to determine if Registry should be cleaned over a failure + machines, err := h.countMachinesWithToolname() if err != nil { return err } + // Callback to remove any created vsock entries in the Windows Registry if the creation fails + removeRegistryEntriesCallBack := func() error { + // Allow removal only if user is Admin and this is the first machine created. + // If there are already existing machines, the vsock entries should remain. + // + // There is no need to check for admin rights here as this is already a requirement + // to create the first machine and so it would have failed earlier. + if machines > 0 { + return nil + } + + if err := vsock.RemoveAllHVSockRegistryEntries(); err != nil { + return fmt.Errorf("unable to remove hvsock registry entries: %q", err) + } + + return nil + } + callbackFuncs.Add(removeRegistryEntriesCallBack) + + // Attempt to load an existing HVSock registry entry for networking. + // If no existing entry is found, create a new one. + // Creating a new entry requires administrative rights. + networkHVSock, err := vsock.LoadHVSockRegistryEntryByPurpose(vsock.Network) + if err != nil { + if !windows.HasAdminRights() { + return ErrHypervRegistryInitRequiresElevation + } + networkHVSock, err = vsock.NewHVSockRegistryEntry(vsock.Network) + if err != nil { + return err + } + } + mc.HyperVHypervisor.NetworkVSock = *networkHVSock // Add vsock port numbers to mounts @@ -66,17 +112,6 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf return err } - removeShareCallBack := func() error { - return removeShares(mc) - } - callbackFuncs.Add(removeShareCallBack) - - removeRegistrySockets := func() error { - removeNetworkAndReadySocketsFromRegistry(mc) - return nil - } - callbackFuncs.Add(removeRegistrySockets) - netUnitFile, err := createNetworkUnit(mc.HyperVHypervisor.NetworkVSock.Port) if err != nil { return err @@ -135,15 +170,23 @@ func (h HyperVStubber) MountVolumesToVM(_ *vmconfigs.MachineConfig, _ bool) erro } func (h HyperVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, error) { + // Allow removal in these two cases: + // 1. if the user is Admin + // 2. if the user has Hyper-V admin rights and there are 2+ *NEW* machines. + // *NEW* machines are those created with the vsock entry having the ToolName field. + // + // This is to prevent a non-admin user from deleting the last machine + // which would require removal of vsock entries from the Windows Registry. + if err := h.canRemove(); err != nil { + return nil, nil, err + } + _, vm, err := GetVMFromMC(mc) if err != nil { return nil, nil, err } rmFunc := func() error { - // Tear down vsocks - removeNetworkAndReadySocketsFromRegistry(mc) - // Remove ignition registry entries - not a fatal error // for vm removal // TODO we could improve this by recommending an action be done @@ -152,11 +195,93 @@ func (h HyperVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() err } // disk path removal is done by generic remove - return vm.Remove("") + if err = vm.Remove(""); err != nil { + return err + } + + // remove vsock registry entries + if err := h.removeHvSockFromRegistry(); err != nil { + logrus.Errorf("unable to remove hvsock registry entries: %q", err) + } + + return nil } return []string{}, rmFunc, nil } +func (h HyperVStubber) canCreate() error { + // if admin, can always create + if windows.HasAdminRights() { + return nil + } + // not admin, check if there is at least one existing machine + // if so, user could create more machines just by being member of the hyperv admin group + machines, err := h.countMachinesWithToolname() + if err != nil { + return err + } + // no existing machines, require to be admin + if machines == 0 { + return ErrHypervRegistryInitRequiresElevation + } + // at least 1 machine exists + // if user is member of the hyperv admin group, allow creation + if HasHyperVAdminRights() { + return nil + } + return ErrHypervUserNotInAdminGroup +} + +func (h HyperVStubber) canRemove() error { + // if admin, can always remove + if windows.HasAdminRights() { + return nil + } + // not admin, check if there are multiple machines + // if so, user could remove the machine just by being member of the hyperv admin group + // (only the last machine removal requires Registry changes) + machines, err := h.countMachinesWithToolname() + if err != nil { + return err + } + // more than 1 machine exists, allow removal if user has hyperv admin rights + if machines > 1 && HasHyperVAdminRights() { + return nil + } + return ErrHypervRegistryRemoveRequiresElevation +} + +// countMachinesWithToolname counts only machines that have a toolname field with value "podman". +func (h HyperVStubber) countMachinesWithToolname() (int, error) { + dirs, err := env.GetMachineDirs(h.VMType()) + if err != nil { + return 0, err + } + mcs, err := vmconfigs.LoadMachinesInDir(dirs) + if err != nil { + return 0, err + } + count := 0 + for _, mc := range mcs { + if mc.HyperVHypervisor != nil && mc.HyperVHypervisor.ReadyVsock.ToolName == "podman" { + count++ + } + } + return count, nil +} + +func (h HyperVStubber) removeHvSockFromRegistry() error { + // Remove hvsock registry entries only if this is the last machine + machines, err := h.countMachinesWithToolname() + if err != nil { + return err + } + if machines > 1 { + return nil + } + return vsock.RemoveAllHVSockRegistryEntries() +} + func (h HyperVStubber) RemoveAndCleanMachines(_ *define.MachineDirs) error { return nil } @@ -347,9 +472,19 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, _ *ignition. // simply be derived. So we create the HyperVConfig here. mc.HyperVHypervisor = new(vmconfigs.HyperVConfig) var ignOpts ignition.ReadyUnitOpts - readySock, err := vsock.NewHVSockRegistryEntry(mc.Name, vsock.Events) + + // Attempt to load an existing HVSock registry entry for events. + // If no existing entry is found, create a new one. + // Creating a new entry requires administrative rights. + readySock, err := vsock.LoadHVSockRegistryEntryByPurpose(vsock.Events) if err != nil { - return nil, err + if !windows.HasAdminRights() { + return nil, ErrHypervRegistryInitRequiresElevation + } + readySock, err = vsock.NewHVSockRegistryEntry(vsock.Events) + if err != nil { + return nil, err + } } // TODO Stopped here ... fails bc mc.Hypervisor is nil ... this can be nil checked prior and created @@ -451,20 +586,6 @@ func resizeDisk(newSize strongunits.GiB, imagePath *define.VMFile) error { return nil } -// removeNetworkAndReadySocketsFromRegistry removes the Network and Ready sockets -// from the Windows Registry -func removeNetworkAndReadySocketsFromRegistry(mc *vmconfigs.MachineConfig) { - // Remove the HVSOCK for networking - if err := mc.HyperVHypervisor.NetworkVSock.Remove(); err != nil { - logrus.Errorf("unable to remove registry entry for %s: %q", mc.HyperVHypervisor.NetworkVSock.KeyName, err) - } - - // Remove the HVSOCK for events - if err := mc.HyperVHypervisor.ReadyVsock.Remove(); err != nil { - logrus.Errorf("unable to remove registry entry for %s: %q", mc.HyperVHypervisor.ReadyVsock.KeyName, err) - } -} - // readAndSplitIgnition reads the ignition file and splits it into key:value pairs func readAndSplitIgnition(mc *vmconfigs.MachineConfig, vm *hypervctl.VirtualMachine) error { ignFile, err := mc.IgnitionFile() diff --git a/pkg/machine/hyperv/volumes.go b/pkg/machine/hyperv/volumes.go index 61132868d5..93987fd5aa 100644 --- a/pkg/machine/hyperv/volumes.go +++ b/pkg/machine/hyperv/volumes.go @@ -12,35 +12,10 @@ import ( "github.com/containers/podman/v6/pkg/machine" "github.com/containers/podman/v6/pkg/machine/hyperv/vsock" "github.com/containers/podman/v6/pkg/machine/vmconfigs" + "github.com/containers/podman/v6/pkg/machine/windows" "github.com/sirupsen/logrus" ) -func removeShares(mc *vmconfigs.MachineConfig) error { - var removalErr error - - for _, mount := range mc.Mounts { - if mount.VSockNumber == nil { - // nothing to do if the vsock number was never defined - continue - } - - vsockReg, err := vsock.LoadHVSockRegistryEntry(*mount.VSockNumber) - if err != nil { - logrus.Debugf("Vsock %d for mountpoint %s does not have a valid registry entry, skipping removal", *mount.VSockNumber, mount.Target) - continue - } - - if err := vsockReg.Remove(); err != nil { - if removalErr != nil { - logrus.Errorf("Error removing vsock: %v", removalErr) - } - removalErr = fmt.Errorf("removing vsock %d for mountpoint %s: %w", *mount.VSockNumber, mount.Target, err) - } - } - - return removalErr -} - func startShares(mc *vmconfigs.MachineConfig) error { for _, mount := range mc.Mounts { var args []string @@ -72,11 +47,31 @@ func startShares(mc *vmconfigs.MachineConfig) error { } func createShares(mc *vmconfigs.MachineConfig) (err error) { - for _, mount := range mc.Mounts { - testVsock, err := vsock.NewHVSockRegistryEntry(mc.Name, vsock.Fileserver) - if err != nil { - return err + fileServerVsocks, err := vsock.LoadAllHVSockRegistryEntriesByPurpose(vsock.Fileserver) + if err != nil { + return fmt.Errorf("failed to load existing file server vsock registry entries: %w", err) + } + for i, mount := range mc.Mounts { + var testVsock *vsock.HVSockRegistryEntry + + // Check if there's an existing file server vsock entry that can be reused for the current mount. + if i < len(fileServerVsocks) { + testVsock = fileServerVsocks[i] + } else { + // If no existing vsock entry can be reused, a new one must be created. + // Creating a new HVSockRegistryEntry requires administrator privileges. + if !windows.HasAdminRights() { + if i == 0 { + return ErrHypervRegistryInitRequiresElevation + } + return ErrHypervRegistryUpdateRequiresElevation + } + testVsock, err = vsock.NewHVSockRegistryEntry(vsock.Fileserver) + if err != nil { + return err + } } + mount.VSockNumber = &testVsock.Port logrus.Debugf("Going to share directory %s via 9p on vsock %d", mount.Source, testVsock.Port) } diff --git a/pkg/machine/hyperv/vsock/vsock.go b/pkg/machine/hyperv/vsock/vsock.go index ed0547e008..0be6ca2df5 100644 --- a/pkg/machine/hyperv/vsock/vsock.go +++ b/pkg/machine/hyperv/vsock/vsock.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "strconv" "strings" "github.com/Microsoft/go-winio" @@ -21,6 +22,9 @@ var ErrVSockRegistryEntryExists = errors.New("registry entry already exists") const ( // HvsockMachineName is the string identifier for the machine name in a registry entry HvsockMachineName = "MachineName" + // HvsockToolName is the string identifier for the tool name in a registry entry + HvsockToolName = "ToolName" + PodmanToolName = "podman" // HvsockPurpose is the string identifier for the sock purpose in a registry entry HvsockPurpose = "Purpose" // VsockRegistryPath describes the registry path to where the hvsock registry entries live @@ -75,11 +79,19 @@ func openVSockRegistryEntry(entry string) (registry.Key, error) { // HVSockRegistryEntry describes a registry entry used in Windows for HVSOCK implementations type HVSockRegistryEntry struct { - KeyName string `json:"key_name"` - Purpose HVSockPurpose `json:"purpose"` - Port uint64 `json:"port"` - MachineName string `json:"machineName"` - Key registry.Key `json:"key,omitempty"` + KeyName string `json:"key_name"` + Purpose HVSockPurpose `json:"purpose"` + Port uint64 `json:"port"` + + // MachineName is deprecated. + // Registry entries are now shared across machines, so a machine-specific identifier isn't appropriate here. + MachineName string `json:"machineName,omitempty"` + + // ToolName identifies the application that created this registry entry (e.g., Podman). + // This provides information about the entry's origin and can be used for filter entries + // if purpose is not enough. + ToolName string `json:"creator_tool,omitempty"` + Key registry.Key `json:"key,omitempty"` } // Add creates a new Windows registry entry with string values from the @@ -117,7 +129,7 @@ func (hv *HVSockRegistryEntry) Add() error { if err := newKey.SetStringValue(HvsockPurpose, hv.Purpose.string()); err != nil { return err } - return newKey.SetStringValue(HvsockMachineName, hv.MachineName) + return newKey.SetStringValue(HvsockToolName, hv.ToolName) } // Remove deletes the registry key and its string values @@ -136,8 +148,8 @@ func (hv *HVSockRegistryEntry) validate() error { if len(hv.Purpose.string()) < 1 { return errors.New("required field purpose is empty") } - if len(hv.MachineName) < 1 { - return errors.New("required field machinename is empty") + if len(hv.ToolName) < 1 { + return errors.New("required field toolName is empty") } if len(hv.KeyName) < 1 { return errors.New("required field keypath is empty") @@ -188,7 +200,7 @@ func findOpenHVSockPort() (uint64, error) { // NewHVSockRegistryEntry is a constructor to make a new registry entry in Windows. After making the new // object, you must call the add() method to *actually* add it to the Windows registry. -func NewHVSockRegistryEntry(machineName string, purpose HVSockPurpose) (*HVSockRegistryEntry, error) { +func NewHVSockRegistryEntry(purpose HVSockPurpose) (*HVSockRegistryEntry, error) { // a so-called wildcard entry ... everything from FACB -> 6D3 is MS special sauce // for a " linux vm". this first segment is hexi for the hvsock port number // 00000400-FACB-11E6-BD58-64006A7986D3 @@ -197,10 +209,10 @@ func NewHVSockRegistryEntry(machineName string, purpose HVSockPurpose) (*HVSockR return nil, err } r := HVSockRegistryEntry{ - KeyName: portToKeyName(port), - Purpose: purpose, - Port: port, - MachineName: machineName, + KeyName: portToKeyName(port), + Purpose: purpose, + Port: port, + ToolName: PodmanToolName, } if err := r.Add(); err != nil { return nil, err @@ -232,16 +244,11 @@ func LoadHVSockRegistryEntry(port uint64) (*HVSockRegistryEntry, error) { return nil, err } - machineName, _, err := k.GetStringValue(HvsockMachineName) - if err != nil { - return nil, err - } return &HVSockRegistryEntry{ - KeyName: keyName, - Purpose: purpose, - Port: port, - MachineName: machineName, - Key: k, + KeyName: keyName, + Purpose: purpose, + Port: port, + Key: k, }, nil } @@ -274,3 +281,157 @@ func (hv *HVSockRegistryEntry) ListenSetupWait() (func() error, io.Closer, error return <-errChan }, listener, nil } + +// loadAllHVSockRegistryEntries loads HVSock registry entries, filtered by purpose and optionally limited by size. +// If limit is -1, it returns all matching entries. Otherwise, it returns up to 'limit' entries. +// The caller is responsible for closing the registry.Key in each returned HVSockRegistryEntry. +// Non-matching or excess keys are closed within this function. +func loadHVSockRegistryEntries(purpose HVSockPurpose, limit int) ([]*HVSockRegistryEntry, error) { + parentKey, err := registry.OpenKey(registry.LOCAL_MACHINE, VsockRegistryPath, registry.ENUMERATE_SUB_KEYS) + if err != nil { + logrus.Errorf("failed to open registry key: %s: %v", VsockRegistryPath, err) + return nil, err + } + defer func() { + if err := parentKey.Close(); err != nil { + logrus.Errorf("failed to close registry key: %v", err) + } + }() + + subKeyNames, err := parentKey.ReadSubKeyNames(-1) + if err != nil { + logrus.Errorf("failed to read subkey names from %s: %v", VsockRegistryPath, err) + return nil, err + } + + allEntries := []*HVSockRegistryEntry{} + for _, subKeyName := range subKeyNames { + if limit != -1 && len(allEntries) >= limit { + break + } + + fqPath := fmt.Sprintf("%s\\%s", VsockRegistryPath, subKeyName) + k, err := openVSockRegistryEntry(fqPath) + if err != nil { + logrus.Debugf("Could not open registry entry %s: %v", fqPath, err) + continue + } + + p, _, err := k.GetStringValue(HvsockPurpose) + if err != nil { + logrus.Debugf("Could not read purpose from registry entry %s: %v", fqPath, err) + k.Close() + continue + } + + toolName, _, err := k.GetStringValue(HvsockToolName) + if err != nil { + logrus.Debugf("Could not read tool name from registry entry %s: %v", fqPath, err) + k.Close() + continue + } + + k.Close() + + entryPurpose, err := toHVSockPurpose(p) + if err != nil { + logrus.Debugf("Could not convert purpose string %q for entry %s: %v", p, fqPath, err) + continue + } + + if !entryPurpose.Equal(purpose.string()) { + continue + } + + if toolName != PodmanToolName { + continue + } + + parts := strings.Split(subKeyName, "-") + if len(parts) == 0 { + logrus.Debugf("Malformed key name %s: cannot extract port", subKeyName) + continue + } + + portHex := parts[0] + port, err := parseHexToUint64(portHex) + if err != nil { + logrus.Debugf("Could not parse port from key name %s: %v", subKeyName, err) + continue + } + + allEntries = append(allEntries, &HVSockRegistryEntry{ + KeyName: subKeyName, + Purpose: entryPurpose, + Port: port, + Key: k, + ToolName: PodmanToolName, + }) + } + + return allEntries, nil +} + +func LoadHVSockRegistryEntryByPurpose(purpose HVSockPurpose) (*HVSockRegistryEntry, error) { + entries, err := loadHVSockRegistryEntries(purpose, 1) + if err != nil { + return nil, err + } + if len(entries) != 1 { + return nil, fmt.Errorf("no hvsock registry entry found for purpose: %s", purpose.string()) + } + + return entries[0], nil +} + +func LoadAllHVSockRegistryEntriesByPurpose(purpose HVSockPurpose) ([]*HVSockRegistryEntry, error) { + entries, err := loadHVSockRegistryEntries(purpose, -1) + if err != nil { + return nil, err + } + + return entries, nil +} + +func parseHexToUint64(hex string) (uint64, error) { + return strconv.ParseUint(hex, 16, 64) +} + +// It removes HVSock registry entries for Network, Events, and Fileserver. +// It returns loading errors immediately. For removals, it attempts all, logs individual failures, +// and returns a joined error (via errors.Join) if any occur. +// Returns nil only if all entries are loaded and removed successfully. +func RemoveAllHVSockRegistryEntries() error { + // Tear down vsocks + networkSocks, err := LoadAllHVSockRegistryEntriesByPurpose(Network) + if err != nil { + return err + } + eventsSocks, err := LoadAllHVSockRegistryEntriesByPurpose(Events) + if err != nil { + return err + } + fileserverSocks, err := LoadAllHVSockRegistryEntriesByPurpose(Fileserver) + if err != nil { + return err + } + + allSocks := []*HVSockRegistryEntry{} + allSocks = append(allSocks, networkSocks...) + allSocks = append(allSocks, eventsSocks...) + allSocks = append(allSocks, fileserverSocks...) + + var removalErrors []error + for _, sock := range allSocks { + if err := sock.Remove(); err != nil { + logrus.Errorf("unable to remove registry entry for %s: %q", sock.KeyName, err) + removalErrors = append(removalErrors, fmt.Errorf("failed to remove sock %s: %w", sock.KeyName, err)) + } + } + + if len(removalErrors) > 0 { + return errors.Join(removalErrors...) + } + + return nil +} diff --git a/pkg/machine/provider/platform_windows.go b/pkg/machine/provider/platform_windows.go index 412177bd5f..d2e6522d15 100644 --- a/pkg/machine/provider/platform_windows.go +++ b/pkg/machine/provider/platform_windows.go @@ -39,9 +39,6 @@ func GetByVMType(resolvedVMType define.VMType) (vmconfigs.VMProvider, error) { case define.WSLVirt: return new(wsl.WSLStubber), nil case define.HyperVVirt: - if !windows.HasAdminRights() { - return nil, fmt.Errorf("hyperv machines require admin authority") - } return new(hyperv.HyperVStubber), nil default: } diff --git a/pkg/machine/windows/util_windows.go b/pkg/machine/windows/util_windows.go index 8a6ae9841e..64e7b261a2 100644 --- a/pkg/machine/windows/util_windows.go +++ b/pkg/machine/windows/util_windows.go @@ -1,16 +1,10 @@ package windows import ( - "errors" - "github.com/sirupsen/logrus" "golang.org/x/sys/windows" ) -var ( - ErrHypervRequiresAdmin = errors.New("Hyper-V machines require admin rights to run. Please run Podman as an administrator") -) - func HasAdminRights() bool { var sid *windows.SID