Skip to content

Replace macOS ps exec with in-process sysctl-based Unity process discovery#1882

Merged
hatayama merged 2 commits into
feature/pause-point-feedback-round3-integrationfrom
feat/macos-in-process-unity-process-enum
Jul 20, 2026
Merged

Replace macOS ps exec with in-process sysctl-based Unity process discovery#1882
hatayama merged 2 commits into
feature/pause-point-feedback-round3-integrationfrom
feat/macos-in-process-unity-process-enum

Conversation

@hatayama

@hatayama hatayama commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Unity Editor discovery on macOS no longer depends on exec'ing /bin/ps, so it now works inside sandboxed environments (e.g. Claude Code's default sandbox) that deny exec of external binaries.
  • uloop's plain launch, get-logs, compile, and other commands that need to detect an already-running Unity Editor for this project now succeed in such sandboxes; they previously failed outright because the ps invocation itself was denied.

User Impact

  • Before: any uloop command path that needed to check for a running Unity Editor on macOS shelled out to ps. In sandboxes that block exec of external binaries, this made Unity process discovery fail entirely with a permission error, even though the rest of the command logic could otherwise run.
  • After: discovery reads process info directly via sysctl (kern.proc.all / kern.procargs2), which sandboxes that block ps still permit. Discovery now works in both sandboxed and normal environments with identical results.
  • Residual limitation (out of scope for this PR): launch -r/-q still kill the found Unity process via kill(2) against a non-child PID. Sandboxes that deny process discovery also independently deny sending signals to processes they don't own, so -r/-q will still fail inside such a sandbox even though discovery itself now succeeds. This is a separate, pre-existing sandbox restriction that no process-discovery implementation can work around.

Changes

  • cli/common/unityprocess/process.go: removed the ps -axo ... -ww exec call and its output parser; kept the existing project-path/batchmode matching rules (isUnityEditorCommand, extractProjectPath) and exposed them through a new matchMacUnityProcess(pid, command) helper shared by both the old command-line-based tests and the new argv-based path. Added a build-tag-free parseMacProcArgs2 buffer parser for the kern.procargs2 sysctl layout (leading int32 argc, NUL-terminated exec path, NUL padding, then argc NUL-terminated argv entries; both darwin architectures are little-endian).
  • cli/common/unityprocess/process_darwin.go (new, //go:build darwin): the real sysctl-based listUnityProcessesMac, using golang.org/x/sys/unix's SysctlKinfoProcSlice("kern.proc.all") for enumeration and SysctlRaw("kern.procargs2", pid) for argv. Processes whose argv can't be read (e.g. EPERM for another user's process) are skipped, matching ps's own visibility limits.
  • cli/common/unityprocess/process_other.go (new, //go:build !darwin): a stub so the shared dispatcher compiles on every target OS; never reached at runtime.
  • cli/common/unityprocess/timeouts.go: corrected a comment now that macOS enumeration no longer uses the timed external-command path.
  • cli/common/unityprocess/process_test.go: replaced the old ps-output-parsing test with TestMatchMacUnityProcessExtractsProjectPath (same coverage: quoted path, batchmode-skip, unquoted path) and added fixture-based tests for the buffer parser (TestParseMacProcArgs2DecodesArgv, TestParseMacProcArgs2RejectsShortBuffer, TestParseMacProcArgs2RejectsMissingExecPathTerminator).
  • cli/common/go.mod: promoted golang.org/x/sys from an indirect to a direct dependency (go mod tidy).
  • cli/project-runner/shared-inputs-stamp.json, cli/dispatcher/shared-inputs-stamp.json: refreshed via scripts/stamp-release-inputs.sh per this repo's shared release-input trigger convention, since non-test cli/common/**/*.go sources changed.

Verification

  • sh scripts/check-go-cli.sh: 0 lint issues, all packages pass (including cli/common/unityprocess, cli/project-runner, cli/dispatcher).
  • sh scripts/build-go-cli.sh: rebuilt dev binaries successfully.
  • Sandbox enumeration proof: a standalone Go probe calling unityprocess.FindRunningUnityProcess directly, run under Claude Code's default sandbox (no sandbox override), correctly found the actual running Unity Editor process for this repo. In the same sandbox, plain ps/pgrep both fail with "operation not permitted."
  • Regression check: outside the sandbox, dist/darwin-arm64/uloop launch -r --project-path <project root> still succeeds end-to-end (finds the existing process, kills it, and relaunches), confirming no behavior change for the normal (non-sandboxed) path.
  • This PR targets an integration branch, not main/v3-beta, so repository CI workflows (filtered to [main, v3-beta]) do not fire here; the checks above are the local equivalent.

Review in cubic

…ocess discovery

Sandboxed environments (e.g. Claude Code's default sandbox) deny exec of
external binaries including /bin/ps, which broke Unity Editor discovery
on macOS entirely. Replace listUnityProcessesMac's `ps -axo` invocation
with kern.proc.all / kern.procargs2 sysctl reads via golang.org/x/sys/unix,
which the sandbox permits. Matching/filtering logic (isUnityEditorCommand,
extractProjectPath) is unchanged and now shared via matchMacUnityProcess so
both the old ps-based tests and the new sysctl path exercise the same rule.

Darwin-only syscall code is isolated behind a //go:build darwin file
(process_darwin.go, with a process_other.go stub for other OSes) because
this repo's CI has no macOS runner; the buffer parser and matcher stay in
the shared, build-tag-free process.go so ubuntu-latest CI still covers them.

Verified via a direct Go probe that FindRunningUnityProcess succeeds inside
Claude Code's default sandbox (where ps/pgrep themselves fail with
"operation not permitted"), and that `uloop launch -r` still succeeds
outside the sandbox with no regression. Note: -r/-q's kill of the found
process is a separate, pre-existing sandbox restriction (kill(2) against a
non-child PID) that this change does not and cannot resolve.

Also promotes golang.org/x/sys from an indirect to a direct dependency
(go mod tidy) and refreshes cli/project-runner and cli/dispatcher's
shared-inputs-stamp.json per the shared release-input trigger convention.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7707323-c6a5-44bb-9683-d225c69c2bd0

📥 Commits

Reviewing files that changed from the base of the PR and between 935dbe8 and fae5f32.

📒 Files selected for processing (1)
  • cli/common/unityprocess/process_darwin.go

📝 Walkthrough

Walkthrough

macOS Unity process discovery now uses Darwin sysctl APIs, decodes kern.procargs2 buffers, and matches Unity editor commands to project paths. The previous ps parsing is removed, with platform stubs, focused tests, timeout documentation, and input hashes updated.

Changes

Unity process discovery

Layer / File(s) Summary
Process argument decoding and Unity matching
cli/common/unityprocess/process.go
Replaces macOS ps parsing with parseMacProcArgs2 and matchMacUnityProcess, including little-endian argc decoding and project-path extraction.
Darwin enumeration and platform wiring
cli/common/unityprocess/process_darwin.go, cli/common/unityprocess/process_other.go, cli/common/unityprocess/timeouts.go
Enumerates kern.proc.all, reads per-process kern.procargs2 data, skips unavailable arguments, and provides a non-Darwin unsupported-platform stub.
Parser validation and synchronized inputs
cli/common/unityprocess/process_test.go, cli/dispatcher/shared-inputs-stamp.json, cli/project-runner/shared-inputs-stamp.json
Tests Unity matching and valid or malformed procargs2 buffers while updating shared input hashes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant listUnityProcessesMac
  participant kern.proc.all
  participant macProcessArgs
  participant parseMacProcArgs2
  participant matchMacUnityProcess
  listUnityProcessesMac->>kern.proc.all: enumerate process records
  listUnityProcessesMac->>macProcessArgs: read kern.procargs2 for each PID
  macProcessArgs->>parseMacProcArgs2: decode raw argument buffer
  parseMacProcArgs2-->>macProcessArgs: return executable path and argv
  listUnityProcessesMac->>matchMacUnityProcess: match command and project path
  matchMacUnityProcess-->>listUnityProcessesMac: return matched UnityProcess
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: switching macOS Unity discovery from ps to in-process sysctl-based detection.
Description check ✅ Passed The description is directly related and accurately describes the macOS sysctl-based Unity process discovery change and its impact.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/macos-in-process-unity-process-enum

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The comment claimed ps could not see EPERM'd processes at all, but ps
can list them; it only fails to read another user's argv. Corrected
per advisor review to avoid misleading future readers.
@hatayama
hatayama merged commit fbfcca1 into feature/pause-point-feedback-round3-integration Jul 20, 2026
@hatayama
hatayama deleted the feat/macos-in-process-unity-process-enum branch July 20, 2026 16:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/common/unityprocess/process.go`:
- Around line 118-145: Cap or otherwise validate the raw argc value in
parseMacProcArgs2 before passing it as the capacity to make([]string, 0, argc).
Ensure malformed values cannot trigger an enormous allocation or panic, while
preserving parsing of valid argument counts and the existing short-buffer and
missing-terminator errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73e2d72a-c57e-4683-af73-c64c3cba07b9

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddf777 and 935dbe8.

⛔ Files ignored due to path filters (1)
  • cli/common/go.mod is excluded by none and included by none
📒 Files selected for processing (7)
  • cli/common/unityprocess/process.go
  • cli/common/unityprocess/process_darwin.go
  • cli/common/unityprocess/process_other.go
  • cli/common/unityprocess/process_test.go
  • cli/common/unityprocess/timeouts.go
  • cli/dispatcher/shared-inputs-stamp.json
  • cli/project-runner/shared-inputs-stamp.json

Comment on lines +118 to 145
func parseMacProcArgs2(buf []byte) ([]string, error) {
if len(buf) < 4 {
return nil, fmt.Errorf("procargs2 buffer too short: %d bytes", len(buf))
}

command := matches[2]
if !isUnityEditorCommand(command, macUnityExecutablePattern) {
continue
}
projectPath := extractProjectPath(command)
if projectPath == "" {
continue
}
argc := int(binary.LittleEndian.Uint32(buf[:4]))
rest := buf[4:]

processes = append(processes, UnityProcess{Pid: pid, projectPath: projectPath})
execPathEnd := bytes.IndexByte(rest, 0)
if execPathEnd < 0 {
return nil, fmt.Errorf("procargs2 missing exec path terminator")
}
return processes
rest = rest[execPathEnd:]
for len(rest) > 0 && rest[0] == 0 {
rest = rest[1:]
}

args := make([]string, 0, argc)
for len(rest) > 0 && len(args) < argc {
argEnd := bytes.IndexByte(rest, 0)
if argEnd < 0 {
break
}
args = append(args, string(rest[:argEnd]))
rest = rest[argEnd+1:]
}
return args, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound argc before using it as slice capacity.

argc is read directly from the raw sysctl buffer and used unbounded as make([]string, 0, argc). A corrupted/unexpected buffer with a very large leading value (e.g. near 0xFFFFFFFF) would attempt a huge allocation and could panic, defeating the malformed-buffer hardening this function is otherwise designed and tested for (short-buffer and missing-terminator cases are already rejected).

🛡️ Proposed fix to cap the pre-allocation
+const maxProcArgs2Argv = 4096 // sane upper bound; real argv counts are far smaller
+
 func parseMacProcArgs2(buf []byte) ([]string, error) {
 	if len(buf) < 4 {
 		return nil, fmt.Errorf("procargs2 buffer too short: %d bytes", len(buf))
 	}
 
 	argc := int(binary.LittleEndian.Uint32(buf[:4]))
+	if argc < 0 || argc > maxProcArgs2Argv {
+		return nil, fmt.Errorf("procargs2 argc out of range: %d", argc)
+	}
 	rest := buf[4:]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func parseMacProcArgs2(buf []byte) ([]string, error) {
if len(buf) < 4 {
return nil, fmt.Errorf("procargs2 buffer too short: %d bytes", len(buf))
}
command := matches[2]
if !isUnityEditorCommand(command, macUnityExecutablePattern) {
continue
}
projectPath := extractProjectPath(command)
if projectPath == "" {
continue
}
argc := int(binary.LittleEndian.Uint32(buf[:4]))
rest := buf[4:]
processes = append(processes, UnityProcess{Pid: pid, projectPath: projectPath})
execPathEnd := bytes.IndexByte(rest, 0)
if execPathEnd < 0 {
return nil, fmt.Errorf("procargs2 missing exec path terminator")
}
return processes
rest = rest[execPathEnd:]
for len(rest) > 0 && rest[0] == 0 {
rest = rest[1:]
}
args := make([]string, 0, argc)
for len(rest) > 0 && len(args) < argc {
argEnd := bytes.IndexByte(rest, 0)
if argEnd < 0 {
break
}
args = append(args, string(rest[:argEnd]))
rest = rest[argEnd+1:]
}
return args, nil
}
const maxProcArgs2Argv = 4096 // sane upper bound; real argv counts are far smaller
func parseMacProcArgs2(buf []byte) ([]string, error) {
if len(buf) < 4 {
return nil, fmt.Errorf("procargs2 buffer too short: %d bytes", len(buf))
}
argc := int(binary.LittleEndian.Uint32(buf[:4]))
if argc < 0 || argc > maxProcArgs2Argv {
return nil, fmt.Errorf("procargs2 argc out of range: %d", argc)
}
rest := buf[4:]
execPathEnd := bytes.IndexByte(rest, 0)
if execPathEnd < 0 {
return nil, fmt.Errorf("procargs2 missing exec path terminator")
}
rest = rest[execPathEnd:]
for len(rest) > 0 && rest[0] == 0 {
rest = rest[1:]
}
args := make([]string, 0, argc)
for len(rest) > 0 && len(args) < argc {
argEnd := bytes.IndexByte(rest, 0)
if argEnd < 0 {
break
}
args = append(args, string(rest[:argEnd]))
rest = rest[argEnd+1:]
}
return args, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/common/unityprocess/process.go` around lines 118 - 145, Cap or otherwise
validate the raw argc value in parseMacProcArgs2 before passing it as the
capacity to make([]string, 0, argc). Ensure malformed values cannot trigger an
enormous allocation or panic, while preserving parsing of valid argument counts
and the existing short-buffer and missing-terminator errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant