summaryrefslogtreecommitdiff
path: root/pkg/machine/wsl/util_windows.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/machine/wsl/util_windows.go')
-rw-r--r--pkg/machine/wsl/util_windows.go338
1 files changed, 338 insertions, 0 deletions
diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go
new file mode 100644
index 000000000..95e4c9894
--- /dev/null
+++ b/pkg/machine/wsl/util_windows.go
@@ -0,0 +1,338 @@
+package wsl
+
+import (
+ "encoding/base64"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "unicode/utf16"
+ "unsafe"
+
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
+
+ "github.com/containers/storage/pkg/homedir"
+)
+
+//nolint
+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
+}
+
+//nolint
+type Luid struct {
+ lowPart uint32
+ highPart int32
+}
+
+type LuidAndAttributes struct {
+ luid Luid
+ attributes uint32
+}
+
+type TokenPrivileges struct {
+ privilegeCount uint32
+ privileges [1]LuidAndAttributes
+}
+
+//nolint // Cleaner to refer to the official OS constant names, and consistent with syscall
+const (
+ SEE_MASK_NOCLOSEPROCESS = 0x40
+ EWX_FORCEIFHUNG = 0x10
+ EWX_REBOOT = 0x02
+ EWX_RESTARTAPPS = 0x40
+ SHTDN_REASON_MAJOR_APPLICATION = 0x00040000
+ SHTDN_REASON_MINOR_INSTALLATION = 0x00000002
+ SHTDN_REASON_FLAG_PLANNED = 0x80000000
+ TOKEN_ADJUST_PRIVILEGES = 0x0020
+ TOKEN_QUERY = 0x0008
+ SE_PRIVILEGE_ENABLED = 0x00000002
+ SE_ERR_ACCESSDENIED = 0x05
+)
+
+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 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: 1,
+ }
+ 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 := syscall.Handle(info.hProcess)
+ defer syscall.CloseHandle(handle)
+
+ w, err := syscall.WaitForSingleObject(handle, syscall.INFINITE)
+ switch w {
+ case syscall.WAIT_OBJECT_0:
+ break
+ case syscall.WAIT_FAILED:
+ return errors.Wrap(err, "could not wait for process, failed")
+ default:
+ return errors.Errorf("could not wait for process, unknown error")
+ }
+ 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 errors.Wrap(err, message)
+ }
+
+ return errors.New(message)
+}
+
+func wrapMaybef(err error, format string, args ...interface{}) error {
+ if err != nil {
+ return errors.Wrapf(err, format, args...)
+ }
+
+ return errors.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 errors.Wrap(err, "could not determine data directory")
+ }
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ return errors.Wrap(err, "could not create data directory")
+ }
+ commFile := filepath.Join(dataDir, "podman-relaunch.dat")
+ if err := ioutil.WriteFile(commFile, []byte(encoded), 0600); err != nil {
+ return errors.Wrap(err, "could not serialize command state")
+ }
+
+ command := fmt.Sprintf(pShellLaunch, commFile)
+ if _, err := os.Lstat(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 fallbacak in cases of long usernames (>89 chars)
+ if len(wtCommand) < 260 {
+ command = wtCommand
+ }
+ }
+
+ if err := addRunOnceRegistryEntry(command); err != nil {
+ return err
+ }
+
+ if err := obtainShutdownPrivilege(); 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
+ }
+
+ user32 := syscall.NewLazyDLL("user32")
+ procExit := user32.NewProc("ExitWindowsEx")
+ if ret, _, err := procExit.Call(EWX_REBOOT|EWX_RESTARTAPPS|EWX_FORCEIFHUNG,
+ SHTDN_REASON_MAJOR_APPLICATION|SHTDN_REASON_MINOR_INSTALLATION|SHTDN_REASON_FLAG_PLANNED); ret != 1 {
+ return errors.Wrap(err, "reboot failed")
+ }
+
+ return nil
+}
+
+func obtainShutdownPrivilege() error {
+ const SeShutdownName = "SeShutdownPrivilege"
+
+ advapi32 := syscall.NewLazyDLL("advapi32")
+ OpenProcessToken := advapi32.NewProc("OpenProcessToken")
+ LookupPrivilegeValue := advapi32.NewProc("LookupPrivilegeValueW")
+ AdjustTokenPrivileges := advapi32.NewProc("AdjustTokenPrivileges")
+
+ proc, _ := syscall.GetCurrentProcess()
+
+ var hToken uintptr
+ if ret, _, err := OpenProcessToken.Call(uintptr(proc), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, uintptr(unsafe.Pointer(&hToken))); ret != 1 {
+ return errors.Wrap(err, "error opening process token")
+ }
+
+ var privs TokenPrivileges
+ if ret, _, err := LookupPrivilegeValue.Call(uintptr(0), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(SeShutdownName))), uintptr(unsafe.Pointer(&(privs.privileges[0].luid)))); ret != 1 {
+ return errors.Wrap(err, "error looking up shutdown privilege")
+ }
+
+ privs.privilegeCount = 1
+ privs.privileges[0].attributes = SE_PRIVILEGE_ENABLED
+
+ if ret, _, err := AdjustTokenPrivileges.Call(hToken, 0, uintptr(unsafe.Pointer(&privs)), 0, uintptr(0), 0); ret != 1 {
+ return errors.Wrap(err, "error enabling shutdown privilege on token")
+ }
+
+ return nil
+}
+
+func addRunOnceRegistryEntry(command string) error {
+ k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
+ if err != nil {
+ return errors.Wrap(err, "could not open RunOnce registry entry")
+ }
+
+ defer k.Close()
+
+ if err := k.SetExpandStringValue("podman-machine", command); err != nil {
+ return errors.Wrap(err, "could not open RunOnce registry entry")
+ }
+
+ return nil
+}
+
+func encodeUTF16Bytes(s string) []byte {
+ u16 := utf16.Encode([]rune(s))
+ u16le := make([]byte, len(u16)*2)
+ for i := 0; i < len(u16); i++ {
+ u16le[i<<1] = byte(u16[i])
+ u16le[(i<<1)+1] = byte(u16[i] >> 8)
+ }
+ return u16le
+}
+
+func MessageBox(caption, title string, fail bool) int {
+ var format int
+ if fail {
+ format = 0x10
+ } else {
+ format = 0x41
+ }
+
+ user32 := syscall.NewLazyDLL("user32.dll")
+ captionPtr, _ := syscall.UTF16PtrFromString(caption)
+ titlePtr, _ := syscall.UTF16PtrFromString(title)
+ ret, _, _ := user32.NewProc("MessageBoxW").Call(
+ uintptr(0),
+ uintptr(unsafe.Pointer(captionPtr)),
+ uintptr(unsafe.Pointer(titlePtr)),
+ uintptr(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, " ")
+}