mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-13 03:13:42 +00:00
fix: align process handling and fix Windows crashes (#1376)
The intermittent Windows test crashes came from step errors formatting a nil `rc.Run`. `fmt` recovers the nil dereference, but on CPUs with AMX the recovered fault overflows into the heap (https://github.com/golang/go/issues/81238). The errors now name the job, and the Windows test VM hides AMX and runs on UTC, so cached boots no longer jump back 8 hours mid-test. Windows commands now start suspended and resume once they are in their job objects, so no child escapes them. That drops the process scan reading other processes' memory and the last `unsafe` code, which `depguard` now denies. Step processes outlive a crashed runner, and commands are no longer looked up in the runner's working directory, both as with GitHub's runner. Like GitHub's runner, host jobs on Linux and macOS now end the processes carrying the job's `RUNNER_TRACKING_ID`, and image pulls and Dockerfile action builds are retried. Reviewed-on: https://gitea.com/gitea/runner/pulls/1376 Reviewed-by: bircni <[email protected]> Co-authored-by: silverwind <[email protected]>
This commit is contained in:
1 parent
142d9f8c88
commit
dcba546cb3
32 files changed
+374
-593
No files matched your search
@@ -1,263 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func KillProcessesWithPaths(ctx context.Context, dirs []string, trackingID string) (int, error) {
|
||||
if len(dirs) == 0 || trackingID == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
pids, err := listProcessIDs()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
self := uint32(os.Getpid())
|
||||
var ownSession uint32
|
||||
if err := windows.ProcessIdToSessionId(self, &ownSession); err != nil {
|
||||
return 0, fmt.Errorf("look up own session: %w", err)
|
||||
}
|
||||
|
||||
var killed int
|
||||
var errs []error
|
||||
for _, pid := range pids {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if pid == self || pid == 0 || pid == 4 {
|
||||
continue
|
||||
}
|
||||
var session uint32
|
||||
if err := windows.ProcessIdToSessionId(pid, &session); err != nil || session != ownSession {
|
||||
continue
|
||||
}
|
||||
ok, err := terminateIfPathsMatch(pid, dirs, trackingID)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if ok {
|
||||
killed++
|
||||
}
|
||||
}
|
||||
|
||||
return killed, errors.Join(errs...)
|
||||
}
|
||||
|
||||
func terminateIfPathsMatch(pid uint32, dirs []string, trackingID string) (bool, error) {
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ|windows.PROCESS_TERMINATE, false, pid)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
defer func() { _ = windows.CloseHandle(handle) }()
|
||||
|
||||
params, err := processParametersHandle(handle)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
matches := false
|
||||
fields:
|
||||
for _, field := range []struct {
|
||||
offset uintptr
|
||||
wow64 uintptr
|
||||
command bool
|
||||
}{
|
||||
{unsafe.Offsetof(windows.RTL_USER_PROCESS_PARAMETERS{}.CurrentDirectory), 0x24, false},
|
||||
{unsafe.Offsetof(windows.RTL_USER_PROCESS_PARAMETERS{}.ImagePathName), 0x38, false},
|
||||
{unsafe.Offsetof(windows.RTL_USER_PROCESS_PARAMETERS{}.CommandLine), 0x40, true},
|
||||
} {
|
||||
value, err := params.unicodeString(handle, field.offset, field.wow64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if !field.command && dirContainsPath(dir, value) || field.command && strings.Contains(strings.ToLower(value), strings.ToLower(strings.TrimRight(dir, `\/`)+`\`)) {
|
||||
matches = true
|
||||
break fields
|
||||
}
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
return false, nil
|
||||
}
|
||||
value, err := params.environmentVariable(handle, "RUNNER_TRACKING_ID")
|
||||
if err != nil || value != trackingID {
|
||||
return false, nil
|
||||
}
|
||||
if err := windows.TerminateProcess(handle, 1); err != nil {
|
||||
return false, fmt.Errorf("terminate pid %d: %w", pid, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// listProcessIDs returns the PIDs of every process in a Toolhelp snapshot.
|
||||
func listProcessIDs() ([]uint32, error) {
|
||||
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = windows.CloseHandle(snap) }()
|
||||
|
||||
var entry windows.ProcessEntry32
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
|
||||
pids := make([]uint32, 0, 256)
|
||||
err = windows.Process32First(snap, &entry)
|
||||
for err == nil {
|
||||
pids = append(pids, entry.ProcessID)
|
||||
err = windows.Process32Next(snap, &entry)
|
||||
}
|
||||
if !errors.Is(err, windows.ERROR_NO_MORE_FILES) {
|
||||
return pids, err
|
||||
}
|
||||
return pids, nil
|
||||
}
|
||||
|
||||
type remoteParameters struct {
|
||||
address uintptr
|
||||
wow64 bool
|
||||
}
|
||||
|
||||
func processParametersHandle(handle windows.Handle) (remoteParameters, error) {
|
||||
var pbi windows.PROCESS_BASIC_INFORMATION
|
||||
var retLen uint32
|
||||
if err := windows.NtQueryInformationProcess(handle, windows.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), &retLen); err != nil {
|
||||
return remoteParameters{}, err
|
||||
}
|
||||
pebAddr := uintptr(unsafe.Pointer(pbi.PebBaseAddress))
|
||||
wow64 := false
|
||||
if unsafe.Sizeof(uintptr(0)) == 8 {
|
||||
var peb32 uintptr
|
||||
if err := windows.NtQueryInformationProcess(handle, windows.ProcessWow64Information, unsafe.Pointer(&peb32), uint32(unsafe.Sizeof(peb32)), &retLen); err != nil {
|
||||
return remoteParameters{}, err
|
||||
}
|
||||
if peb32 != 0 {
|
||||
pebAddr = peb32
|
||||
wow64 = true
|
||||
}
|
||||
}
|
||||
if pebAddr == 0 {
|
||||
return remoteParameters{}, errors.New("nil PEB base address")
|
||||
}
|
||||
params := remoteParameters{wow64: wow64}
|
||||
address, err := params.pointer(handle, pebAddr+params.offset(unsafe.Offsetof(windows.PEB{}.ProcessParameters), 0x10))
|
||||
if err != nil {
|
||||
return remoteParameters{}, err
|
||||
}
|
||||
if address == 0 {
|
||||
return remoteParameters{}, errors.New("nil process parameters")
|
||||
}
|
||||
params.address = address
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (p remoteParameters) offset(native, wow64 uintptr) uintptr {
|
||||
if p.wow64 {
|
||||
return wow64
|
||||
}
|
||||
return native
|
||||
}
|
||||
|
||||
func (p remoteParameters) pointer(handle windows.Handle, address uintptr) (uintptr, error) {
|
||||
var value uintptr
|
||||
size := unsafe.Sizeof(value)
|
||||
if p.wow64 {
|
||||
size = 4
|
||||
}
|
||||
var read uintptr
|
||||
if err := windows.ReadProcessMemory(handle, address, (*byte)(unsafe.Pointer(&value)), size, &read); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if read != size {
|
||||
return 0, errors.New("short pointer read")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (p remoteParameters) unicodeString(handle windows.Handle, native, wow64 uintptr) (string, error) {
|
||||
address := p.address + p.offset(native, wow64)
|
||||
length, err := readRemoteU16(handle, address)
|
||||
if err != nil || length == 0 {
|
||||
return "", err
|
||||
}
|
||||
buffer, err := p.pointer(handle, address+p.offset(unsafe.Offsetof(windows.NTUnicodeString{}.Buffer), 4))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value := make([]uint16, (int(length)+1)/2)
|
||||
var read uintptr
|
||||
if err := windows.ReadProcessMemory(handle, buffer, (*byte)(unsafe.Pointer(&value[0])), uintptr(length), &read); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if read != uintptr(length) {
|
||||
return "", errors.New("short string read")
|
||||
}
|
||||
return windows.UTF16ToString(value), nil
|
||||
}
|
||||
|
||||
func (p remoteParameters) environmentVariable(handle windows.Handle, name string) (string, error) {
|
||||
address, err := p.pointer(handle, p.address+p.offset(unsafe.Offsetof(windows.RTL_USER_PROCESS_PARAMETERS{}.Environment), 0x48))
|
||||
if err != nil || address == 0 {
|
||||
return "", err
|
||||
}
|
||||
var entry []uint16
|
||||
for consumed := uintptr(0); consumed < 32*1024*1024; {
|
||||
var region windows.MemoryBasicInformation
|
||||
if err := windows.VirtualQueryEx(handle, address+consumed, ®ion, unsafe.Sizeof(region)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
size := min(uintptr(4096), region.BaseAddress+region.RegionSize-address-consumed)
|
||||
if size < 2 {
|
||||
return "", errors.New("invalid environment region")
|
||||
}
|
||||
buffer := make([]uint16, size/2)
|
||||
var read uintptr
|
||||
if err := windows.ReadProcessMemory(handle, address+consumed, (*byte)(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)*2), &read); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if read != uintptr(len(buffer)*2) {
|
||||
return "", errors.New("short environment read")
|
||||
}
|
||||
for _, value := range buffer {
|
||||
if value != 0 {
|
||||
entry = append(entry, value)
|
||||
continue
|
||||
}
|
||||
if len(entry) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
key, value, ok := strings.Cut(windows.UTF16ToString(entry), "=")
|
||||
if ok && strings.EqualFold(key, name) {
|
||||
return value, nil
|
||||
}
|
||||
entry = entry[:0]
|
||||
}
|
||||
consumed += read
|
||||
}
|
||||
return "", errors.New("environment exceeds read limit")
|
||||
}
|
||||
|
||||
// readRemoteU16 reads a single uint16 from another process.
|
||||
func readRemoteU16(handle windows.Handle, addr uintptr) (uint16, error) {
|
||||
var v uint16
|
||||
var n uintptr
|
||||
if err := windows.ReadProcessMemory(handle, addr, (*byte)(unsafe.Pointer(&v)), unsafe.Sizeof(v), &n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n != unsafe.Sizeof(v) {
|
||||
return 0, errors.New("short uint16 read")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
Reference in new issue
Block a user