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 }