Visitar URL original
fix(security): verify private runtime directories by bnema · Pull Request #42 · bnema/vev · GitHub
Skip to content

fix(security): verify private runtime directories - #42

Merged
bnema merged 9 commits into
improve/001-ci-pipelinefrom
improve/002-socket-dir-hardening
Jul 8, 2026
Merged

bnema merged 9 commits into
improve/001-ci-pipelinefrom
improve/002-socket-dir-hardening

Conversation

@bnema

@bnema bnema commented Jul 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add pkg/safedir.EnsurePrivate for vev-owned private directories
  • wire runtime/state/socket/log/snapshot/spawn/UDP registry directory setup through safedir
  • add hostile-directory and fallback regression coverage

Verification

  • go test ./pkg/safedir ./internal/adapters/ipc ./pkg/kv ./internal/app -race
  • go test ./internal/adapters/ipc -race
  • make lint
  • make test
  • wrap-up review: approved
  • CodeRabbit CLI: findings reviewed; strict 0700 compatibility and parent-chain validation are intentionally out of scope for this plan

Stack: 2/6. Base PR: #41.

Summary by CodeRabbit

  • New Features

    • Improved reliability for reconnects, retries, and session recovery during transient network issues.
    • Added stronger handling for delayed updates so the app keeps working smoothly when acknowledgements lag.
  • Bug Fixes

    • Fixed cases where links could drop too early or stay stuck during recoverable transport errors.
    • Improved consistency for socket, log, snapshot, and storage directory handling.
    • Added protections against unsafe filesystem paths and permissions.

Copilot AI review requested due to automatic review settings July 8, 2026 15:27
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1b659416-5344-4695-9337-3863c1033b0b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors dgram proxy retry handling to retry sends in-place instead of via a retry-events channel, adds transport send-stall dead-link detection and packet-conn rebind hooks, introduces a safedir.EnsurePrivate helper adopted across multiple directory-creation sites, adds daemon output-ack-gated paint deferral, and adds client handling of the Offline link state.

Changes

Dgram Proxy/Transport Resilience

Layer / File(s) Summary
Proxy retry loop refactor
internal/adapters/dgram/proxy.go
Removes retryEvents/waitRetry; copyFrames now retries destination Send in-place with RetryBackoff when retryRecoverable is set, and Run's errCh handling is simplified to treat errors as fatal except daemon EOF.
Proxy retry/link-event tests
internal/adapters/dgram/proxy_test.go
Rewrites fakeTransport and proxy runtime tests to cover table-driven send retries, sustained ErrPendingFull ordering, and offline/dead link event teardown.
Transport send-stall detection and rebind hook
internal/adapters/dgram/transport.go, internal/adapters/dgram/ssh_dialer.go
Adds a first timestamp to pending reliable frames, a new checkSendStall for send-stall death detection, hopPacketConnOnce on probing transition, and RebindPacketConn wiring in RemoteDialer.Dial.
Transport hop/send-stall tests
internal/adapters/dgram/transport_test.go
Updates deadlineCapturePC to block until Close, switches tests to ProbeAfter, and adds tests for send-stall death and hop timing at probing vs degraded silence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Private Directory Enforcement (safedir)

Layer / File(s) Summary
safedir.EnsurePrivate implementation and tests
pkg/safedir/safedir.go, pkg/safedir/safedir_test.go
New package validates directory symlink/type/ownership/0700-permission and returns descriptive errors, with a table-driven test suite.
Adoption in dgram registry and IPC listener
internal/adapters/dgram/proxy_registry.go, internal/adapters/dgram/proxy_registry_test.go, internal/adapters/ipc/listener.go, internal/adapters/ipc/listener_test.go
Replaces os.MkdirAll with safedir.EnsurePrivate; tests use nested private directories and add a hostile-permission rejection test.
Adoption in snapshot store and app spawn/daemon
internal/adapters/snapshot/store.go, internal/adapters/snapshot/store_test.go, internal/app/spawn.go, internal/app/spawn_test.go, internal/app/daemon_integration_test.go
Replaces directory creation with safedir.EnsurePrivate in snapshot write and spawn lock acquisition; tests adjusted to nested private directories.
Adoption in logging, persist, and kv
internal/logging/logging.go, internal/logging/logging_test.go, internal/persist/persist_test.go, pkg/kv/kv.go, pkg/kv/kv_test.go
Replaces directory creation with safedir.EnsurePrivate in logging.Setup and kv.Open; test files add private-directory helpers.

Estimated code review effort: 2 (Simple) | ~15 minutes

Daemon Output-Ack-Gated Paint Deferral

Layer / File(s) Summary
Paint deferral state and gating logic
internal/usecase/daemon/client.go, internal/usecase/daemon/daemon.go, internal/usecase/daemon/render.go, internal/usecase/daemon/resume.go, internal/usecase/daemon/paint_gate_test.go
Adds paintDeferred state and maxUnackedOutputStates cap; paint defers when ack lag is at cap, MsgAck handling flushes deferred paints, parkAttachment clears deferral, with new tests.

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

Client Offline Link Reconnect Handling

Layer / File(s) Summary
Offline link event handling and test
internal/usecase/client/client.go, internal/usecase/client/reconnect.go, internal/usecase/client/reconnect_toast_test.go
Adds errLinkOffline sentinel and an attachOnce branch returning it as retryable on ports.LinkStateOffline, with a test asserting the reconnect stage and reconnectable error.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Daemon
  participant runConnLoop
  Daemon->>Client: MsgOutput (ack lag reaches cap)
  Daemon->>Daemon: paint() marks paintDeferred, returns early
  Client->>runConnLoop: MsgAck
  runConnLoop->>Daemon: takeDeferredPaint() returns true
  Daemon->>Client: paint(sess, ac, false) flushes diff
Loading
sequenceDiagram
  participant DaemonOutput
  participant copyFrames
  participant ClientTransport
  DaemonOutput->>copyFrames: received frame
  copyFrames->>ClientTransport: Send(frame)
  ClientTransport-->>copyFrames: recoverable error
  loop retryRecoverable
    copyFrames->>copyFrames: wait RetryBackoff
    copyFrames->>ClientTransport: retry Send(same frame)
  end
  ClientTransport-->>copyFrames: success
Loading

Possibly related PRs

  • bnema/vev#40: Both PRs modify the same ProxyRuntime.Run/copyFrames retry-events lifecycle in internal/adapters/dgram/proxy.go.
  • bnema/vev#36: Both PRs modify the UDP proxy runtime's link-state-driven recovery/retry behavior in internal/adapters/dgram/proxy.go.
  • bnema/vev#33: Both PRs touch RemoteDialer.Dial's UDP transport bootstrap in internal/adapters/dgram/ssh_dialer.go.

Poem

Thump-thump, the burrow's doors now lock at 0700 tight,
Frames retry in gentle loops till packets land just right,
Painters pause when acks fall behind, then flush it all at once,
Offline link? No worries, hop back on with resume response!
🐇✨ A tidy warren, safe and swift tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main security-focused change to verify private runtime directories.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve/002-socket-dir-hardening

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens vev’s runtime and on-disk state handling by introducing a shared pkg/safedir.EnsurePrivate helper and routing multiple runtime directory creation points through it, so vev refuses to use pre-existing directories that are not private to the current user.

Changes:

  • Added pkg/safedir.EnsurePrivate to create/validate private (0700) vev-owned directories and reject symlinks / wrong ownership / wrong perms.
  • Wired directory setup for KV storage, logging, daemon spawn lock, IPC socket directory, snapshot storage, and UDP proxy registry through safedir.
  • Added regression coverage for hostile directory scenarios and adjusted tests to exercise the “create nested vev dir” behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/safedir/safedir.go Adds EnsurePrivate helper to create/validate private directories.
pkg/safedir/safedir_test.go Adds unit tests covering permission, symlink, and non-directory rejection.
pkg/kv/kv.go Uses EnsurePrivate for WAL directory creation before opening the store file.
internal/logging/logging.go Secures the logging directory with EnsurePrivate before opening log files.
internal/app/spawn.go Secures the daemon socket/spawn directory with EnsurePrivate before spawn locking.
internal/app/spawn_test.go Updates tests to use nested “vev” dir paths consistent with hardened setup.
internal/app/daemon_integration_test.go Starts daemons under a nested “vev” dir path.
internal/adapters/snapshot/store.go Uses EnsurePrivate for snapshot dir creation (but retains a redundant chmod).
internal/adapters/ipc/listener.go Secures socket dir with EnsurePrivate and wraps errors with clearer context.
internal/adapters/ipc/listener_test.go Adds hostile socket-dir rejection test and updates temp dir usage.
internal/adapters/dgram/proxy_registry.go Secures UDP proxy registry dir with EnsurePrivate before locking.
Comments suppressed due to low confidence (1)

internal/adapters/snapshot/store.go:44

  • After calling safedir.EnsurePrivate(s.dir), the directory is already verified to have exact 0700 perms. The subsequent os.Chmod(s.dir, 0o700) is therefore redundant and can also introduce a new failure mode on filesystems where chmod is unsupported/restricted (even when perms are already acceptable), causing snapshot writes to fail unnecessarily.
	if err := safedir.EnsurePrivate(s.dir); err != nil {
		return fmt.Errorf("create snapshot dir: %w", err)
	}
	if err := os.Chmod(s.dir, 0o700); err != nil {
		return fmt.Errorf("chmod snapshot dir: %w", err)
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

bnema added 8 commits July 8, 2026 18:32
chore(ci): add build, lint, and test workflow
Add a per-frame first-send timestamp and a checkSendStall pass on the
resend/heartbeat ticks: when the oldest unacked reliable frame stays
unacked for deadAfter despite a retransmit, declare the link dead. This
catches one-way-dead links whose receive side stays fresh from peer
heartbeats, which the receive-silence check misses.

Also move the one-shot socket hop from the Offline (30s) transition to
Probing (20s) so the client rebind runs before the link is abandoned.
Retry the same daemon->client frame in place while the link error stays
recoverable instead of restarting the copier and dropping the in-flight
frame, which desynchronized the client screen on a reliable stream.

Retries no longer arm the idle TTL: it is driven solely by link-state
events (armed on Degraded/Probing/Offline, disarmed on Connected, Dead is
immediate). A persistently pending-full but Connected link therefore
survives rather than being killed after 30s. Send-stall death now comes
from the transport layer.
Build the client transport with a RebindPacketConn that binds a fresh
local UDP socket via the existing listen helper, so the probing-state
socket hop can heal NAT rebinds and stale conntrack paths in production.
On a LinkStateOffline event, draw the toast and return a retryable
errLinkOffline so Run re-dials with the resume token at ~30s of silence
instead of waiting for the transport to reach Dead at 60s. Degraded and
Probing stay toast-only.
Add maxUnackedOutputStates: when the client is that many output states
behind, paint defers the diff instead of composing another, marking a
per-client paintDeferred flag under sendMu. The MsgOutputAck handler
flushes one cumulative paint once acks catch up. Reset paints (explicit
invalidations) bypass the gate and clear the flag; park clears it too.

This paces the daemon's paint rate to the client's ack rate so heavy
output degrades to lower fps on a slow link instead of overflowing the
UDP proxy window and tripping the 1s send watchdog.
Review findings on PR #47: exercise the flush end-to-end via runConnLoop instead of calling advanceOutputAck/takeDeferredPaint directly, and make requireNoOutputFrame drain non-output frames for the full window instead of returning on the first frame.
fix(remote): stop UDP attach collapsing under congestion
@bnema
bnema force-pushed the improve/002-socket-dir-hardening branch from de2121b to 070db83 Compare July 8, 2026 17:40
@bnema

bnema commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bnema

bnema commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/adapters/snapshot/store.go (1)

39-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant os.Chmod after safedir.EnsurePrivate.

safedir.EnsurePrivate already fails the call unless s.dir is exactly 0700 (owned by the current uid). By the time execution reaches line 42, the directory is guaranteed to already be 0700, making this os.Chmod a no-op that only adds an unreachable error path. This was needed previously because bare os.MkdirAll didn't re-verify existing-directory permissions, but that rationale no longer applies.

♻️ Proposed cleanup
 func (s *Store) Write(name string, data []byte) error {
 	if err := safedir.EnsurePrivate(s.dir); err != nil {
 		return fmt.Errorf("create snapshot dir: %w", err)
 	}
-	if err := os.Chmod(s.dir, 0o700); err != nil {
-		return fmt.Errorf("chmod snapshot dir: %w", err)
-	}
 
 	tmp, err := os.CreateTemp(s.dir, ".tmp-*")
🤖 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 `@internal/adapters/snapshot/store.go` around lines 39 - 44, Remove the
redundant permission fix-up in snapshot store initialization: in the snapshot
directory setup path inside the Store logic, `safedir.EnsurePrivate` already
guarantees `s.dir` is private, so the subsequent `os.Chmod` call in that same
block is unnecessary. Update the initialization flow to keep only the
`safedir.EnsurePrivate` check and its error handling, leaving `Store` to fail
early if the directory is not already `0700`.
🤖 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 `@internal/adapters/dgram/proxy.go`:
- Around line 151-172: The retry loop in the copier can keep running after Run
exits because the shared context is not canceled on every exit path. Update Run
to derive a child context for the copier goroutines and defer its cancel so both
the daemon and client copy paths stop when Run returns, including idle-timer and
EOF exits. Make sure the retry/backoff logic in proxyCopySender (the Send loop
with recoverableClientLink and c.clk.NewTimer) observes that child context
instead of continuing to treat dgram closed errors as recoverable after
shutdown.

In `@pkg/safedir/safedir.go`:
- Around line 14-39: EnsurePrivate currently validates a path snapshot in
EnsurePrivate but still leaves callers using the string path afterward, which
allows a post-check swap. Update the safedir flow to return or retain a pinned
directory handle from EnsurePrivate using dirfd-based opening and fstat on that
handle, then have subsequent operations use that handle or relative lookups
instead of re-resolving the path. Keep the existing symlink, ownership, and 0700
checks in EnsurePrivate, but make them part of a handle-based validation so the
directory cannot be replaced after validation.

---

Outside diff comments:
In `@internal/adapters/snapshot/store.go`:
- Around line 39-44: Remove the redundant permission fix-up in snapshot store
initialization: in the snapshot directory setup path inside the Store logic,
`safedir.EnsurePrivate` already guarantees `s.dir` is private, so the subsequent
`os.Chmod` call in that same block is unnecessary. Update the initialization
flow to keep only the `safedir.EnsurePrivate` check and its error handling,
leaving `Store` to fail early if the directory is not already `0700`.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e6218331-d314-4cdb-8001-7e966c0ae71f

📥 Commits

Reviewing files that changed from the base of the PR and between fa92ec3 and 070db83.

📒 Files selected for processing (29)
  • internal/adapters/dgram/proxy.go
  • internal/adapters/dgram/proxy_registry.go
  • internal/adapters/dgram/proxy_registry_test.go
  • internal/adapters/dgram/proxy_test.go
  • internal/adapters/dgram/ssh_dialer.go
  • internal/adapters/dgram/transport.go
  • internal/adapters/dgram/transport_test.go
  • internal/adapters/ipc/listener.go
  • internal/adapters/ipc/listener_test.go
  • internal/adapters/snapshot/store.go
  • internal/adapters/snapshot/store_test.go
  • internal/app/daemon_integration_test.go
  • internal/app/spawn.go
  • internal/app/spawn_test.go
  • internal/logging/logging.go
  • internal/logging/logging_test.go
  • internal/persist/persist_test.go
  • internal/usecase/client/client.go
  • internal/usecase/client/reconnect.go
  • internal/usecase/client/reconnect_toast_test.go
  • internal/usecase/daemon/client.go
  • internal/usecase/daemon/daemon.go
  • internal/usecase/daemon/paint_gate_test.go
  • internal/usecase/daemon/render.go
  • internal/usecase/daemon/resume.go
  • pkg/kv/kv.go
  • pkg/kv/kv_test.go
  • pkg/safedir/safedir.go
  • pkg/safedir/safedir_test.go

Comment on lines +151 to 172
// Retry the same frame in place while the destination link error stays
// recoverable (congestion, transient path loss). The frame is never
// dropped — on a reliable stream a gap desynchronizes the client screen.
for {
if err := dir.dst.Send(f); err != nil {
if dir.retryPendingFull && errors.Is(err, ErrPendingFull) && recoverableClientLink(dir.dst, err) {
if !retrying {
retrying = true
if !sendProxyRetryEvent(c.ctx, c.retryEvents, proxyRetryStart) {
return
}
}
t := c.clk.NewTimer(c.retryBackoff)
select {
case <-t.C():
t.Stop()
continue
case <-c.ctx.Done():
t.Stop()
return
}
}
c.errCh <- proxyCopyErr{kind: dir.sendKind, err: err}
return
err := dir.dst.Send(f)
if err == nil {
break
}
if retrying && !sendProxyRetryEvent(c.ctx, c.retryEvents, proxyRetryDone) {
return
if dir.retryRecoverable && recoverableClientLink(dir.dst, err) {
t := c.clk.NewTimer(c.retryBackoff)
select {
case <-t.C():
t.Stop()
continue
case <-c.ctx.Done():
t.Stop()
return
}
}
break
c.errCh <- proxyCopyErr{kind: dir.sendKind, err: err}
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect Close() and LinkState transition in the real transport to confirm
# whether Close forces a non-recoverable (Dead) link state.
fd -e go . internal/adapters/dgram --exec rg -nP -C4 'func .*Close\(|LinkState\s*\(|LinkStateDead' {}

Repository: bnema/vev

Length of output: 6870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== proxy.go outline ==\n'
ast-grep outline internal/adapters/dgram/proxy.go --view expanded || true

printf '\n== proxy.go relevant lines ==\n'
sed -n '1,260p' internal/adapters/dgram/proxy.go

printf '\n== transport.go outline ==\n'
ast-grep outline internal/adapters/dgram/transport.go --view expanded || true

printf '\n== transport.go relevant lines ==\n'
sed -n '1,420p' internal/adapters/dgram/transport.go

Repository: bnema/vev

Length of output: 22535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== closeWithError and surrounding transport state ==\n'
sed -n '780,860p' internal/adapters/dgram/transport.go

printf '\n== tests mentioning Close / LinkStateDead / offline states ==\n'
rg -n -C3 'Close\(\)|LinkStateDead|LinkStateOffline|LinkStateDegraded|LinkStateProbing|ErrLinkDead|ErrPendingFull' internal/adapters/dgram/*_test.go

Repository: bnema/vev

Length of output: 28839


Cancel the copier context when Run exits. Client.Close() does not force LinkState() to Dead, so a daemon→client retry that was already in the backoff loop can keep treating errors.New("dgram: closed") as recoverable after the idle-timer or EOF exit paths. Derive a child context in Run and defer cancel() so both copier goroutines stop on every exit path.

🤖 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 `@internal/adapters/dgram/proxy.go` around lines 151 - 172, The retry loop in
the copier can keep running after Run exits because the shared context is not
canceled on every exit path. Update Run to derive a child context for the copier
goroutines and defer its cancel so both the daemon and client copy paths stop
when Run returns, including idle-timer and EOF exits. Make sure the
retry/backoff logic in proxyCopySender (the Send loop with recoverableClientLink
and c.clk.NewTimer) observes that child context instead of continuing to treat
dgram closed errors as recoverable after shutdown.

Comment thread pkg/safedir/safedir.go
Comment on lines +14 to +39
func EnsurePrivate(dir string) error {
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
fi, err := os.Lstat(dir)
if err != nil {
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("safedir: %s is a symlink", dir)
}
if !fi.IsDir() {
return fmt.Errorf("safedir: %s is not a directory", dir)
}
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("safedir: cannot stat %s", dir)
}
if int(st.Uid) != os.Getuid() {
return fmt.Errorf("safedir: %s is owned by uid %d, not current uid %d — refusing to use it", dir, st.Uid, os.Getuid())
}
if perm := fi.Mode().Perm(); perm != 0o700 {
return fmt.Errorf("safedir: %s has permissions %04o, want 0700 — refusing to use it", dir, perm)
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files 'pkg/safedir/*' 'pkg/**/*' | sed -n '1,120p'

printf '\n== safedir.go (lines with numbers) ==\n'
cat -n pkg/safedir/safedir.go

printf '\n== Search for EnsurePrivate call sites ==\n'
rg -n 'EnsurePrivate\(' .

printf '\n== Search for pkg/safedir imports ==\n'
rg -n 'pkg/safedir|safedir\.EnsurePrivate' .

printf '\n== Build tags / platform guards near safedir ==\n'
rg -n '^//go:build|^// \+build|syscall.Stat_t|ModeSymlink|MkdirAll' pkg/safedir pkg -g '!**/*_test.go'

Repository: bnema/vev

Length of output: 4890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== safedir tests ==\n'
cat -n pkg/safedir/safedir_test.go

printf '\n== caller snippets ==\n'
for f in internal/adapters/ipc/listener.go internal/adapters/dgram/proxy_registry.go internal/adapters/snapshot/store.go internal/logging/logging.go internal/app/spawn.go pkg/kv/kv.go; do
  echo
  echo "### $f"
  sed -n '1,140p' "$f" | cat -n
done

printf '\n== openat / O_DIRECTORY / fstat search ==\n'
rg -n 'O_DIRECTORY|openat|fstat|Fstat|open\(' pkg internal

Repository: bnema/vev

Length of output: 31951


Close the post-validation path race. EnsurePrivate checks a path snapshot, but callers keep using the string path for later opens/renames, so a writable parent can swap the entry after validation and bypass the 0700/ownership guarantee. Use a pinned directory handle (O_DIRECTORY/fstat, or keep operations relative to a dirfd) instead of re-resolving the path.

🤖 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 `@pkg/safedir/safedir.go` around lines 14 - 39, EnsurePrivate currently
validates a path snapshot in EnsurePrivate but still leaves callers using the
string path afterward, which allows a post-check swap. Update the safedir flow
to return or retain a pinned directory handle from EnsurePrivate using
dirfd-based opening and fstat on that handle, then have subsequent operations
use that handle or relative lookups instead of re-resolving the path. Keep the
existing symlink, ownership, and 0700 checks in EnsurePrivate, but make them
part of a handle-based validation so the directory cannot be replaced after
validation.

@bnema
bnema force-pushed the improve/002-socket-dir-hardening branch 2 times, most recently from c926a40 to f8a1d7c Compare July 8, 2026 18:41
@bnema
bnema merged commit f2e4d67 into improve/001-ci-pipeline Jul 8, 2026
2 checks passed
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.

2 participants