github.com/containers/podman/v4@v4.9.4/pkg/machine/wsl/util_windows.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package wsl
     5  
     6  import (
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  	"unicode/utf16"
    15  	"unsafe"
    16  
    17  	"github.com/containers/storage/pkg/homedir"
    18  	"github.com/sirupsen/logrus"
    19  	"golang.org/x/sys/windows"
    20  	"golang.org/x/sys/windows/registry"
    21  )
    22  
    23  // nolint
    24  type SHELLEXECUTEINFO struct {
    25  	cbSize         uint32
    26  	fMask          uint32
    27  	hwnd           syscall.Handle
    28  	lpVerb         uintptr
    29  	lpFile         uintptr
    30  	lpParameters   uintptr
    31  	lpDirectory    uintptr
    32  	nShow          int
    33  	hInstApp       syscall.Handle
    34  	lpIDList       uintptr
    35  	lpClass        uintptr
    36  	hkeyClass      syscall.Handle
    37  	dwHotKey       uint32
    38  	hIconOrMonitor syscall.Handle
    39  	hProcess       syscall.Handle
    40  }
    41  
    42  // nolint
    43  type Luid struct {
    44  	lowPart  uint32
    45  	highPart int32
    46  }
    47  
    48  type LuidAndAttributes struct {
    49  	luid       Luid
    50  	attributes uint32
    51  }
    52  
    53  type TokenPrivileges struct {
    54  	privilegeCount uint32
    55  	privileges     [1]LuidAndAttributes
    56  }
    57  
    58  // nolint // Cleaner to refer to the official OS constant names, and consistent with syscall
    59  const (
    60  	SEE_MASK_NOCLOSEPROCESS         = 0x40
    61  	EWX_FORCEIFHUNG                 = 0x10
    62  	EWX_REBOOT                      = 0x02
    63  	EWX_RESTARTAPPS                 = 0x40
    64  	SHTDN_REASON_MAJOR_APPLICATION  = 0x00040000
    65  	SHTDN_REASON_MINOR_INSTALLATION = 0x00000002
    66  	SHTDN_REASON_FLAG_PLANNED       = 0x80000000
    67  	TOKEN_ADJUST_PRIVILEGES         = 0x0020
    68  	TOKEN_QUERY                     = 0x0008
    69  	SE_PRIVILEGE_ENABLED            = 0x00000002
    70  	SE_ERR_ACCESSDENIED             = 0x05
    71  )
    72  
    73  func winVersionAtLeast(major uint, minor uint, build uint) bool {
    74  	var out [3]uint32
    75  
    76  	in := []uint32{uint32(major), uint32(minor), uint32(build)}
    77  	out[0], out[1], out[2] = windows.RtlGetNtVersionNumbers()
    78  
    79  	for i, o := range out {
    80  		if in[i] > o {
    81  			return false
    82  		}
    83  		if in[i] < o {
    84  			return true
    85  		}
    86  	}
    87  
    88  	return true
    89  }
    90  
    91  func hasAdminRights() bool {
    92  	var sid *windows.SID
    93  
    94  	// See: https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
    95  	if err := windows.AllocateAndInitializeSid(
    96  		&windows.SECURITY_NT_AUTHORITY,
    97  		2,
    98  		windows.SECURITY_BUILTIN_DOMAIN_RID,
    99  		windows.DOMAIN_ALIAS_RID_ADMINS,
   100  		0, 0, 0, 0, 0, 0,
   101  		&sid); err != nil {
   102  		logrus.Warnf("SID allocation error: %s", err)
   103  		return false
   104  	}
   105  	defer windows.FreeSid(sid)
   106  
   107  	//  From MS docs:
   108  	// "If TokenHandle is NULL, CheckTokenMembership uses the impersonation
   109  	//  token of the calling thread. If the thread is not impersonating,
   110  	//  the function duplicates the thread's primary token to create an
   111  	//  impersonation token."
   112  	token := windows.Token(0)
   113  
   114  	member, err := token.IsMember(sid)
   115  	if err != nil {
   116  		logrus.Warnf("Token Membership Error: %s", err)
   117  		return false
   118  	}
   119  
   120  	return member || token.IsElevated()
   121  }
   122  
   123  func relaunchElevatedWait() error {
   124  	e, _ := os.Executable()
   125  	d, _ := os.Getwd()
   126  	exe, _ := syscall.UTF16PtrFromString(e)
   127  	cwd, _ := syscall.UTF16PtrFromString(d)
   128  	arg, _ := syscall.UTF16PtrFromString(buildCommandArgs(true))
   129  	verb, _ := syscall.UTF16PtrFromString("runas")
   130  
   131  	shell32 := syscall.NewLazyDLL("shell32.dll")
   132  
   133  	info := &SHELLEXECUTEINFO{
   134  		fMask:        SEE_MASK_NOCLOSEPROCESS,
   135  		hwnd:         0,
   136  		lpVerb:       uintptr(unsafe.Pointer(verb)),
   137  		lpFile:       uintptr(unsafe.Pointer(exe)),
   138  		lpParameters: uintptr(unsafe.Pointer(arg)),
   139  		lpDirectory:  uintptr(unsafe.Pointer(cwd)),
   140  		nShow:        1,
   141  	}
   142  	info.cbSize = uint32(unsafe.Sizeof(*info))
   143  	procShellExecuteEx := shell32.NewProc("ShellExecuteExW")
   144  	if ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(info))); ret == 0 { // 0 = False
   145  		err := syscall.GetLastError()
   146  		if info.hInstApp == SE_ERR_ACCESSDENIED {
   147  			return wrapMaybe(err, "request to elevate privileges was denied")
   148  		}
   149  		return wrapMaybef(err, "could not launch process, ShellEX Error = %d", info.hInstApp)
   150  	}
   151  
   152  	handle := syscall.Handle(info.hProcess)
   153  	defer syscall.CloseHandle(handle)
   154  
   155  	w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE)
   156  	switch w {
   157  	case syscall.WAIT_OBJECT_0:
   158  		break
   159  	case syscall.WAIT_FAILED:
   160  		return fmt.Errorf("could not wait for process, failed: %w", err)
   161  	default:
   162  		return errors.New("could not wait for process, unknown error")
   163  	}
   164  	var code uint32
   165  	if err := syscall.GetExitCodeProcess(handle, &code); err != nil {
   166  		return err
   167  	}
   168  	if code != 0 {
   169  		return &ExitCodeError{uint(code)}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func wrapMaybe(err error, message string) error {
   176  	if err != nil {
   177  		return fmt.Errorf("%v: %w", message, err)
   178  	}
   179  
   180  	return errors.New(message)
   181  }
   182  
   183  func wrapMaybef(err error, format string, args ...interface{}) error {
   184  	if err != nil {
   185  		return fmt.Errorf(format+": %w", append(args, err)...)
   186  	}
   187  
   188  	return fmt.Errorf(format, args...)
   189  }
   190  
   191  func reboot() error {
   192  	const (
   193  		wtLocation   = `Microsoft\WindowsApps\wt.exe`
   194  		wtPrefix     = `%LocalAppData%\Microsoft\WindowsApps\wt -p "Windows PowerShell" `
   195  		localAppData = "LocalAppData"
   196  		pShellLaunch = `powershell -noexit "powershell -EncodedCommand (Get-Content '%s')"`
   197  	)
   198  
   199  	exe, _ := os.Executable()
   200  	relaunch := fmt.Sprintf("& %s %s", syscall.EscapeArg(exe), buildCommandArgs(false))
   201  	encoded := base64.StdEncoding.EncodeToString(encodeUTF16Bytes(relaunch))
   202  
   203  	dataDir, err := homedir.GetDataHome()
   204  	if err != nil {
   205  		return fmt.Errorf("could not determine data directory: %w", err)
   206  	}
   207  	if err := os.MkdirAll(dataDir, 0755); err != nil {
   208  		return fmt.Errorf("could not create data directory: %w", err)
   209  	}
   210  	commFile := filepath.Join(dataDir, "podman-relaunch.dat")
   211  	if err := os.WriteFile(commFile, []byte(encoded), 0600); err != nil {
   212  		return fmt.Errorf("could not serialize command state: %w", err)
   213  	}
   214  
   215  	command := fmt.Sprintf(pShellLaunch, commFile)
   216  	if _, err := os.Lstat(filepath.Join(os.Getenv(localAppData), wtLocation)); err == nil {
   217  		wtCommand := wtPrefix + command
   218  		// RunOnce is limited to 260 chars (supposedly no longer in Builds >= 19489)
   219  		// For now fallback in cases of long usernames (>89 chars)
   220  		if len(wtCommand) < 260 {
   221  			command = wtCommand
   222  		}
   223  	}
   224  
   225  	if err := addRunOnceRegistryEntry(command); err != nil {
   226  		return err
   227  	}
   228  
   229  	if err := obtainShutdownPrivilege(); err != nil {
   230  		return err
   231  	}
   232  
   233  	message := "To continue the process of enabling WSL, the system needs to reboot. " +
   234  		"Alternatively, you can cancel and reboot manually\n\n" +
   235  		"After rebooting, please wait a minute or two for podman machine to relaunch and continue installing."
   236  
   237  	if MessageBox(message, "Podman Machine", false) != 1 {
   238  		fmt.Println("Reboot is required to continue installation, please reboot at your convenience")
   239  		os.Exit(ErrorSuccessRebootRequired)
   240  		return nil
   241  	}
   242  
   243  	user32 := syscall.NewLazyDLL("user32")
   244  	procExit := user32.NewProc("ExitWindowsEx")
   245  	if ret, _, err := procExit.Call(EWX_REBOOT|EWX_RESTARTAPPS|EWX_FORCEIFHUNG,
   246  		SHTDN_REASON_MAJOR_APPLICATION|SHTDN_REASON_MINOR_INSTALLATION|SHTDN_REASON_FLAG_PLANNED); ret != 1 {
   247  		return fmt.Errorf("reboot failed: %w", err)
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  func obtainShutdownPrivilege() error {
   254  	const SeShutdownName = "SeShutdownPrivilege"
   255  
   256  	advapi32 := syscall.NewLazyDLL("advapi32")
   257  	OpenProcessToken := advapi32.NewProc("OpenProcessToken")
   258  	LookupPrivilegeValue := advapi32.NewProc("LookupPrivilegeValueW")
   259  	AdjustTokenPrivileges := advapi32.NewProc("AdjustTokenPrivileges")
   260  
   261  	proc, _ := syscall.GetCurrentProcess()
   262  
   263  	var hToken uintptr
   264  	if ret, _, err := OpenProcessToken.Call(uintptr(proc), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, uintptr(unsafe.Pointer(&hToken))); ret != 1 {
   265  		return fmt.Errorf("opening process token: %w", err)
   266  	}
   267  
   268  	var privs TokenPrivileges
   269  	if ret, _, err := LookupPrivilegeValue.Call(uintptr(0), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(SeShutdownName))), uintptr(unsafe.Pointer(&(privs.privileges[0].luid)))); ret != 1 {
   270  		return fmt.Errorf("looking up shutdown privilege: %w", err)
   271  	}
   272  
   273  	privs.privilegeCount = 1
   274  	privs.privileges[0].attributes = SE_PRIVILEGE_ENABLED
   275  
   276  	if ret, _, err := AdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&privs)), 0, uintptr(0), 0); ret != 1 {
   277  		return fmt.Errorf("enabling shutdown privilege on token: %w", err)
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  func addRunOnceRegistryEntry(command string) error {
   284  	k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
   285  	if err != nil {
   286  		return fmt.Errorf("could not open RunOnce registry entry: %w", err)
   287  	}
   288  
   289  	defer k.Close()
   290  
   291  	if err := k.SetExpandStringValue("podman-machine", command); err != nil {
   292  		return fmt.Errorf("could not open RunOnce registry entry: %w", err)
   293  	}
   294  
   295  	return nil
   296  }
   297  
   298  func encodeUTF16Bytes(s string) []byte {
   299  	u16 := utf16.Encode([]rune(s))
   300  	u16le := make([]byte, len(u16)*2)
   301  	for i := 0; i < len(u16); i++ {
   302  		u16le[i<<1] = byte(u16[i])
   303  		u16le[(i<<1)+1] = byte(u16[i] >> 8)
   304  	}
   305  	return u16le
   306  }
   307  
   308  func MessageBox(caption, title string, fail bool) int {
   309  	var format int
   310  	if fail {
   311  		format = 0x10
   312  	} else {
   313  		format = 0x41
   314  	}
   315  
   316  	user32 := syscall.NewLazyDLL("user32.dll")
   317  	captionPtr, _ := syscall.UTF16PtrFromString(caption)
   318  	titlePtr, _ := syscall.UTF16PtrFromString(title)
   319  	ret, _, _ := user32.NewProc("MessageBoxW").Call(
   320  		uintptr(0),
   321  		uintptr(unsafe.Pointer(captionPtr)),
   322  		uintptr(unsafe.Pointer(titlePtr)),
   323  		uintptr(format))
   324  
   325  	return int(ret)
   326  }
   327  
   328  func buildCommandArgs(elevate bool) string {
   329  	var args []string
   330  	for _, arg := range os.Args[1:] {
   331  		if arg != "--reexec" {
   332  			args = append(args, syscall.EscapeArg(arg))
   333  			if elevate && arg == "init" {
   334  				args = append(args, "--reexec")
   335  			}
   336  		}
   337  	}
   338  	return strings.Join(args, " ")
   339  }