Skip to content

fix(engine/php): probe the workspace with os stat, not the sandboxed medium - #49

Merged
Snider merged 1 commit into
mainfrom
fix/php-workspace-symlink-probe
Aug 8, 2026
Merged

fix(engine/php): probe the workspace with os stat, not the sandboxed medium#49
Snider merged 1 commit into
mainfrom
fix/php-workspace-symlink-probe

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Found by sweeping go-render for the go-io os.Root landmine flagged in the estate (the same one that broke go-container's docker detection). go-render has a hit, in engine/php.

Mechanism

go-io v0.15.2 moved the local medium onto os.Root, which by design refuses any path leaving its root — including one that merely traverses a symlink. Correct for sandboxed media; wrong for a search that is unbounded by construction.

loadWorkspaceConfig and findWorkspaceRoot walk up an arbitrary ancestor chain to the filesystem root, so no medium root can contain them. Reached through a symlinked project directory — an ordinary macOS layout, and what any git worktree or /tmp-based test produces:

io.Local.IsFile(via symlink) = false        (os.Stat succeeds)
io.Local.Read(via symlink)   = "path escapes from parent"

Those are exactly the two branches loadWorkspaceConfig used to conclude "no config here" — so it walked straight past a config that exists and reported the workspace as unconfigured. Silently: no error, just a default config.

Fix

Both probes use the os-level core.Stat / core.ReadFile, already the house idiom for probing in this repo — display/webkit/pkg/container/core_helpers.go resolves PATH the same way. go-io is correct and untouched; coreio media stay in place for the sandboxed I/O everywhere else.

Receipt

TestLoadWorkspaceConfig_ThroughSymlink against the old coreio path:

--- FAIL: .../through_symlink        config not found
--- FAIL: .../child_through_symlink  config not found
--- PASS: .../direct

Against this fix all three PASS, plus TestIsRegularFile covering file / directory / absent, each also via a symlink.

Sweep result for the rest of the repo

  • coreio.Local.Statzero call sites.
  • coreio.Local.IsFile (the other os.Root-routed probe) — 2 sites, both fixed here.
  • Everything else: Read / Write / Open / Create / Append / EnsureDir on paths inside the repo (display/ctmltest fixtures, cli/codegen output) — correctly sandboxed, left alone.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

…medium

go-io v0.15.2 moved the local medium onto os.Root, which by design
refuses any path leaving its root — including one that merely traverses
a symlink. Correct for sandboxed media; wrong for a search that is
unbounded by construction.

loadWorkspaceConfig and findWorkspaceRoot walk UP an arbitrary ancestor
chain to the filesystem root, so no medium root can contain them. Reached
through a symlinked project directory — an ordinary macOS layout, and
what any git worktree or /tmp-based test produces — coreio.Local.Read
failed "path escapes from parent" AND coreio.Local.IsFile returned false.
Those are the two branches loadWorkspaceConfig used to decide "no config
here", so it walked straight past a config that exists and reported the
workspace as unconfigured. Silent: no error, just a default config.

Both probes now use the os-level core.Stat / core.ReadFile, which is
already the house idiom for probing in this repo (display/webkit/pkg/
container/core_helpers.go resolves PATH the same way). go-io is correct
and untouched; this is a right-tool fix, and coreio media stay in place
for the sandboxed I/O everywhere else in the tree.

Receipt — TestLoadWorkspaceConfig_ThroughSymlink against the OLD coreio
path:

    --- FAIL: .../through_symlink        config not found
    --- FAIL: .../child_through_symlink  config not found
    --- PASS: .../direct

and against this fix: all three PASS, plus TestIsRegularFile covering
file / directory / absent, each also via a symlink.

Repo sweep for the same landmine: coreio.Local.Stat has zero call sites;
the only other os.Root-routed probe was Local.IsFile, both instances of
which are the two fixed here. Every remaining coreio.Local use is
Read/Write/Open/Create/Append/EnsureDir on paths inside the repo
(display/ctmltest fixtures, cli/codegen output) — correctly sandboxed,
left alone.

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Snider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dac9cc2-c265-4295-88ef-45734a0fd86f

📥 Commits

Reviewing files that changed from the base of the PR and between 95593e4 and 77ea795.

📒 Files selected for processing (2)
  • go/engine/php/workspace.go
  • go/engine/php/workspace_symlink_test.go

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

@Snider
Snider merged commit 9415ed2 into main Aug 8, 2026
2 checks passed
@Snider
Snider deleted the fix/php-workspace-symlink-probe branch August 8, 2026 09:05
Snider added a commit that referenced this pull request Aug 8, 2026
#51)

* test(engine/php): real coverage for detect, env, extract and workspace

engine/php had 363 running tests and 0.7% coverage. They were empty
bodies and tautologies — `subject := IsLaravelProject; AssertNotNil(t,
subject); AssertEqual(t, "good", "good")` takes a func reference (non-nil
by construction) and asserts a literal equals itself. Same family PR #46
deleted 94 of elsewhere. Replaced, not deleted, for four files.

Coverage 0.7% -> 7.6%. Per function in the files touched:

  detect.go     all 12 at 100.0%
  probe.go      100.0% / 85.7%
  workspace.go  100.0% / 93.8% / 84.6% / 100.0%
  env.go        76.2% / 100.0% / 85.7% / 92.3%, resolveDataDir 35.3%
  extract.go    70.6%

resolveDataDir cannot exceed ~35% in one run: only one GOOS branch
executes. The rest of extract.go is core.MkdirTemp / PathRel / MkdirAll
failures with no injection point.

Tests are hermetic — t.TempDir fixtures, fstest.MapFS for the embedded
tree, t.Setenv to redirect HOME so PrepareRuntimeEnvironment never
touches a real user profile, t.Chdir for the workspace search. Fault
injection is real: truncated and non-JSON composer.json, unreadable
octane config, unwritable data dir and Laravel root, a filesystem whose
reads fail mid-walk, malformed and unsupported-version workspace.yaml.

ONE PRODUCTION CHANGE, from a defect the tests exposed — a second
instance of the go-io os.Root class fixed in #49, this time in
detect.go. Project probing ran through the coreio local medium, which
refuses any path leaving its root, including one that merely traverses a
symlink. Handed a symlinked project path — ~/Sites/app pointing at a
volume, a git worktree, a bind-mounted container path — every file read
as absent, so IsLaravelProject returned false FOR A LARAVEL PROJECT and
GetLaravelAppName returned "". Silently: no error, just a wrong answer.

  probe.go adds probeExists / probeRead over core.Stat / core.ReadFile,
  the same os-level probing already used by workspace.go and
  display/webkit/pkg/container. go-io is correct and untouched; coreio
  media stay for sandboxed I/O.

  Receipt: probed before the fix —
    IsLaravelProject(real)=true   IsLaravelProject(symlink)=false
    GetLaravelAppName(real)="Probe"  GetLaravelAppName(symlink)=""
  TestIsLaravelProject_ThroughSymlink now pins both as equal.

The medium seam is unaffected in practice: SetMedium has no callers
outside its own build-tagged example.

Gates: engine/php green under -race; whole library 59 packages ok, 0
failures; vet + gofmt clean.

Co-Authored-By: Virgil <virgil@lethean.io>

* test(engine/php): scope the Extract leak check to its own temp prefix

TestExtract_Bad and TestExtract_Ugly snapshotted the whole system temp
directory either side of the call, so any unrelated process creating a
temporary file between the two reads would be reported as a leaked
extraction. On a busy CI runner that is a flake that looks like a real
failure.

Only entries carrying Extract's own "go-php-laravel-" prefix are counted
now, which is still a true leak check and immune to unrelated activity.

Receipt: go test -run TestExtract_ -count=2 ./engine/php/ green.

Co-Authored-By: Virgil <virgil@lethean.io>
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