返回 DeepSeek-Reasonix
acl_windows.go
根目录 / internal / winaclresidue / acl_windows.go
1 //go:build windows
2
3 package winaclresidue
4
5 import (
6 "bytes"
7 "context"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "time"
15
16 "golang.org/x/sys/windows"
17
18 "reasonix/internal/proc"
19 )
20
21 const (
22 allApplicationPackagesSID = "S-1-15-2-1"
23 allRestrictedApplicationPackagesSID = "S-1-15-2-2"
24 // stillActiveExitCode is STILL_ACTIVE from GetExitCodeProcess.
25 stillActiveExitCode = 259
26 // icacls can stall on antivirus-scanned volumes; the bound keeps a stuck
27 // cleanup from pinning startup or a credential read.
28 icaclsTimeout = 30 * time.Second
29 )
30
31 // markerDir is the directory the retired backend used for crash-residue
32 // markers; the name is part of the on-disk contract with older builds.
33 func markerDir() string {
34 return filepath.Join(os.TempDir(), "windows-sandbox-denylocks")
35 }
36
37 // SweepStaleMarkers removes the ACEs recorded by crashed runs of the retired
38 // backend whose owning process is provably gone, then deletes their markers.
39 // It is best-effort: errors keep the marker for a later sweep and never block
40 // the caller.
41 func SweepStaleMarkers() {
42 dir := markerDir()
43 entries, err := os.ReadDir(dir)
44 if err != nil {
45 return
46 }
47 sids := residueSIDs()
48 for _, entry := range entries {
49 if entry.IsDir() {
50 continue
51 }
52 pid, ok := markerOwnerPID(entry.Name())
53 if !ok || !processExited(pid) {
54 continue
55 }
56 sweepMarkerFile(filepath.Join(dir, entry.Name()), sids)
57 }
58 }
59
60 // residueSIDs is the exact trustee set the retired backend applied; removing
61 // only these cannot disturb a legitimate ACL.
62 func residueSIDs() []string {
63 userSID, _ := currentProcessUserSIDString()
64 return dedupeSIDStrings([]string{allApplicationPackagesSID, allRestrictedApplicationPackagesSID, userSID})
65 }
66
67 // sweepMarkerFile removes the recorded ACEs and deletes the marker only when
68 // every removal succeeded, so a failed cleanup keeps its evidence for the next
69 // sweep instead of orphaning the ACE on disk.
70 func sweepMarkerFile(markerPath string, sids []string) {
71 clean := true
72 for _, e := range readResidueMarker(markerPath) {
73 if isWindowsSystemRoot(e.path) {
74 continue
75 }
76 if _, err := os.Stat(e.path); err != nil {
77 continue
78 }
79 flag := "/remove:g"
80 if e.kind == residueDeny {
81 flag = "/remove:d"
82 }
83 for _, sid := range sids {
84 if err := icacls(e.path, flag, "*"+sid, "/C"); err != nil {
85 clean = false
86 }
87 }
88 }
89 if clean {
90 _ = os.Remove(markerPath)
91 }
92 }
93
94 // processExited reports whether a marker owner is provably gone. This package
95 // writes no markers, so one carrying our own PID was left by a dead
96 // predecessor after PID reuse. Access denied does not prove death.
97 func processExited(pidText string) bool {
98 if pidText == strconv.Itoa(os.Getpid()) {
99 return true
100 }
101 pid, err := strconv.ParseUint(pidText, 10, 32)
102 if err != nil || pid == 0 {
103 return false
104 }
105 handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
106 if err != nil {
107 return errors.Is(err, windows.ERROR_INVALID_PARAMETER)
108 }
109 defer windows.CloseHandle(handle)
110 var code uint32
111 if err := windows.GetExitCodeProcess(handle, &code); err != nil {
112 return false
113 }
114 return code != stillActiveExitCode
115 }
116
117 func currentProcessUserSIDString() (string, error) {
118 user, err := windows.GetCurrentProcessToken().GetTokenUser()
119 if err != nil {
120 return "", err
121 }
122 if user == nil || user.User.Sid == nil {
123 return "", fmt.Errorf("current process token has no user SID")
124 }
125 return user.User.Sid.String(), nil
126 }
127
128 // systemRootTool resolves a Windows system tool below %SystemRoot%\System32
129 // so a PATH-shadowed binary can never run in its place.
130 func systemRootTool(name string) string {
131 root := os.Getenv("SystemRoot")
132 if root == "" {
133 root = os.Getenv("windir")
134 }
135 if root == "" {
136 root = `C:\Windows`
137 }
138 full := filepath.Join(root, "System32", name)
139 if _, err := os.Stat(full); err == nil {
140 return full
141 }
142 return name
143 }
144
145 func icacls(path string, args ...string) error {
146 ctx, cancel := context.WithTimeout(context.Background(), icaclsTimeout)
147 defer cancel()
148 cmd := proc.CommandContext(ctx, systemRootTool("icacls.exe"), append([]string{path}, args...)...)
149 proc.HideWindow(cmd)
150 var out bytes.Buffer
151 cmd.Stdout, cmd.Stderr = &out, &out
152 if err := cmd.Run(); err != nil {
153 return fmt.Errorf("icacls %q %s: %w: %s", path, strings.Join(args, " "), err, strings.TrimSpace(out.String()))
154 }
155 return nil
156 }
157
157 lines GO