fix(darwin): harden process inspection and pty tests
- commit
- 27e6e25b8a574980f8ba5a599ef9ed67f33f7847
- author
- brice · · 2026-07-16 18:45 UTC
- parent
- c46ba52
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +131 -32
M internal/adapters/pty/pty_darwin_test.go +1 -14
| @@ -57,20 +57,7 @@ func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |||
| 57 | 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | |
| 58 | 58 | require.NoError(t, err) | |
| 59 | 59 | ||
| 60 | - | buf := make([]byte, 256) | |
| 61 | - | deadline := time.Now().Add(5 * time.Second) | |
| 62 | - | var output string | |
| 63 | - | for !strings.Contains(output, "roundtrip") { | |
| 64 | - | if time.Now().After(deadline) { | |
| 65 | - | t.Fatalf("did not read echoed data, got %q", output) | |
| 66 | - | } | |
| 67 | - | n, readErr := echoPTY.Read(buf) | |
| 68 | - | output += string(buf[:n]) | |
| 69 | - | if readErr != nil { | |
| 70 | - | require.ErrorIs(t, readErr, io.EOF) | |
| 71 | - | break | |
| 72 | - | } | |
| 73 | - | } | |
| 60 | + | output := readUntilDarwin(t, echoPTY, "roundtrip", 5*time.Second) | |
| 74 | 61 | require.Contains(t, output, "roundtrip") | |
| 75 | 62 | } | |
| 76 | 63 | ||
| @@ -57,20 +57,7 @@ func TestOpen_DarwinEchoAndOutputEOF(t *testing.T) { | |||||
| 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | 57 | _, err = io.WriteString(echoPTY, "roundtrip\n") | ||
| 58 | require.NoError(t, err) | 58 | require.NoError(t, err) | ||
| 59 | 59 | ||||
| 60 | - | buf := make([]byte, 256) | 60 | + | output := readUntilDarwin(t, echoPTY, "roundtrip", 5*time.Second) |
| 61 | - | deadline := time.Now().Add(5 * time.Second) | |||
| 62 | - | var output string | |||
| 63 | - | for !strings.Contains(output, "roundtrip") { | |||
| 64 | - | if time.Now().After(deadline) { | |||
| 65 | - | t.Fatalf("did not read echoed data, got %q", output) | |||
| 66 | - | } | |||
| 67 | - | n, readErr := echoPTY.Read(buf) | |||
| 68 | - | output += string(buf[:n]) | |||
| 69 | - | if readErr != nil { | |||
| 70 | - | require.ErrorIs(t, readErr, io.EOF) | |||
| 71 | - | break | |||
| 72 | - | } | |||
| 73 | - | } | |||
| 74 | require.Contains(t, output, "roundtrip") | 61 | require.Contains(t, output, "roundtrip") | ||
| 75 | } | 62 | } | ||
| 76 | 63 | ||||
M internal/platform/process_darwin_arm64.go +27 -8
| @@ -22,6 +22,8 @@ const ( | |||
| 22 | 22 | procPIDVnodePathInfo = 9 | |
| 23 | 23 | darwinKinfoProcSize = 0x288 | |
| 24 | 24 | darwinVnodePathInfoLen = 2352 | |
| 25 | + | maxDarwinPID = 1<<31 - 1 | |
| 26 | + | rawSysctlMaxRetries = 3 | |
| 25 | 27 | ) | |
| 26 | 28 | ||
| 27 | 29 | // ProcessInspector implements ports.ProcessInspector using Darwin sysctl and | |
| @@ -100,10 +102,11 @@ func ProcessCwd(pid int) (string, error) { | |||
| 100 | 102 | ||
| 101 | 103 | // ProcessComm returns the p_comm field from KERN_PROC_PID data. | |
| 102 | 104 | func ProcessComm(pid int) (string, error) { | |
| 103 | - | if pid <= 0 { | |
| 105 | + | pid32, ok := darwinPID(pid) | |
| 106 | + | if !ok { | |
| 104 | 107 | return "", fmt.Errorf("process comm: invalid pid %d", pid) | |
| 105 | 108 | } | |
| 106 | - | data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, int32(pid)}) | |
| 109 | + | data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, pid32}) | |
| 107 | 110 | if err != nil { | |
| 108 | 111 | return "", fmt.Errorf("process comm: sysctl pid %d: %w", pid, err) | |
| 109 | 112 | } | |
| @@ -124,10 +127,11 @@ func ProcessComm(pid int) (string, error) { | |||
| 124 | 127 | ||
| 125 | 128 | // ProcessArgv returns argv from KERN_PROCARGS2. | |
| 126 | 129 | func ProcessArgv(pid int) ([]string, error) { | |
| 127 | - | if pid <= 0 { | |
| 130 | + | pid32, ok := darwinPID(pid) | |
| 131 | + | if !ok { | |
| 128 | 132 | return nil, fmt.Errorf("process argv: invalid pid %d", pid) | |
| 129 | 133 | } | |
| 130 | - | data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, int32(pid)}) | |
| 134 | + | data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, pid32}) | |
| 131 | 135 | if err != nil { | |
| 132 | 136 | return nil, fmt.Errorf("process argv: sysctl pid %d: %w", pid, err) | |
| 133 | 137 | } | |
| @@ -162,12 +166,23 @@ func readDarwinProcessRecords() ([]processRecord, error) { | |||
| 162 | 166 | return recs, nil | |
| 163 | 167 | } | |
| 164 | 168 | ||
| 169 | + | func darwinPID(pid int) (int32, bool) { | |
| 170 | + | if pid <= 0 || pid > maxDarwinPID { | |
| 171 | + | return 0, false | |
| 172 | + | } | |
| 173 | + | return int32(pid), true | |
| 174 | + | } | |
| 175 | + | ||
| 165 | 176 | // rawSysctl reads a binary sysctl MIB, retrying if a growing result races the | |
| 166 | 177 | // size query. It is the stdlib syscall equivalent of sysctl(3). | |
| 167 | 178 | func rawSysctl(mib []int32) ([]byte, error) { | |
| 168 | - | for { | |
| 179 | + | return rawSysctlWith(mib, sysctl) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func rawSysctlWith(mib []int32, call func([]int32, *byte, *uintptr) error) ([]byte, error) { | |
| 183 | + | for retry := 0; retry <= rawSysctlMaxRetries; retry++ { | |
| 169 | 184 | var size uintptr | |
| 170 | - | if err := sysctl(mib, nil, &size); err != nil { | |
| 185 | + | if err := call(mib, nil, &size); err != nil { | |
| 171 | 186 | return nil, err | |
| 172 | 187 | } | |
| 173 | 188 | if size == 0 { | |
| @@ -175,14 +190,18 @@ func rawSysctl(mib []int32) ([]byte, error) { | |||
| 175 | 190 | } | |
| 176 | 191 | buf := make([]byte, size) | |
| 177 | 192 | n := size | |
| 178 | - | if err := sysctl(mib, &buf[0], &n); err != nil { | |
| 179 | - | if err == syscall.ENOMEM { | |
| 193 | + | if err := call(mib, &buf[0], &n); err != nil { | |
| 194 | + | if err == syscall.ENOMEM && retry < rawSysctlMaxRetries { | |
| 180 | 195 | continue | |
| 181 | 196 | } | |
| 197 | + | if err == syscall.ENOMEM { | |
| 198 | + | return nil, fmt.Errorf("sysctl %v: result grew after %d retries: %w", mib, rawSysctlMaxRetries, err) | |
| 199 | + | } | |
| 182 | 200 | return nil, err | |
| 183 | 201 | } | |
| 184 | 202 | return buf[:n], nil | |
| 185 | 203 | } | |
| 204 | + | panic("unreachable") | |
| 186 | 205 | } | |
| 187 | 206 | ||
| 188 | 207 | func sysctl(mib []int32, old *byte, oldlen *uintptr) error { | |
| @@ -22,6 +22,8 @@ const ( | |||||
| 22 | procPIDVnodePathInfo = 9 | 22 | procPIDVnodePathInfo = 9 | ||
| 23 | darwinKinfoProcSize = 0x288 | 23 | darwinKinfoProcSize = 0x288 | ||
| 24 | darwinVnodePathInfoLen = 2352 | 24 | darwinVnodePathInfoLen = 2352 | ||
| 25 | + | maxDarwinPID = 1<<31 - 1 | |||
| 26 | + | rawSysctlMaxRetries = 3 | |||
| 25 | ) | 27 | ) | ||
| 26 | 28 | ||||
| 27 | // ProcessInspector implements ports.ProcessInspector using Darwin sysctl and | 29 | // ProcessInspector implements ports.ProcessInspector using Darwin sysctl and | ||
| @@ -100,10 +102,11 @@ func ProcessCwd(pid int) (string, error) { | |||||
| 100 | 102 | ||||
| 101 | // ProcessComm returns the p_comm field from KERN_PROC_PID data. | 103 | // ProcessComm returns the p_comm field from KERN_PROC_PID data. | ||
| 102 | func ProcessComm(pid int) (string, error) { | 104 | func ProcessComm(pid int) (string, error) { | ||
| 103 | - | if pid <= 0 { | 105 | + | pid32, ok := darwinPID(pid) |
| 106 | + | if !ok { | |||
| 104 | return "", fmt.Errorf("process comm: invalid pid %d", pid) | 107 | return "", fmt.Errorf("process comm: invalid pid %d", pid) | ||
| 105 | } | 108 | } | ||
| 106 | - | data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, int32(pid)}) | 109 | + | data, err := rawSysctl([]int32{ctlKern, kernProc, kernProcPID, pid32}) |
| 107 | if err != nil { | 110 | if err != nil { | ||
| 108 | return "", fmt.Errorf("process comm: sysctl pid %d: %w", pid, err) | 111 | return "", fmt.Errorf("process comm: sysctl pid %d: %w", pid, err) | ||
| 109 | } | 112 | } | ||
| @@ -124,10 +127,11 @@ func ProcessComm(pid int) (string, error) { | |||||
| 124 | 127 | ||||
| 125 | // ProcessArgv returns argv from KERN_PROCARGS2. | 128 | // ProcessArgv returns argv from KERN_PROCARGS2. | ||
| 126 | func ProcessArgv(pid int) ([]string, error) { | 129 | func ProcessArgv(pid int) ([]string, error) { | ||
| 127 | - | if pid <= 0 { | 130 | + | pid32, ok := darwinPID(pid) |
| 131 | + | if !ok { | |||
| 128 | return nil, fmt.Errorf("process argv: invalid pid %d", pid) | 132 | return nil, fmt.Errorf("process argv: invalid pid %d", pid) | ||
| 129 | } | 133 | } | ||
| 130 | - | data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, int32(pid)}) | 134 | + | data, err := rawSysctl([]int32{ctlKern, kernProcArgs2, pid32}) |
| 131 | if err != nil { | 135 | if err != nil { | ||
| 132 | return nil, fmt.Errorf("process argv: sysctl pid %d: %w", pid, err) | 136 | return nil, fmt.Errorf("process argv: sysctl pid %d: %w", pid, err) | ||
| 133 | } | 137 | } | ||
| @@ -162,12 +166,23 @@ func readDarwinProcessRecords() ([]processRecord, error) { | |||||
| 162 | return recs, nil | 166 | return recs, nil | ||
| 163 | } | 167 | } | ||
| 164 | 168 | ||||
| 169 | + | func darwinPID(pid int) (int32, bool) { | |||
| 170 | + | if pid <= 0 || pid > maxDarwinPID { | |||
| 171 | + | return 0, false | |||
| 172 | + | } | |||
| 173 | + | return int32(pid), true | |||
| 174 | + | } | |||
| 175 | + | ||||
| 165 | // rawSysctl reads a binary sysctl MIB, retrying if a growing result races the | 176 | // rawSysctl reads a binary sysctl MIB, retrying if a growing result races the | ||
| 166 | // size query. It is the stdlib syscall equivalent of sysctl(3). | 177 | // size query. It is the stdlib syscall equivalent of sysctl(3). | ||
| 167 | func rawSysctl(mib []int32) ([]byte, error) { | 178 | func rawSysctl(mib []int32) ([]byte, error) { | ||
| 168 | - | for { | 179 | + | return rawSysctlWith(mib, sysctl) |
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | func rawSysctlWith(mib []int32, call func([]int32, *byte, *uintptr) error) ([]byte, error) { | |||
| 183 | + | for retry := 0; retry <= rawSysctlMaxRetries; retry++ { | |||
| 169 | var size uintptr | 184 | var size uintptr | ||
| 170 | - | if err := sysctl(mib, nil, &size); err != nil { | 185 | + | if err := call(mib, nil, &size); err != nil { |
| 171 | return nil, err | 186 | return nil, err | ||
| 172 | } | 187 | } | ||
| 173 | if size == 0 { | 188 | if size == 0 { | ||
| @@ -175,14 +190,18 @@ func rawSysctl(mib []int32) ([]byte, error) { | |||||
| 175 | } | 190 | } | ||
| 176 | buf := make([]byte, size) | 191 | buf := make([]byte, size) | ||
| 177 | n := size | 192 | n := size | ||
| 178 | - | if err := sysctl(mib, &buf[0], &n); err != nil { | 193 | + | if err := call(mib, &buf[0], &n); err != nil { |
| 179 | - | if err == syscall.ENOMEM { | 194 | + | if err == syscall.ENOMEM && retry < rawSysctlMaxRetries { |
| 180 | continue | 195 | continue | ||
| 181 | } | 196 | } | ||
| 197 | + | if err == syscall.ENOMEM { | |||
| 198 | + | return nil, fmt.Errorf("sysctl %v: result grew after %d retries: %w", mib, rawSysctlMaxRetries, err) | |||
| 199 | + | } | |||
| 182 | return nil, err | 200 | return nil, err | ||
| 183 | } | 201 | } | ||
| 184 | return buf[:n], nil | 202 | return buf[:n], nil | ||
| 185 | } | 203 | } | ||
| 204 | + | panic("unreachable") | |||
| 186 | } | 205 | } | ||
| 187 | 206 | ||||
| 188 | func sysctl(mib []int32, old *byte, oldlen *uintptr) error { | 207 | func sysctl(mib []int32, old *byte, oldlen *uintptr) error { | ||
M internal/platform/process_darwin_arm64_test.go +90 -10
| @@ -3,7 +3,11 @@ | |||
| 3 | 3 | package platform | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "errors" | |
| 6 | 7 | "os" | |
| 8 | + | "os/exec" | |
| 9 | + | "path/filepath" | |
| 10 | + | "strings" | |
| 7 | 11 | "syscall" | |
| 8 | 12 | "testing" | |
| 9 | 13 | "unsafe" | |
| @@ -37,31 +41,107 @@ func TestDarwinProcessInspectorCurrentProcess(t *testing.T) { | |||
| 37 | 41 | if err != nil { | |
| 38 | 42 | t.Fatalf("Cwd(%d): %v", pid, err) | |
| 39 | 43 | } | |
| 40 | - | if cwd == "" { | |
| 41 | - | t.Fatal("Cwd returned an empty path") | |
| 44 | + | cwdInfo, err := os.Stat(cwd) | |
| 45 | + | if err != nil { | |
| 46 | + | t.Fatalf("stat Cwd(%d) result %q: %v", pid, cwd, err) | |
| 47 | + | } | |
| 48 | + | currentDirInfo, err := os.Stat(".") | |
| 49 | + | if err != nil { | |
| 50 | + | t.Fatal(err) | |
| 51 | + | } | |
| 52 | + | if !os.SameFile(cwdInfo, currentDirInfo) { | |
| 53 | + | t.Fatalf("Cwd(%d) = %q; does not identify the current directory", pid, cwd) | |
| 42 | 54 | } | |
| 43 | 55 | ||
| 44 | 56 | comm, err := inspector.Comm(pid) | |
| 45 | 57 | if err != nil { | |
| 46 | 58 | t.Fatalf("Comm(%d): %v", pid, err) | |
| 47 | 59 | } | |
| 48 | - | if comm == "" { | |
| 49 | - | t.Fatal("Comm returned an empty command") | |
| 60 | + | wantComm := filepath.Base(os.Args[0]) | |
| 61 | + | if len(wantComm) > len(externProc{}.Comm)-1 { | |
| 62 | + | wantComm = wantComm[:len(externProc{}.Comm)-1] | |
| 63 | + | } | |
| 64 | + | if comm != wantComm { | |
| 65 | + | t.Fatalf("Comm(%d) = %q; want %q", pid, comm, wantComm) | |
| 50 | 66 | } | |
| 51 | 67 | ||
| 52 | 68 | argv, err := inspector.Argv(pid) | |
| 53 | 69 | if err != nil { | |
| 54 | 70 | t.Fatalf("Argv(%d): %v", pid, err) | |
| 55 | 71 | } | |
| 56 | - | if len(argv) == 0 { | |
| 57 | - | t.Fatal("Argv returned no arguments") | |
| 72 | + | if len(argv) == 0 || argv[0] != os.Args[0] { | |
| 73 | + | t.Fatalf("Argv(%d) = %#v; want argv[0] %q", pid, argv, os.Args[0]) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | cmd := exec.Command("/bin/sleep", "5") | |
| 77 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | |
| 78 | + | if err := cmd.Start(); err != nil { | |
| 79 | + | t.Fatal(err) | |
| 80 | + | } | |
| 81 | + | defer func() { | |
| 82 | + | _ = cmd.Process.Kill() | |
| 83 | + | _ = cmd.Wait() | |
| 84 | + | }() | |
| 85 | + | groupArgv, err := inspector.GroupArgv(cmd.Process.Pid, -1) | |
| 86 | + | if err != nil { | |
| 87 | + | t.Fatalf("GroupArgv(child group): %v", err) | |
| 88 | + | } | |
| 89 | + | if len(groupArgv) != 2 || groupArgv[0] != "/bin/sleep" || groupArgv[1] != "5" { | |
| 90 | + | t.Fatalf("GroupArgv(child group) = %#v; want [/bin/sleep 5]", groupArgv) | |
| 58 | 91 | } | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func TestDarwinPIDRejectsUnrepresentableValues(t *testing.T) { | |
| 95 | + | for _, pid := range []int{0, -1, maxDarwinPID + 1} { | |
| 96 | + | if _, err := ProcessComm(pid); err == nil { | |
| 97 | + | t.Errorf("ProcessComm(%d) succeeded; want invalid PID error", pid) | |
| 98 | + | } | |
| 99 | + | if _, err := ProcessArgv(pid); err == nil { | |
| 100 | + | t.Errorf("ProcessArgv(%d) succeeded; want invalid PID error", pid) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | } | |
| 59 | 104 | ||
| 60 | - | groupArgv, err := inspector.GroupArgv(syscall.Getpgrp(), -1) | |
| 105 | + | func TestRawSysctlRetriesAfterENOMEM(t *testing.T) { | |
| 106 | + | var sizeCalls, readCalls int | |
| 107 | + | data, err := rawSysctlWith([]int32{1, 2}, func(_ []int32, old *byte, oldlen *uintptr) error { | |
| 108 | + | if old == nil { | |
| 109 | + | sizeCalls++ | |
| 110 | + | *oldlen = 1 | |
| 111 | + | return nil | |
| 112 | + | } | |
| 113 | + | readCalls++ | |
| 114 | + | if readCalls == 1 { | |
| 115 | + | return syscall.ENOMEM | |
| 116 | + | } | |
| 117 | + | *old = 'x' | |
| 118 | + | *oldlen = 1 | |
| 119 | + | return nil | |
| 120 | + | }) | |
| 61 | 121 | if err != nil { | |
| 62 | - | t.Fatalf("GroupArgv(current group): %v", err) | |
| 122 | + | t.Fatal(err) | |
| 123 | + | } | |
| 124 | + | if string(data) != "x" || sizeCalls != 2 || readCalls != 2 { | |
| 125 | + | t.Fatalf("rawSysctl calls = size %d, read %d, data %q; want 2, 2, x", sizeCalls, readCalls, data) | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func TestRawSysctlStopsRetryingAfterLimit(t *testing.T) { | |
| 130 | + | var sizeCalls, readCalls int | |
| 131 | + | _, err := rawSysctlWith([]int32{1, 2}, func(_ []int32, old *byte, oldlen *uintptr) error { | |
| 132 | + | if old == nil { | |
| 133 | + | sizeCalls++ | |
| 134 | + | *oldlen = 1 | |
| 135 | + | return nil | |
| 136 | + | } | |
| 137 | + | readCalls++ | |
| 138 | + | return syscall.ENOMEM | |
| 139 | + | }) | |
| 140 | + | if !errors.Is(err, syscall.ENOMEM) || !strings.Contains(err.Error(), "result grew after") { | |
| 141 | + | t.Fatalf("rawSysctl exhaustion error = %v; want contextual ENOMEM", err) | |
| 63 | 142 | } | |
| 64 | - | if len(groupArgv) == 0 { | |
| 65 | - | t.Fatal("GroupArgv returned no arguments for the current process group") | |
| 143 | + | wantCalls := rawSysctlMaxRetries + 1 | |
| 144 | + | if sizeCalls != wantCalls || readCalls != wantCalls { | |
| 145 | + | t.Fatalf("rawSysctl calls = size %d, read %d; want %d each", sizeCalls, readCalls, wantCalls) | |
| 66 | 146 | } | |
| 67 | 147 | } | |
| @@ -3,7 +3,11 @@ | |||||
| 3 | package platform | 3 | package platform | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "errors" | |||
| 6 | "os" | 7 | "os" | ||
| 8 | + | "os/exec" | |||
| 9 | + | "path/filepath" | |||
| 10 | + | "strings" | |||
| 7 | "syscall" | 11 | "syscall" | ||
| 8 | "testing" | 12 | "testing" | ||
| 9 | "unsafe" | 13 | "unsafe" | ||
| @@ -37,31 +41,107 @@ func TestDarwinProcessInspectorCurrentProcess(t *testing.T) { | |||||
| 37 | if err != nil { | 41 | if err != nil { | ||
| 38 | t.Fatalf("Cwd(%d): %v", pid, err) | 42 | t.Fatalf("Cwd(%d): %v", pid, err) | ||
| 39 | } | 43 | } | ||
| 40 | - | if cwd == "" { | 44 | + | cwdInfo, err := os.Stat(cwd) |
| 41 | - | t.Fatal("Cwd returned an empty path") | 45 | + | if err != nil { |
| 46 | + | t.Fatalf("stat Cwd(%d) result %q: %v", pid, cwd, err) | |||
| 47 | + | } | |||
| 48 | + | currentDirInfo, err := os.Stat(".") | |||
| 49 | + | if err != nil { | |||
| 50 | + | t.Fatal(err) | |||
| 51 | + | } | |||
| 52 | + | if !os.SameFile(cwdInfo, currentDirInfo) { | |||
| 53 | + | t.Fatalf("Cwd(%d) = %q; does not identify the current directory", pid, cwd) | |||
| 42 | } | 54 | } | ||
| 43 | 55 | ||||
| 44 | comm, err := inspector.Comm(pid) | 56 | comm, err := inspector.Comm(pid) | ||
| 45 | if err != nil { | 57 | if err != nil { | ||
| 46 | t.Fatalf("Comm(%d): %v", pid, err) | 58 | t.Fatalf("Comm(%d): %v", pid, err) | ||
| 47 | } | 59 | } | ||
| 48 | - | if comm == "" { | 60 | + | wantComm := filepath.Base(os.Args[0]) |
| 49 | - | t.Fatal("Comm returned an empty command") | 61 | + | if len(wantComm) > len(externProc{}.Comm)-1 { |
| 62 | + | wantComm = wantComm[:len(externProc{}.Comm)-1] | |||
| 63 | + | } | |||
| 64 | + | if comm != wantComm { | |||
| 65 | + | t.Fatalf("Comm(%d) = %q; want %q", pid, comm, wantComm) | |||
| 50 | } | 66 | } | ||
| 51 | 67 | ||||
| 52 | argv, err := inspector.Argv(pid) | 68 | argv, err := inspector.Argv(pid) | ||
| 53 | if err != nil { | 69 | if err != nil { | ||
| 54 | t.Fatalf("Argv(%d): %v", pid, err) | 70 | t.Fatalf("Argv(%d): %v", pid, err) | ||
| 55 | } | 71 | } | ||
| 56 | - | if len(argv) == 0 { | 72 | + | if len(argv) == 0 || argv[0] != os.Args[0] { |
| 57 | - | t.Fatal("Argv returned no arguments") | 73 | + | t.Fatalf("Argv(%d) = %#v; want argv[0] %q", pid, argv, os.Args[0]) |
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | cmd := exec.Command("/bin/sleep", "5") | |||
| 77 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | |||
| 78 | + | if err := cmd.Start(); err != nil { | |||
| 79 | + | t.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | defer func() { | |||
| 82 | + | _ = cmd.Process.Kill() | |||
| 83 | + | _ = cmd.Wait() | |||
| 84 | + | }() | |||
| 85 | + | groupArgv, err := inspector.GroupArgv(cmd.Process.Pid, -1) | |||
| 86 | + | if err != nil { | |||
| 87 | + | t.Fatalf("GroupArgv(child group): %v", err) | |||
| 88 | + | } | |||
| 89 | + | if len(groupArgv) != 2 || groupArgv[0] != "/bin/sleep" || groupArgv[1] != "5" { | |||
| 90 | + | t.Fatalf("GroupArgv(child group) = %#v; want [/bin/sleep 5]", groupArgv) | |||
| 58 | } | 91 | } | ||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func TestDarwinPIDRejectsUnrepresentableValues(t *testing.T) { | |||
| 95 | + | for _, pid := range []int{0, -1, maxDarwinPID + 1} { | |||
| 96 | + | if _, err := ProcessComm(pid); err == nil { | |||
| 97 | + | t.Errorf("ProcessComm(%d) succeeded; want invalid PID error", pid) | |||
| 98 | + | } | |||
| 99 | + | if _, err := ProcessArgv(pid); err == nil { | |||
| 100 | + | t.Errorf("ProcessArgv(%d) succeeded; want invalid PID error", pid) | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 59 | 104 | ||||
| 60 | - | groupArgv, err := inspector.GroupArgv(syscall.Getpgrp(), -1) | 105 | + | func TestRawSysctlRetriesAfterENOMEM(t *testing.T) { |
| 106 | + | var sizeCalls, readCalls int | |||
| 107 | + | data, err := rawSysctlWith([]int32{1, 2}, func(_ []int32, old *byte, oldlen *uintptr) error { | |||
| 108 | + | if old == nil { | |||
| 109 | + | sizeCalls++ | |||
| 110 | + | *oldlen = 1 | |||
| 111 | + | return nil | |||
| 112 | + | } | |||
| 113 | + | readCalls++ | |||
| 114 | + | if readCalls == 1 { | |||
| 115 | + | return syscall.ENOMEM | |||
| 116 | + | } | |||
| 117 | + | *old = 'x' | |||
| 118 | + | *oldlen = 1 | |||
| 119 | + | return nil | |||
| 120 | + | }) | |||
| 61 | if err != nil { | 121 | if err != nil { | ||
| 62 | - | t.Fatalf("GroupArgv(current group): %v", err) | 122 | + | t.Fatal(err) |
| 123 | + | } | |||
| 124 | + | if string(data) != "x" || sizeCalls != 2 || readCalls != 2 { | |||
| 125 | + | t.Fatalf("rawSysctl calls = size %d, read %d, data %q; want 2, 2, x", sizeCalls, readCalls, data) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func TestRawSysctlStopsRetryingAfterLimit(t *testing.T) { | |||
| 130 | + | var sizeCalls, readCalls int | |||
| 131 | + | _, err := rawSysctlWith([]int32{1, 2}, func(_ []int32, old *byte, oldlen *uintptr) error { | |||
| 132 | + | if old == nil { | |||
| 133 | + | sizeCalls++ | |||
| 134 | + | *oldlen = 1 | |||
| 135 | + | return nil | |||
| 136 | + | } | |||
| 137 | + | readCalls++ | |||
| 138 | + | return syscall.ENOMEM | |||
| 139 | + | }) | |||
| 140 | + | if !errors.Is(err, syscall.ENOMEM) || !strings.Contains(err.Error(), "result grew after") { | |||
| 141 | + | t.Fatalf("rawSysctl exhaustion error = %v; want contextual ENOMEM", err) | |||
| 63 | } | 142 | } | ||
| 64 | - | if len(groupArgv) == 0 { | 143 | + | wantCalls := rawSysctlMaxRetries + 1 |
| 65 | - | t.Fatal("GroupArgv returned no arguments for the current process group") | 144 | + | if sizeCalls != wantCalls || readCalls != wantCalls { |
| 145 | + | t.Fatalf("rawSysctl calls = size %d, read %d; want %d each", sizeCalls, readCalls, wantCalls) | |||
| 66 | } | 146 | } | ||
| 67 | } | 147 | } | ||
M pkg/rawterm/rawterm_test.go +13 -0
| @@ -90,6 +90,11 @@ func TestMakeRawRestore(t *testing.T) { | |||
| 90 | 90 | ||
| 91 | 91 | fd := int(slave.Fd()) | |
| 92 | 92 | ||
| 93 | + | var original syscall.Termios | |
| 94 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&original)); err != nil { | |
| 95 | + | t.Fatalf("get termios before MakeRaw: %v", err) | |
| 96 | + | } | |
| 97 | + | ||
| 93 | 98 | old, err := MakeRaw(fd) | |
| 94 | 99 | if err != nil { | |
| 95 | 100 | t.Fatalf("MakeRaw: %v", err) | |
| @@ -115,6 +120,14 @@ func TestMakeRawRestore(t *testing.T) { | |||
| 115 | 120 | if err := Restore(fd, old); err != nil { | |
| 116 | 121 | t.Fatalf("Restore: %v", err) | |
| 117 | 122 | } | |
| 123 | + | ||
| 124 | + | var restored syscall.Termios | |
| 125 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&restored)); err != nil { | |
| 126 | + | t.Fatalf("get termios after Restore: %v", err) | |
| 127 | + | } | |
| 128 | + | if restored != original { | |
| 129 | + | t.Errorf("termios after Restore = %#v, want original %#v", restored, original) | |
| 130 | + | } | |
| 118 | 131 | } | |
| 119 | 132 | ||
| 120 | 133 | func TestForegroundProcessGroup(t *testing.T) { | |
| @@ -90,6 +90,11 @@ func TestMakeRawRestore(t *testing.T) { | |||||
| 90 | 90 | ||||
| 91 | fd := int(slave.Fd()) | 91 | fd := int(slave.Fd()) | ||
| 92 | 92 | ||||
| 93 | + | var original syscall.Termios | |||
| 94 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&original)); err != nil { | |||
| 95 | + | t.Fatalf("get termios before MakeRaw: %v", err) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 93 | old, err := MakeRaw(fd) | 98 | old, err := MakeRaw(fd) | ||
| 94 | if err != nil { | 99 | if err != nil { | ||
| 95 | t.Fatalf("MakeRaw: %v", err) | 100 | t.Fatalf("MakeRaw: %v", err) | ||
| @@ -115,6 +120,14 @@ func TestMakeRawRestore(t *testing.T) { | |||||
| 115 | if err := Restore(fd, old); err != nil { | 120 | if err := Restore(fd, old); err != nil { | ||
| 116 | t.Fatalf("Restore: %v", err) | 121 | t.Fatalf("Restore: %v", err) | ||
| 117 | } | 122 | } | ||
| 123 | + | ||||
| 124 | + | var restored syscall.Termios | |||
| 125 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&restored)); err != nil { | |||
| 126 | + | t.Fatalf("get termios after Restore: %v", err) | |||
| 127 | + | } | |||
| 128 | + | if restored != original { | |||
| 129 | + | t.Errorf("termios after Restore = %#v, want original %#v", restored, original) | |||
| 130 | + | } | |||
| 118 | } | 131 | } | ||
| 119 | 132 | ||||
| 120 | func TestForegroundProcessGroup(t *testing.T) { | 133 | func TestForegroundProcessGroup(t *testing.T) { | ||