Files
podman/pkg/machine/wsl/util_windows.go
lstocchi 8532ecb710 fix wsl install workflow on machine init command
this patch changes how the detection of wsl works.
The old way of using wsl --status command output to detect some missing features required by WSL is not fully reliable.
WSL checks if the wsl feature is enabled and if the vmcompute service do exist. However, this is not enough to identify if the virtual machine platform feature is enabled. The vmcompute service could exist because it has been installed by other tools or it could exist but being stopped.

The way proposed by this patch is to try execute the import command and,
if it fails, check the error and if it is related to the Host Compute
Service try to install all features required by WSL.

The flow is the same as before, the user is asked to execute the podman
machine init command with elevated privileges. Eventually, after
enabling WSL and VMP features, the user is asked to reboot the machine.

When the machine restarts, the powershell gets invoked again and execute
the command init.

The code also fixes some issues that could cause misbehaviors when
invoking recursively the elevated shell, like an unreleased lock, or a
missing file.

Signed-off-by: lstocchi <lstocchi@redhat.com>
2025-06-04 14:26:48 +02:00

298 lines
8.3 KiB
Go

//go:build windows
package wsl
import (
"bytes"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
"github.com/Microsoft/go-winio"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/storage/pkg/fileutils"
"github.com/containers/storage/pkg/homedir"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
type SHELLEXECUTEINFO struct {
cbSize uint32
fMask uint32
hwnd syscall.Handle
lpVerb uintptr
lpFile uintptr
lpParameters uintptr
lpDirectory uintptr
nShow int
hInstApp syscall.Handle
lpIDList uintptr
lpClass uintptr
hkeyClass syscall.Handle
dwHotKey uint32
hIconOrMonitor syscall.Handle
hProcess syscall.Handle
}
// Cleaner to refer to the official OS constant names, and consistent with syscall
// Ref: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfow#members
const (
SEE_MASK_NOCLOSEPROCESS = 0x40
SE_ERR_ACCESSDENIED = 0x05
)
const (
// ref: https://learn.microsoft.com/en-us/windows/win32/secauthz/privilege-constants#constants
rebootPrivilege = "SeShutdownPrivilege"
// "Application: Installation (Planned)" A planned restart or shutdown to perform application installation.
// ref: https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes
rebootReason = windows.SHTDN_REASON_MAJOR_APPLICATION | windows.SHTDN_REASON_MINOR_INSTALLATION | windows.SHTDN_REASON_FLAG_PLANNED
// ref: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-exitwindowsex#parameters
rebootFlags = windows.EWX_REBOOT | windows.EWX_RESTARTAPPS | windows.EWX_FORCEIFHUNG
)
func winVersionAtLeast(major uint, minor uint, build uint) bool {
var out [3]uint32
in := []uint32{uint32(major), uint32(minor), uint32(build)}
out[0], out[1], out[2] = windows.RtlGetNtVersionNumbers()
for i, o := range out {
if in[i] > o {
return false
}
if in[i] < o {
return true
}
}
return true
}
func HasAdminRights() bool {
var sid *windows.SID
// See: https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
if err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid); err != nil {
logrus.Warnf("SID allocation error: %s", err)
return false
}
defer func() {
_ = windows.FreeSid(sid)
}()
// 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 || token.IsElevated()
}
func relaunchElevatedWait() error {
e, _ := os.Executable()
d, _ := os.Getwd()
exe, _ := syscall.UTF16PtrFromString(e)
cwd, _ := syscall.UTF16PtrFromString(d)
arg, _ := syscall.UTF16PtrFromString(buildCommandArgs(true))
verb, _ := syscall.UTF16PtrFromString("runas")
shell32 := syscall.NewLazyDLL("shell32.dll")
info := &SHELLEXECUTEINFO{
fMask: SEE_MASK_NOCLOSEPROCESS,
hwnd: 0,
lpVerb: uintptr(unsafe.Pointer(verb)),
lpFile: uintptr(unsafe.Pointer(exe)),
lpParameters: uintptr(unsafe.Pointer(arg)),
lpDirectory: uintptr(unsafe.Pointer(cwd)),
nShow: syscall.SW_SHOWNORMAL,
}
info.cbSize = uint32(unsafe.Sizeof(*info))
procShellExecuteEx := shell32.NewProc("ShellExecuteExW")
if ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))); ret == 0 { // 0 = False
err := syscall.GetLastError()
if info.hInstApp == SE_ERR_ACCESSDENIED {
return wrapMaybe(err, "request to elevate privileges was denied")
}
return wrapMaybef(err, "could not launch process, ShellEX Error = %d", info.hInstApp)
}
handle := info.hProcess
defer func() {
_ = syscall.CloseHandle(handle)
}()
w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE)
switch w {
case syscall.WAIT_OBJECT_0:
break
case syscall.WAIT_FAILED:
return fmt.Errorf("could not wait for process, failed: %w", err)
default:
return fmt.Errorf("could not wait for process, unknown error. event: %X, err: %v", w, err)
}
var code uint32
if err := syscall.GetExitCodeProcess(handle, &code); err != nil {
return err
}
if code != 0 {
return &ExitCodeError{uint(code)}
}
return nil
}
func wrapMaybe(err error, message string) error {
if err != nil {
return fmt.Errorf("%v: %w", message, err)
}
return errors.New(message)
}
func wrapMaybef(err error, format string, args ...interface{}) error {
if err != nil {
return fmt.Errorf(format+": %w", append(args, err)...)
}
return fmt.Errorf(format, args...)
}
func reboot() error {
const (
wtLocation = `Microsoft\WindowsApps\wt.exe`
wtPrefix = `%LocalAppData%\Microsoft\WindowsApps\wt -p "Windows PowerShell" `
localAppData = "LocalAppData"
pShellLaunch = `powershell -noexit "powershell -EncodedCommand (Get-Content '%s')"`
)
exe, _ := os.Executable()
relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), buildCommandArgs(false))
encoded := base64.StdEncoding.EncodeToString(encodeUTF16Bytes(relaunch))
dataDir, err := homedir.GetDataHome()
if err != nil {
return fmt.Errorf("could not determine data directory: %w", err)
}
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("could not create data directory: %w", err)
}
commFile := filepath.Join(dataDir, "podman-relaunch.dat")
if err := os.WriteFile(commFile, []byte(encoded), 0600); err != nil {
return fmt.Errorf("could not serialize command state: %w", err)
}
command := fmt.Sprintf(pShellLaunch, commFile)
if err := fileutils.Lexists(filepath.Join(os.Getenv(localAppData), wtLocation)); err == nil {
wtCommand := wtPrefix + command
// RunOnce is limited to 260 chars (supposedly no longer in Builds >= 19489)
// For now fallback in cases of long usernames (>89 chars)
if len(wtCommand) < 260 {
command = wtCommand
}
}
if err := addRunOnceRegistryEntry(command); err != nil {
return err
}
message := "To continue the process of enabling WSL, the system needs to reboot. " +
"Alternatively, you can cancel and reboot manually\n\n" +
"After rebooting, please wait a minute or two for podman machine to relaunch and continue installing."
if MessageBox(message, "Podman Machine", false) != 1 {
fmt.Println("Reboot is required to continue installation, please reboot at your convenience")
os.Exit(ErrorSuccessRebootRequired)
return nil
}
if err := winio.RunWithPrivilege(rebootPrivilege, func() error {
if err := windows.ExitWindowsEx(rebootFlags, rebootReason); err != nil {
return fmt.Errorf("execute ExitWindowsEx to reboot system failed: %w", err)
}
return nil
}); err != nil {
return fmt.Errorf("cannot reboot system: %w", err)
}
return define.ErrRebootInitiated
}
func addRunOnceRegistryEntry(command string) error {
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
if err != nil {
return fmt.Errorf("could not open RunOnce registry entry: %w", err)
}
defer k.Close()
if err := k.SetExpandStringValue("podman-machine", command); err != nil {
return fmt.Errorf("could not open RunOnce registry entry: %w", err)
}
return nil
}
func encodeUTF16Bytes(s string) []byte {
u16 := utf16.Encode([]rune(s))
buf := new(bytes.Buffer)
for _, r := range u16 {
_ = binary.Write(buf, binary.LittleEndian, r)
}
return buf.Bytes()
}
func MessageBox(caption, title string, fail bool) int {
var format uint32
if fail {
format = windows.MB_ICONERROR
} else {
format = windows.MB_OKCANCEL | windows.MB_ICONINFORMATION
}
captionPtr, _ := syscall.UTF16PtrFromString(caption)
titlePtr, _ := syscall.UTF16PtrFromString(title)
ret, _ := windows.MessageBox(0, captionPtr, titlePtr, format)
return int(ret)
}
func buildCommandArgs(elevate bool) string {
var args []string
for _, arg := range os.Args[1:] {
if arg != "--reexec" {
args = append(args, syscall.EscapeArg(arg))
if elevate && arg == "init" {
args = append(args, "--reexec")
}
}
}
return strings.Join(args, " ")
}