podman/pkg/machine/wsl/util_windows.go

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, " ")
}