Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions engine/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,23 @@
**9,621**(对照 `f4c50506`)、**9,659**(对照 `be48afbe`),生产 3,852、
测试 5,758。

按包(对照 `26dbcae`,后续分支):
2026-11-12 的 ports 重写在此基础上继续下降:对照 `26dbcae` 为 **8,933** 行 / 664 段,
分布在 135 个文件(生产 3,564、测试 5,369)。

按包(对照 `26dbcae`,ports 重写后):

| 包 | 生产 | 测试 | 合计 |
| --- | ---: | ---: | ---: |
| `internal/daemon` | 2,073 | 3,457 | 5,530 |
| `internal/cmd` | 837 | 703 | 1,540 |
| `internal/groups` | 336 | 506 | 842 |
| `internal/scanner` | 220 | 540 | 760 |
| `internal/ports` | 288 | 389 | 677 |
| `internal/ports` | 0 | 0 | 0 |
| `internal/killer` | 86 | 158 | 244 |
| `internal/store` | 12 | 5 | 17 |

`internal/claims`、`internal/display`、`internal/testenv` 与 `internal/store/claims*.go`
已经归零;`internal/docker` 与 `internal/ports/displayname.go` 只剩结构体字段与表驱动
`internal/claims`、`internal/display`、`internal/testenv`、`internal/ports` 与
`internal/store/claims*.go` 已经归零;`internal/docker` 只剩结构体字段与表驱动
用例数据。`internal/store` 已整包重写:生产代码从 1,076 行降到 199 行。

```sh
Expand Down
256 changes: 97 additions & 159 deletions engine/internal/ports/cwd_peb.go
Original file line number Diff line number Diff line change
@@ -1,205 +1,143 @@
package ports

import (
"encoding/binary"
"strings"
"unicode/utf16"
"unsafe"
)

// Windows keeps a process's current directory in its own address space, in the
// PEB, not in any table the kernel will hand out. Reading it is therefore
// pointer arithmetic over three hops of foreign memory:
// A Windows process keeps its current directory in its own address space, not
// in a table the kernel hands out. Reading it means following three pointers
// through foreign memory:
//
// PEB.ProcessParameters -> RTL_USER_PROCESS_PARAMETERS
// RTL_USER_PROCESS_PARAMETERS.CurrentDirectory.DosPath -> UNICODE_STRING
// UNICODE_STRING.Buffer -> the UTF-16 path itself
// PEB.ProcessParameters -> RTL_USER_PROCESS_PARAMETERS
// ...CurrentDirectory.DosPath -> UNICODE_STRING
// UNICODE_STRING.Buffer -> the UTF-16 path
//
// None of that arithmetic needs Windows to be exercised, so the offsets and the
// decoding live here, in a file that builds everywhere and is unit-tested on
// the developer's machine. cwd_windows.go supplies the three reads.

// pebLayout is the byte geometry of those structures for one pointer size.
// Every field is an offset in bytes from the start of its structure.
type pebLayout struct {
// ptrSize is the width of a pointer in the target process: 8 for a 64-bit
// process read from a 64-bit option-berth, 4 for a 32-bit process read from a
// 32-bit option-berth.
ptrSize int
// processParameters is PEB.ProcessParameters.
processParameters uintptr
// currentDirectory is RTL_USER_PROCESS_PARAMETERS.CurrentDirectory.DosPath,
// i.e. the UNICODE_STRING at the head of the CURDIR.
currentDirectory uintptr
// unicodeBuffer is UNICODE_STRING.Buffer; unicodeSize is sizeof(UNICODE_STRING).
unicodeBuffer uintptr
unicodeSize int
}

// layout64 is the geometry for 64-bit targets, spelled out so a test can pin it
// without trusting the same arithmetic it is checking. These are the numbers
// every debugger prints for ntdll's structures on amd64/arm64.
var layout64 = pebLayout{
ptrSize: 8,
processParameters: 0x20,
currentDirectory: 0x38,
unicodeBuffer: 0x08,
unicodeSize: 0x10,
// Only the offsets depend on the target ABI. Everything else works on plain
// byte slices, so this file builds and is tested on every platform;
// cwd_windows.go supplies the remote reads.

// winDirGeometry is where one target ABI keeps those three structures.
// Offsets are bytes from the start of the structure named by the field.
type winDirGeometry struct {
wordsize int // pointer width in the target process
params uintptr // PEB.ProcessParameters
currentDir uintptr // RTL_USER_PROCESS_PARAMETERS.CurrentDirectory.DosPath
}

// layout32 is the same geometry for 32-bit targets.
var layout32 = pebLayout{
ptrSize: 4,
processParameters: 0x10,
currentDirectory: 0x24,
unicodeBuffer: 0x04,
unicodeSize: 0x08,
}

// The mirror structs below exist only so the layout can be *derived* rather
// than asserted: Go lays them out with the same natural alignment the C
// declarations get, so unsafe.Offsetof reproduces ntdll's offsets for whatever
// word size this binary was built for. They are never allocated.

type mirrorUnicodeString struct {
Length uint16
MaximumLength uint16
Buffer uintptr
}

type mirrorCurDir struct {
DosPath mirrorUnicodeString
Handle uintptr
}

type mirrorProcessParameters struct {
MaximumLength uint32
Length uint32
Flags uint32
DebugFlags uint32
ConsoleHandle uintptr
ConsoleFlags uint32
StandardInput uintptr
StandardOutput uintptr
StandardError uintptr
CurrentDirectory mirrorCurDir
}

type mirrorPEB struct {
InheritedAddressSpace byte
ReadImageFileExecOptions byte
BeingDebugged byte
BitField byte
Mutant uintptr
ImageBaseAddress uintptr
Ldr uintptr
ProcessParameters uintptr
}
// The two Windows ABIs. A 32-bit target has the same structures with
// half-width pointers; the target's word size, not this binary's, picks the
// numbers, so they cannot both be derived from one compiled layout.
var (
winDir64 = winDirGeometry{wordsize: 8, params: 0x20, currentDir: 0x38}
winDir32 = winDirGeometry{wordsize: 4, params: 0x10, currentDir: 0x24}
)

// nativeLayout is the geometry of a process with this binary's own word size.
// A 64-bit option-berth reads 64-bit PEBs — including those of WOW64 processes, which
// keep a populated 64-bit PEB alongside their 32-bit one.
func nativeLayout() pebLayout {
return pebLayout{
ptrSize: int(unsafe.Sizeof(uintptr(0))),
processParameters: unsafe.Offsetof(mirrorPEB{}.ProcessParameters),
currentDirectory: unsafe.Offsetof(mirrorProcessParameters{}.CurrentDirectory),
unicodeBuffer: unsafe.Offsetof(mirrorUnicodeString{}.Buffer),
unicodeSize: int(unsafe.Sizeof(mirrorUnicodeString{})),
// bufferOffset is UNICODE_STRING.Buffer: two 16-bit lengths and then the
// pointer, aligned to the target's word size.
func (g winDirGeometry) bufferOffset() uintptr { return uintptr(g.wordsize) }

// headerSize is sizeof(UNICODE_STRING) in the target: the two lengths are
// padded out to the pointer width, which halves the structure on a 32-bit
// target.
func (g winDirGeometry) headerSize() int { return 2 * g.wordsize }

// selfGeometry is the ABI this binary reads natively. A 64-bit build reads
// 64-bit PEBs, including those of WOW64 processes, which keep a populated
// 64-bit PEB next to their 32-bit one.
func selfGeometry() winDirGeometry {
if unsafe.Sizeof(uintptr(0)) == 8 {
return winDir64
}
return winDir32
}

// pointerAt decodes the little-endian pointer stored at off. ok is false when
// buf is too short, which is how a short or refused ReadProcessMemory surfaces.
func (l pebLayout) pointerAt(buf []byte, off uintptr) (addr uint64, ok bool) {
end := off + uintptr(l.ptrSize)
if uintptr(len(buf)) < end {
// uintptrValue decodes one little-endian pointer of the given width. A short
// buffer is how a refused or truncated remote read arrives here: half a
// pointer would otherwise decode to a plausible-looking wrong address.
func uintptrValue(b []byte, at uintptr, wordsize int) (uint64, bool) {
if at > uintptr(len(b)) || uintptr(len(b))-at < uintptr(wordsize) {
return 0, false
}
for i := l.ptrSize - 1; i >= 0; i-- {
addr = addr<<8 | uint64(buf[off+uintptr(i)])
switch wordsize {
case 8:
return binary.LittleEndian.Uint64(b[at:]), true
case 4:
return uint64(binary.LittleEndian.Uint32(b[at:])), true
default:
return 0, false
}
return addr, true
}

// unicodeStringAt decodes the UNICODE_STRING that starts at off: its Length (in
// bytes, not characters — the field is famously not a count of runes) and the
// address of its buffer. ok is false for a truncated read, an odd Length, an
// empty Length or a null buffer. Length needs no upper bound of its own: it is
// a uint16, so the worst a wrong offset can ask us to allocate is 64 KiB.
func (l pebLayout) unicodeStringAt(buf []byte, off uintptr) (length int, addr uint64, ok bool) {
if uintptr(len(buf)) < off+uintptr(l.unicodeSize) {
return 0, 0, false
}
length = int(buf[off]) | int(buf[off+1])<<8
addr, ok = l.pointerAt(buf, off+l.unicodeBuffer)
if !ok || addr == 0 {
// dosPath decodes the UNICODE_STRING header at the start of b: how many bytes
// the path occupies and where its buffer starts. Length counts bytes, not
// characters, so an odd value is not UTF-16; a zero length or a null buffer
// means the target has no directory to report.
func (g winDirGeometry) dosPath(b []byte) (size int, data uint64, ok bool) {
if len(b) < g.headerSize() {
return 0, 0, false
}
if length <= 0 || length%2 != 0 {
n := int(binary.LittleEndian.Uint16(b[:2]))
addr, ok := uintptrValue(b, g.bufferOffset(), g.wordsize)
if !ok || addr == 0 || n <= 0 || n%2 != 0 {
return 0, 0, false
}
return length, addr, true
return n, addr, true
}

// decodeUTF16LE turns the raw bytes of a UNICODE_STRING buffer into a Go
// string. A trailing NUL is dropped: the PEB's DosPath is usually NUL-padded
// even though Length excludes the terminator, and a stray one would otherwise
// travel all the way into a group name.
func decodeUTF16LE(b []byte) string {
units := make([]uint16, 0, len(b)/2)
for i := 0; i+1 < len(b); i += 2 {
units = append(units, uint16(b[i])|uint16(b[i+1])<<8)
// utf16Text decodes the UTF-16LE bytes of a DosPath. The PEB usually pads the
// buffer with a NUL even though Length excludes it; the terminator is dropped
// so it cannot travel into a group name. A trailing odd byte has no character
// to decode and is ignored.
func utf16Text(b []byte) string {
units := make([]uint16, len(b)/2)
for i := range units {
units[i] = binary.LittleEndian.Uint16(b[2*i:])
}
for len(units) > 0 && units[len(units)-1] == 0 {
units = units[:len(units)-1]
}
return string(utf16.Decode(units))
}

// normalizeWindowsCwd turns the PEB's spelling of a directory into the one the
// rest of option-berth uses as a map key.
//
// Two things have to go. The PEB always stores a trailing backslash
// (`C:\src\api\`), and groups.Canonical — which is filepath.Clean plus
// EvalSymlinks — never produces one, so an unstripped path is a second entry
// for the same directory and the group silently splits in two. And the
// extended-length `\\?\` prefix, which shows up for processes started with a
// long path, is not a spelling any other tool prints: `\\?\C:\src` and
// `C:\src` are the same directory and must not be two groups. `\\?\UNC\host\share`
// is the same escape for a network path and unfolds back to `\\host\share`.
//
// A bare drive root keeps its backslash: `C:\` is the root of C:, while `C:`
// on its own means "whatever directory that drive is parked at", which is a
// different — and unstable — place.
// normalizeWindowsCwd turns the PEB's spelling of a directory into the key the
// rest of option-berth compares. Two decorations have to go: the trailing
// separator the PEB always stores (groups.Canonical never produces one, so it
// would split one project into two groups), and the extended-length `\\?\`
// prefix that long-path launches carry (`\\?\UNC\host\share` unfolds back to
// `\\host\share`). A drive root keeps its separator: `C:\` is fixed, while a
// bare `C:` means wherever that drive happens to be parked.
func normalizeWindowsCwd(raw string) string {
s := strings.TrimSpace(raw)
if s == "" {
return ""
switch {
case strings.HasPrefix(s, `\\?\UNC\`):
s = `\\` + s[len(`\\?\UNC\`):]
case strings.HasPrefix(s, `\\?\`):
s = s[len(`\\?\`):]
}
if rest, found := strings.CutPrefix(s, `\\?\`); found {
if unc, isUNC := strings.CutPrefix(rest, `UNC\`); isUNC {
s = `\\` + unc
} else {
s = rest
}
if isDriveRoot(s) || len(s) < 2 {
return s
}
for len(s) > 1 && (s[len(s)-1] == '\\' || s[len(s)-1] == '/') {
if isDriveRoot(s) {
break
}
s = s[:len(s)-1]
trimmed := strings.TrimRight(s, `\/`)
switch {
case trimmed == s:
return s
case trimmed == "":
return `\`
case len(trimmed) == 2 && trimmed[1] == ':':
// A root written with redundant separators keeps being a root.
return trimmed + `\`
}
return s
return trimmed
}

// isDriveRoot reports whether s is exactly `X:\` or `X:/`.
func isDriveRoot(s string) bool {
if len(s) != 3 || s[1] != ':' {
return false
}
if s[2] != '\\' && s[2] != '/' {
if len(s) != 3 || s[1] != ':' || (s[2] != '\\' && s[2] != '/') {
return false
}
c := s[0]
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
c := s[0] | 0x20
return c >= 'a' && c <= 'z'
}
Loading
Loading