Skip to content

Implement Darwin (MacOS)#169

Open
lancepioch wants to merge 8 commits into
mainfrom
lance/mac
Open

Implement Darwin (MacOS)#169
lancepioch wants to merge 8 commits into
mainfrom
lance/mac

Conversation

@lancepioch

@lancepioch lancepioch commented Feb 7, 2026

Copy link
Copy Markdown
Member

Makes Wings compile and run natively on macOS, so the daemon can be developed and tested on a Mac against Docker Desktop (containers still run in Docker Desktop's Linux VM; Wings itself runs on the host).

Approach

Linux-only syscalls are split into _linux.go / _darwin.go file pairs using Go's filename-based build constraints, with a handful of runtime.GOOS checks where a file split would be overkill:

  • internal/ufsopenat2 is Linux 5.6+ only, so Darwin gets an ENOSYS stub and the openat fallback path validation. fdPath() uses the F_GETPATH fcntl instead of /proc/self/fd/. Chtimesat() emulates UTIME_OMIT (not available on Darwin) by reading and preserving current timestamps. Directory listing uses Getdirentries instead of Getdents, and Darwin dirents resolve Info()/Open() by path because Getdirentries is a userspace emulation (fdopendir/readdir_r) that manipulates the directory fd's seek offset, leaving it unusable for subsequent *at calls (EBADF).
  • config — the UseOpenat2() runtime probe moved to a Linux-only file; Darwin always returns false. EnsurePelicanUser() uses the current user on macOS (no useradd), matching rootless-mode semantics.
  • server/filesystem — Darwin CTime(); a non-Linux stub for the new quotas package (returns explicit "unsupported" errors).
  • system / environment — report "macOS" without reading os-release; never set BlkioWeight on non-Linux hosts (Docker Desktop rejects it, and the cgroup-v2 probe path in blkioWeightSupported() would incorrectly return true on macOS).
  • UnixFS base paths resolve symlinks at construction so fd-path comparisons work on macOS where /var/private/var, and safePath opens the base with AT_FDCWD rather than passing AT_EMPTY_PATH as a dirfd.

Latent bug fixes (affect shared code)

Zero created timestamps on Linuxstat_linux.go's CTime() checked for *unix.Stat_t twice; the second branch was clearly meant to handle *syscall.Stat_t, which is what Sys() returns for FileInfos produced by (*os.File).Stat — the path Filesystem.Stat uses. The file API's created field has therefore been the zero time on Linux. Caught by the new TestStatCTime.

Wrong dirent paths at walk rootsinternal/ufs/walk_unix.go readDir() set rel = name (the parent directory's name) instead of rel = childName for root-level entries. This was invisible on Linux because Linux dirents stat/open via dirfd+name and never read path, but Darwin's path-based Info()/Open() exposed it. Fixed here with a regression test (TestReadDirRootEntryPaths).

Tests

  • internal/ufs: platform tests for fd path resolution, openat path validation (symlink escape), Chtimesat zero-time preservation, and O_LARGEFILE; portable ReadDirMap tests (Info/Open, 100-goroutine concurrency, symlinked base) that now run on Linux CI too.
  • config: UseOpenat2 mode-override and per-platform behavior tests.
  • server/filesystem: CTime() and Stat JSON marshaling tests.
  • New macOS CI job (macos-15) running go test -race ./... so Darwin support doesn't silently rot; the Linux matrix is unchanged.

Also reduces log levels for expected websocket disconnect/cleanup paths (Error/Warning → Info, keeping the error as a log field), adds build-darwin/fixed cross-build Makefile targets, and documents macOS setup in macos.md.

Summary by CodeRabbit

  • New Features

    • Added native macOS support, including platform-specific filesystem handling and configuration.
    • Added macOS build target and improved build trimming, producing both Intel and Apple Silicon binaries.
    • Disabled unsupported behaviors on non-Linux platforms (e.g., disk quotas, blkio weight).
  • Bug Fixes

    • Improved OpenAt2 compatibility and fallback behavior across platforms.
    • Fixed file creation-time reporting on Linux; added Darwin creation-time support.
  • Tests

    • Expanded Unix filesystem test coverage and added automated macOS build/test with race testing.
  • Documentation

    • Added macOS setup/build guide.

@lancepioch lancepioch self-assigned this Feb 7, 2026
@lancepioch
lancepioch requested a review from a team as a code owner February 7, 2026 19:05
@lancepioch lancepioch added the enhancement New feature or request label Feb 7, 2026
@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds macOS builds and CI coverage, Darwin-specific configuration and filesystem implementations, Linux capability handling, cross-platform tests, quota fallbacks, macOS documentation, and lower-severity websocket logging.

Changes

macOS platform support

Layer / File(s) Summary
Platform runtime and capability contracts
config/*, environment/settings.go, system/system.go, server/filesystem/stat_*, server/filesystem/quotas/*
Adds Darwin OS detection, local-user setup, Openat2 platform selection, non-Linux capability fallbacks, blkio gating, and platform-specific creation-time handling.
Platform-specific Unix filesystem syscalls
internal/ufs/file_*, internal/ufs/fs_*
Adds Linux and Darwin implementations for descriptor paths, Openat2, timestamp updates, and large-file constants, with atomic Openat2 fallback state.
Platform-specific directory walking
internal/ufs/walk_*
Routes directory enumeration and entry operations through Linux and Darwin implementations and adjusts root-entry path handling.
Unix filesystem validation
internal/ufs/*_test.go, server/filesystem/filesystem_test.go, server/filesystem/stat_test.go
Adds tests for filesystem metadata, path containment, timestamps, constants, directory access, symlinked roots, concurrency, and serialized stat data.
macOS build, CI, and operating guide
Makefile, .github/workflows/push.yaml, macos.md
Adds Darwin build targets, macOS race-test CI, and instructions for configuring, running, and building Wings on macOS.

Websocket logging levels

Layer / File(s) Summary
Websocket cleanup logging
router/router_server_ws.go, router/websocket/listeners.go
Changes websocket cleanup and event-send messages from error or warning levels to informational logging.

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

Possibly related PRs

Suggested reviewers: parkervcp, quintenqvd0

Poem

A rabbit hops through Darwin’s gate,
Builds two wings and tests their state.
Symlinks twirl, timestamps gleam,
Logs whisper softly through the stream.
“Fresh platform paths!” the bunny sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding Darwin/macOS support across the codebase.
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 lance/mac

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.

@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: 4

🤖 Fix all issues with AI agents
In `@internal/ufs/fs_darwin.go`:
- Around line 40-58: The code in UnixFS.Chtimesat uses linux-style Stat_t fields
Atim/Mtim which don't exist on macOS; replace references to st.Atim and st.Mtim
with Darwin names st.Atimespec and st.Mtimespec and convert their timespecs to
time.Time (e.g., using st.Atimespec.Unix()/UnixNano() or an equivalent
conversion) before assigning atime/mtime so the function compiles and uses the
correct Darwin fields.

In `@internal/ufs/fs_unix.go`:
- Around line 653-660: The field useOpenat2 on UnixFS is written without
synchronization causing a data race; change the UnixFS struct to use an
atomic.Bool for useOpenat2, update all reads to use fs.useOpenat2.Load() and the
write in the open path to fs.useOpenat2.Store(false) (so the branch around
fs._openat2 / fs._openat remains correct), and add the "sync/atomic" import (or
the atomic package alias used) so the race detector is silenced.

In `@internal/ufs/walk_linux.go`:
- Around line 18-33: The defensive fallback in nameFromDirent uses
name[:len(de.Name)] which can panic when ml (derived from de.Reclen) is smaller
than len(de.Name); change the fallback to return the already-sliced name (the
variable name) directly so you never slice past its length—update the fallback
in function nameFromDirent to return name instead of name[:len(de.Name)].

In `@Makefile`:
- Line 24: The cross-build target currently depends on "clean build compress"
but omits "build-darwin" and references a non-existent "compress" target; update
the cross-build dependency list to include build-darwin (so cross-build produces
darwin artifacts) and either add a well-defined compress target (e.g.,
packaging/compression step) or remove "compress" from the dependency list; edit
the Makefile entry for the cross-build target and verify the related targets
"build", "build-darwin", and "clean" are present and correctly implemented.
🧹 Nitpick comments (8)
router/router_server_ws.go (1)

78-78: Log level downgrade for websocket close failure looks intentional and consistent.

This aligns with the similar changes in router/websocket/listeners.go. However, a failed close could occasionally indicate a real issue (e.g., resource leak). Consider using Warn as a middle ground so these don't get completely buried in production, while still not alarming on expected closes.

router/websocket/listeners.go (1)

100-100: Consider keeping Warn for websocket send failures.

Unlike close errors (which are expected during shutdown), a failure to send an event over the websocket may indicate a genuine issue — broken pipe, serialization error, or resource exhaustion. Logging this at Info level risks burying actionable signals in production. Warn would be a more appropriate middle ground.

Proposed change
-		h.Logger().WithField("event", evt).WithField("error", err2).Info("failed to send event over server websocket")
+		h.Logger().WithField("event", evt).WithField("error", err2).Warn("failed to send event over server websocket")
environment/settings.go (1)

120-123: Platform guard for BlkioWeight is reasonable but may be overly conservative.

BlkioWeight is a Linux cgroup feature, so guarding it makes sense for native macOS execution. However, note that Docker Desktop on macOS runs a Linux VM, and the Docker API may accept BlkioWeight there. If users running Wings on macOS with Docker Desktop need IO weight support, this guard would silently skip it.

Consider adding a comment explaining the rationale (e.g., Darwin doesn't support cgroup blkio) so future maintainers understand the intent.

Suggested comment
+	// BlkioWeight is only supported on Linux (cgroup blkio controller).
+	// On macOS, Docker Desktop uses a Linux VM but may not expose this setting.
 	if runtime.GOOS == "linux" {
 		resources.BlkioWeight = l.IoWeight
 	}
Makefile (1)

29-29: Add test to .PHONY declaration.

The static analysis tool correctly flags that test is missing from the .PHONY list. Since test is a valid filename, not declaring it as phony could cause issues if a file named test exists.

Proposed fix
-.PHONY: all build build-darwin compress clean
+.PHONY: all build build-darwin cross-build compress clean test debug rmdebug
macos.md (1)

40-47: Docker socket path may vary by Docker Desktop version.

The path ~/.docker/run/docker.sock is the default for recent Docker Desktop versions, but some installations use /var/run/docker.sock natively or $HOME/.docker/desktop/docker.sock. Consider adding a brief note to verify the actual socket path (e.g., docker context inspect or ls ~/.docker/run/docker.sock) before creating the symlink.

config/config.go (1)

505-515: Darwin user resolution looks correct.

The approach mirrors the existing rootless-mode logic (lines 525–534). Since macOS lacks useradd/adduser, using the current user is the right call.

Note: the user.Current()MustInt(u.Uid)MustInt(u.Gid) pattern is now repeated in three places (darwin, distroless, rootless). Consider extracting a small helper like setCurrentUser() to reduce duplication, but this is not blocking.

♻️ Optional: extract helper to reduce duplication
+// setCurrentUser sets the system user configuration from the current OS user.
+func setCurrentUser() error {
+	u, err := user.Current()
+	if err != nil {
+		return err
+	}
+	_config.System.Username = u.Username
+	_config.System.User.Uid = system.MustInt(u.Uid)
+	_config.System.User.Gid = system.MustInt(u.Gid)
+	return nil
+}

Then in EnsurePelicanUser, the darwin, distroless, and rootless blocks could call setCurrentUser() (with the distroless block keeping its env-var override logic separately).

config/config_openat_linux.go (1)

15-40: Benign race on concurrent first calls to UseOpenat2().

Two goroutines can both observe openat2Set as false and enter the detection path concurrently. Because the syscall result is deterministic for a given kernel, the final cached value will be correct in practice, but the detection work (including the Openat2 syscall + Close) may execute more than once.

A sync.Once would eliminate the redundant work and make the intent clearer. Not a correctness bug given the deterministic nature of the check, so feel free to defer.

internal/ufs/fs_linux.go (1)

58-72: Nit: Sec field in the UTIME_OMIT timespec.

On Line 62, both Sec and Nsec are set to unix.UTIME_OMIT. Per utimensat(2), only the tv_nsec field is inspected for the UTIME_OMIT sentinel — tv_sec is ignored. This works correctly but is slightly misleading. Consider setting Sec: 0 for clarity.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4586d7b and 3a7a902.

📒 Files selected for processing (25)
  • Makefile
  • config/config.go
  • config/config_openat_darwin.go
  • config/config_openat_linux.go
  • config/config_openat_linux_test.go
  • config/config_openat_test.go
  • environment/settings.go
  • internal/ufs/file.go
  • internal/ufs/file_darwin.go
  • internal/ufs/file_linux.go
  • internal/ufs/fs_darwin.go
  • internal/ufs/fs_linux.go
  • internal/ufs/fs_platform_test.go
  • internal/ufs/fs_unix.go
  • internal/ufs/fs_unix_test.go
  • internal/ufs/walk_darwin.go
  • internal/ufs/walk_linux.go
  • internal/ufs/walk_unix.go
  • macos.md
  • router/router_server_ws.go
  • router/websocket/listeners.go
  • server/filesystem/filesystem_test.go
  • server/filesystem/stat_darwin.go
  • server/filesystem/stat_test.go
  • system/system.go
🧰 Additional context used
🧬 Code graph analysis (11)
server/filesystem/stat_darwin.go (1)
server/filesystem/stat.go (1)
  • Stat (14-17)
internal/ufs/file_linux.go (1)
internal/ufs/file_darwin.go (1)
  • O_LARGEFILE (4-4)
internal/ufs/fs_platform_test.go (1)
server/filesystem/stat.go (1)
  • Stat (14-17)
config/config_openat_darwin.go (1)
config/config_openat_linux.go (1)
  • UseOpenat2 (15-41)
config/config.go (2)
system/system.go (1)
  • System (60-67)
system/utils.go (1)
  • MustInt (35-41)
config/config_openat_linux.go (3)
config/config_openat_darwin.go (1)
  • UseOpenat2 (5-7)
config/config.go (1)
  • Get (439-447)
system/system.go (1)
  • System (60-67)
config/config_openat_test.go (3)
config/config.go (3)
  • Set (408-420)
  • Configuration (320-379)
  • SystemConfiguration (138-250)
config/config_openat_darwin.go (1)
  • UseOpenat2 (5-7)
config/config_openat_linux.go (1)
  • UseOpenat2 (15-41)
server/filesystem/stat_test.go (3)
server/filesystem/filesystem_test.go (1)
  • NewFs (20-55)
server/filesystem/stat.go (1)
  • Stat (14-17)
server/filesystem/errors.go (1)
  • Error (25-37)
internal/ufs/fs_unix.go (1)
internal/ufs/file.go (2)
  • O_DIRECTORY (169-169)
  • O_RDONLY (152-152)
internal/ufs/fs_linux.go (3)
internal/ufs/fs_unix.go (1)
  • UnixFS (23-30)
internal/ufs/file.go (1)
  • O_CLOEXEC (172-172)
internal/ufs/file_linux.go (1)
  • O_LARGEFILE (5-5)
internal/ufs/fs_darwin.go (1)
internal/ufs/fs_unix.go (1)
  • UnixFS (23-30)
🪛 checkmake (0.2.2)
Makefile

[warning] 29-29: Missing required phony target "test"

(minphony)

🪛 LanguageTool
macos.md

[style] ~160-~160: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... not exist on Darwin. - unix.Getdents does not exist on Darwin; `unix.Getdirentrie...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[grammar] ~174-~174: Ensure spelling is correct
Context: ...at_linux.go| OriginalCTime()usingunix.Stat_t.Ctim(was//go:build linux) | | stat_darw...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~175-~175: Ensure spelling is correct
Context: ...at_darwin.go|CTime()handling bothunix.Stat_t.Ctimandsyscall.Stat_t.Ctimespec| gola...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.24.7, linux, amd64)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.25.1, linux, amd64)
🔇 Additional comments (31)
server/filesystem/filesystem_test.go (1)

34-37: Good fix for macOS symlink resolution in test setup.

This correctly handles the /var/private/var symlink on macOS, preventing spurious ErrBadPathResolution failures in tests.

router/websocket/listeners.go (1)

30-33: Log level changes for websocket lifecycle events are reasonable.

These log during normal connection teardown, so Info is appropriate.

internal/ufs/file_linux.go (1)

1-5: Clean platform-specific constant definition.

The Linux/Darwin split for O_LARGEFILE is correct: Darwin universally supports large files (hence 0), while Linux needs the explicit flag.

internal/ufs/file.go (1)

172-173: No action needed. AT_EMPTY_PATH is not referenced anywhere in the codebase and is not defined in any platform-specific files, confirming it was safely removed and is not required for the current implementation.

system/system.go (2)

115-129: LGTM — Darwin-specific OS detection looks correct.

Clean branching: Darwin skips osrelease.Read() (which would fail on macOS) and returns a sensible hardcoded value, while the Linux path retains the existing fallback chain.


209-218: LGTM — Early return avoids reading a non-existent file on macOS.

server/filesystem/stat_darwin.go (1)

10-22: The comment and TODO are accurate — Ctim/Ctimespec is status-change time, not creation time.

On macOS, the actual file creation (birth) time is available via Birthtimespec on syscall.Stat_t. Since the TODO already flags this for removal and the Linux counterpart has the same semantics, this is consistent. Just noting for awareness that if the intent ever shifts to truly returning creation time on Darwin, Birthtimespec would be the correct field.

server/filesystem/stat_test.go (2)

9-29: Test looks good but may be fragile if CTime() returns zero time.

On line 23, the test asserts CTime() is non-zero, but the Darwin implementation falls back to time.Time{} if neither unix.Stat_t nor syscall.Stat_t is the underlying type. If a future Go or platform change alters the Sys() return type, this test would fail with a non-obvious error. Consider a more descriptive message or logging st.Sys() type on failure for easier debugging.

That said, this is minor — the current implementations should always match one of the two branches.


31-71: LGTM — Good coverage of JSON marshaling fields.

The test validates the key fields (created, modified, name) and timestamp format. Clean and readable.

macos.md (1)

1-203: Well-written documentation covering both user setup and developer context.

The separation between user-facing setup instructions and developer-focused code change documentation is clear and useful. The explanations for why specific settings are needed (Docker Desktop file sharing paths, machine_id.enable: false, etc.) are particularly helpful.

internal/ufs/file_darwin.go (1)

1-4: LGTM — Correct no-op constant for Darwin.

internal/ufs/fs_unix_test.go (1)

37-40: LGTM — Necessary symlink resolution for macOS test compatibility.

Consistent with the same pattern applied in server/filesystem/filesystem_test.go's NewFs().

internal/ufs/walk_darwin.go (1)

9-18: LGTM — Clean Darwin-specific dirent handling.

Using Namlen directly is the correct approach for Darwin and avoids the NUL-scanning complexity required on Linux. The unsafe.Slice usage is idiomatic for this pattern.

config/config_openat_test.go (1)

8-28: LGTM — reasonable cross-platform smoke test.

Clean setup and the OS-specific assertions are correct: Darwin's UseOpenat2() always returns false, and Linux's result is kernel-dependent.

One minor note: on Linux, if TestUseOpenat2ConfigOverride (in config_openat_linux_test.go) runs first, the atomic cache (openat2Set) will already be populated, so this test won't actually exercise the "auto" detection path — it'll return the cached value. Since the stated goal is just "doesn't panic," that's fine, but worth being aware of if you ever want stronger coverage of the auto path in isolation.

config/config_openat_darwin.go (1)

1-7: LGTM!

Clean platform stub. The comment accurately explains why openat2 is unavailable on Darwin.

config/config.go (1)

804-807: LGTM — prevents osrelease.Read() failure on macOS.

/etc/os-release doesn't exist on macOS, so this early return is necessary to avoid a runtime error.

config/config_openat_linux_test.go (1)

5-25: LGTM — clean config override test.

Properly resets the atomic cache before each assertion, ensuring the config mode is re-evaluated. Both forced "openat" and "openat2" paths are covered.

internal/ufs/fs_unix.go (3)

36-49: LGTM — symlink resolution in base path is essential for macOS.

On macOS, /var is a symlink to /private/var, so without this resolution, the unsafeIsPathInsideOfBase prefix check would fail for any fd whose resolved path goes through /private/var. Silently ignoring EvalSymlinks errors is reasonable here — the path may not exist yet during initialization.


682-683: Good: fdPath(fd) replaces the Linux-specific /proc/self/fd approach.

This is the correct abstraction for cross-platform fd-to-path resolution (Linux uses /proc/self/fd, Darwin uses fcntl(F_GETPATH)).


762-762: Correct: AT_FDCWD is the right choice for opening an absolute path.

Using AT_FDCWD with the absolute fs.basePath is semantically correct and portable across Unix platforms.

internal/ufs/fs_platform_test.go (4)

15-53: LGTM — clean stat validation test.

Covers the essential fields (name, size, directory flag, mod time) through the UnixFS abstraction.


55-88: Good sandbox escape test.

This exercises the critical security invariant that symlinks pointing outside the sandbox root are blocked. Using os.Symlink directly (bypassing UnixFS) correctly simulates an external actor creating the escape link.


90-130: Test depends on timing — consider a more robust assertion.

The 50ms sleep + 100ms tolerance window is reasonable for CI but could be flaky on heavily loaded machines where the filesystem timestamp granularity is coarse (e.g., some filesystems have 1-second or 2-second granularity like HFS+, which is relevant for macOS).

If the test target filesystem on macOS uses APFS (nanosecond granularity), this should be fine. But if run on HFS+ volumes, origMtime and the post-Chtimes mtime could both round to the same second, making the test pass vacuously (not actually verifying preservation). Worth noting but not blocking.


132-145: LGTM — validates platform-specific constant.

Correctly asserts O_LARGEFILE is non-zero on Linux (from unix.O_LARGEFILE) and zero on Darwin (where large file support is default).

internal/ufs/walk_unix.go (1)

187-187: LGTM — clean delegation to platform-specific getdents wrapper.

This change properly delegates OS-specific directory entry reading to the walk_linux.go and walk_darwin.go implementations, with both getdents and nameFromDirent correctly defined in each platform file.

config/config_openat_linux.go (1)

10-13: LGTM on the atomic caching variables.

The two package-level atomic booleans cleanly separate the "is set" flag from the "value" flag.

internal/ufs/fs_linux.go (2)

13-14: LGTM — standard /proc/self/fd approach for Linux.


23-54: LGTM — solid openat2 wrapper with proper flag defaults and RESOLVE_BENEATH.

The EINTR/EAGAIN pass-through without ensurePathError wrapping is correct, as callers will retry the operation.

internal/ufs/fs_darwin.go (2)

15-27: LGTM — correct use of F_GETPATH with runtime.KeepAlive to prevent premature GC of the buffer.


32-34: LGTM — clean stub returning ENOSYS.

internal/ufs/walk_linux.go (1)

10-13: LGTM — thin wrapper over Getdents.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread internal/ufs/fs_darwin.go
Comment thread internal/ufs/fs_unix.go Outdated
Comment thread internal/ufs/walk_linux.go
Comment thread Makefile Outdated
Split Linux-only syscalls into _linux.go/_darwin.go file pairs:
- ufs: openat2 (ENOSYS stub on Darwin), fdPath via F_GETPATH fcntl,
  Chtimesat without UTIME_OMIT, Getdirentries-based getdents, and
  path-based dirent Info/Open (Getdirentries corrupts the stored dirfd)
- config: UseOpenat2 probe moved to a Linux file with a Darwin stub
- server/filesystem: Darwin CTime; non-Linux stub for the quotas package
- system: report macOS without reading os-release
- environment: never set BlkioWeight on non-Linux hosts

Resolve symlinks in UnixFS base paths (/var -> /private/var on macOS)
and use AT_FDCWD instead of AT_EMPTY_PATH as the safePath dirfd.

Also fixes a latent bug where root-level dirents got the parent
directory's name as their path; harmless on Linux (which stats via
dirfd+name) but exposed by Darwin's path-based dirent methods.
CTime checked for *unix.Stat_t twice; the second branch was clearly
meant to handle *syscall.Stat_t, which is what Sys() returns for
FileInfos produced by (*os.File).Stat — the path Filesystem.Stat uses.
As a result the file API's created timestamps were the zero time on
Linux. Caught by the new TestStatCTime.

@parkervcp parkervcp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some comments about using the go:build options to files so they get ignored at compile time and don't need runtime args.

Comment thread config/config_openat_darwin.go
Comment thread config/config_openat_linux.go
Adds //go:build darwin / //go:build linux tags to the darwin- and
linux-suffixed source and test files. The filename suffixes already
imply these constraints, but the explicit tags make the platform
scoping clear at the top of each file, per review feedback.

@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.

Caution

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

⚠️ Outside diff range comments (2)
internal/ufs/fs_unix.go (1)

684-721: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Fix file descriptor leak and clean up dead code on path validation failures.

When path validation fails, the successfully opened file descriptor is returned alongside the error. Since callers (e.g., safePath) treat a non-nil error as a total failure, they do not close the returned descriptor, leading to a resource leak.

Additionally:

  • The check if fs.useOpenat2.Load() at line 709 is dead code. The function already returns early at line 678 if useOpenat2 is true, and the value only transitions from true to false, meaning op will always be "openat" at this point.
  • The var finalPath string declaration is redundant and can be merged into the := assignment.

Close the file descriptor before returning an error and return 0 to prevent callers from accidentally using an invalid or unowned descriptor.

🔒️ Proposed fix to prevent resource leaks and remove dead code
-	var finalPath string
-	finalPath, err := fdPath(fd)
+	finalPath, err := fdPath(fd)
 	if err != nil {
 		if !errors.Is(err, ErrNotExist) {
+			_ = unix.Close(fd)
-			return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
+			return 0, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
 		}

 		// The target of one of the symlinks (EvalSymlinks is recursive)
 		// does not exist. So get the path that does not exist and use
 		// that for further validation instead.
 		var pErr *PathError
 		if !errors.As(err, &pErr) {
+			_ = unix.Close(fd)
-			return fd, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
+			return 0, fmt.Errorf("failed to evaluate symlink: %w", convertErrorType(err))
 		}

 		// Update the final path to whatever directory or path didn't exist while
 		// recursing any symlinks.
 		finalPath = pErr.Path
 		// Ensure the error is wrapped correctly.
 		err = convertErrorType(err)
 	}

 	// Check if the path is within our root.
 	if !fs.unsafeIsPathInsideOfBase(finalPath) {
-		op := "openat"
-		if fs.useOpenat2.Load() {
-			op = "openat2"
-		}
+		_ = unix.Close(fd)
-		return fd, &PathError{
+		return 0, &PathError{
-			Op:   op,
+			Op:   "openat",
 			Path: name,
 			Err:  ErrBadPathResolution,
 		}
 	}

 	// Return the file descriptor and any potential error.
+	if err != nil {
+		_ = unix.Close(fd)
+		return 0, err
+	}
-	return fd, err
+	return fd, 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 `@internal/ufs/fs_unix.go` around lines 684 - 721, Update the path-validation
error returns in the function containing fdPath so they close fd, return
descriptor 0, and preserve the error. Remove the redundant useOpenat2 check and
always use the openat operation name there, and merge finalPath declaration with
its fdPath assignment.
.github/workflows/push.yaml (1)

29-30: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Prevent credential persistence in checkout action.

By default, the actions/checkout step persists the GitHub token in the local .git config. As a security best practice, this should be explicitly disabled to prevent potential token leakage if untrusted code executes during the build or test steps, especially on public repositories or when building/uploading artifacts.

  • .github/workflows/push.yaml#L29-L30: Add with: persist-credentials: false to the checkout action in the build-and-test job.
  • .github/workflows/push.yaml#L91-L92: Add with: persist-credentials: false to the checkout action in the test-macos job.
🛡️ Proposed fixes

For the build-and-test job (.github/workflows/push.yaml#L29-L30):

       - name: Code checkout
         uses: actions/checkout@v6
+        with:
+          persist-credentials: false

For the test-macos job (.github/workflows/push.yaml#L91-L92):

       - name: Code checkout
         uses: actions/checkout@v6
+        with:
+          persist-credentials: false
🤖 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 @.github/workflows/push.yaml around lines 29 - 30, Disable credential
persistence for both checkout steps by adding the checkout action input
persist-credentials: false in .github/workflows/push.yaml lines 29-30 for the
build-and-test job and lines 91-92 for the test-macos job.

Source: Linters/SAST tools

🤖 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.

Outside diff comments:
In @.github/workflows/push.yaml:
- Around line 29-30: Disable credential persistence for both checkout steps by
adding the checkout action input persist-credentials: false in
.github/workflows/push.yaml lines 29-30 for the build-and-test job and lines
91-92 for the test-macos job.

In `@internal/ufs/fs_unix.go`:
- Around line 684-721: Update the path-validation error returns in the function
containing fdPath so they close fd, return descriptor 0, and preserve the error.
Remove the redundant useOpenat2 check and always use the openat operation name
there, and merge finalPath declaration with its fdPath assignment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71d93ebc-585f-4dbe-92ac-00272a71c901

📥 Commits

Reviewing files that changed from the base of the PR and between b4630fa and 3e6536f.

📒 Files selected for processing (31)
  • .github/workflows/push.yaml
  • Makefile
  • config/config.go
  • config/config_openat_darwin.go
  • config/config_openat_linux.go
  • config/config_openat_linux_test.go
  • config/config_openat_test.go
  • environment/settings.go
  • internal/ufs/file.go
  • internal/ufs/file_darwin.go
  • internal/ufs/file_linux.go
  • internal/ufs/fs_darwin.go
  • internal/ufs/fs_linux.go
  • internal/ufs/fs_platform_test.go
  • internal/ufs/fs_unix.go
  • internal/ufs/fs_unix_test.go
  • internal/ufs/readdirmap_test.go
  • internal/ufs/walk_darwin.go
  • internal/ufs/walk_dirent_darwin.go
  • internal/ufs/walk_dirent_linux.go
  • internal/ufs/walk_linux.go
  • internal/ufs/walk_unix.go
  • macos.md
  • router/router_server_ws.go
  • router/websocket/listeners.go
  • server/filesystem/filesystem_test.go
  • server/filesystem/quotas/functions_other.go
  • server/filesystem/stat_darwin.go
  • server/filesystem/stat_linux.go
  • server/filesystem/stat_test.go
  • system/system.go
🚧 Files skipped from review as they are similar to previous changes (22)
  • config/config_openat_test.go
  • internal/ufs/walk_dirent_linux.go
  • internal/ufs/file_linux.go
  • config/config_openat_linux_test.go
  • internal/ufs/file_darwin.go
  • internal/ufs/walk_dirent_darwin.go
  • internal/ufs/fs_linux.go
  • server/filesystem/stat_darwin.go
  • internal/ufs/walk_darwin.go
  • router/router_server_ws.go
  • config/config_openat_linux.go
  • server/filesystem/stat_test.go
  • Makefile
  • internal/ufs/file.go
  • internal/ufs/walk_linux.go
  • system/system.go
  • internal/ufs/fs_darwin.go
  • internal/ufs/fs_platform_test.go
  • internal/ufs/fs_unix_test.go
  • router/websocket/listeners.go
  • config/config.go
  • internal/ufs/walk_unix.go
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.26.4, linux, amd64)
  • GitHub Check: Test macOS (1.25.11)
  • GitHub Check: Test macOS (1.26.4)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.25.11, linux, amd64)
  • GitHub Check: Analyze (go)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-02T13:53:08.995Z
Learnt from: parkervcp
Repo: pelican-dev/wings PR: 171
File: server/power.go:190-203
Timestamp: 2026-03-02T13:53:08.995Z
Learning: In the server package, when quotas are enabled via config.Get().System.Quotas.Enabled, the disk space check using used >= s.DiskSpace() does not require a special guard for unlimited-disk scenarios (DiskSpace() <= 0). The filesystem handles such cases, so the existing check is sufficient. Apply this pattern to similar quota-related disk checks in the server package and ensure tests/docs reflect that unlimited-disk behavior is governed by the filesystem, not by an extra guard in code.

Applied to files:

  • server/filesystem/quotas/functions_other.go
  • server/filesystem/stat_linux.go
  • server/filesystem/filesystem_test.go
🪛 ast-grep (0.44.1)
internal/ufs/fs_unix.go

[warning] 660-660: Narrowing a non-constant integer to a smaller fixed-width type (int8/int16/int32, uint8/uint16/uint32) can silently overflow or wrap, yielding negative or truncated values that are dangerous in size, length, or index logic. Validate the source value is within the target type's range before converting (e.g. bounds-check, or use a checked helper), and avoid narrowing untrusted or len()/parsed values.
Context: uint32(syscallMode(mode))
Note: [CWE-190] Integer Overflow or Wraparound.

(integer-overflow-narrowing-conversion-go)

🪛 zizmor (1.26.1)
.github/workflows/push.yaml

[warning] 29-30: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 91-92: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🔇 Additional comments (7)
internal/ufs/readdirmap_test.go (1)

1-256: LGTM!

server/filesystem/filesystem_test.go (1)

34-37: LGTM!

config/config_openat_darwin.go (1)

1-9: LGTM!

environment/settings.go (1)

145-168: LGTM!

server/filesystem/quotas/functions_other.go (1)

1-36: LGTM!

server/filesystem/stat_linux.go (1)

17-27: LGTM!

macos.md (1)

1-137: LGTM!

@lancepioch
lancepioch requested a review from parkervcp July 18, 2026 13:35
When the post-open path validation in UnixFS.openat failed, the
successfully opened descriptor was returned alongside the error.
Callers treat a non-nil error as a total failure and never close the
returned fd, leaking the descriptor.

Close the descriptor and return 0 on every validation-error path,
including the non-existent-symlink-target case that previously returned
the fd with a non-nil error. Also drop the dead openat2 branch when
building the PathError op: the openat2 path returns early before this
check, so the op is always "openat".
actions/checkout persists the GitHub token in the local .git config by
default. Since both jobs build and upload artifacts, set
persist-credentials: false on each checkout to avoid leaking the token
into the workspace.

@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.

Caution

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

⚠️ Outside diff range comments (1)
internal/ufs/fs_unix.go (1)

703-724: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the artifact error to prevent failures on benign broken symlinks.

The if err != nil block at line 721 will unintentionally fail the open operation and close the descriptor if the file is a benign broken symlink.

Because _openat includes O_NOFOLLOW (line 649), it successfully opens broken symlinks and returns a valid fd. The ErrNotExist here is merely an artifact of filepath.EvalSymlinks (used within Linux's fdPath) aggressively attempting to resolve the final target.

Once we extract the target path to verify it hasn't escaped the sandbox (line 710), we should clear the error so the function successfully returns the valid file descriptor. This ensures the fallback path behaves identically to the openat2(RESOLVE_BENEATH) fast-path, which correctly permits opening broken symlinks.

💡 Proposed fix to retain broken symlink support
-		// Ensure the error is wrapped correctly.
-		err = convertErrorType(err)
+		// Clear the artifact error so we return the valid descriptor for the broken symlink,
+		// consistent with openat2 behavior.
+		err = nil
 	}
 
 	// Check if the path is within our root. We always reach this point using
 	// openat (the openat2 path returns early above), so the op is always
 	// "openat".
 	if !fs.unsafeIsPathInsideOfBase(finalPath) {
 		_ = unix.Close(fd)
 		return 0, &PathError{
 			Op:   "openat",
 			Path: name,
 			Err:  ErrBadPathResolution,
 		}
 	}
 
-	// If path validation surfaced an error (e.g. a non-existent symlink
-	// target), close the descriptor so callers don't leak it.
-	if err != nil {
-		_ = unix.Close(fd)
-		return 0, err
-	}
-
 	// Return the validated file descriptor.
 	return fd, 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 `@internal/ufs/fs_unix.go` around lines 703 - 724, Clear the artifact error
after validating the extracted target path in the openat fallback, before the
final if err != nil check. In the surrounding path-resolution flow, preserve
genuine path-boundary failures while discarding the benign EvalSymlinks
ErrNotExist produced for a successfully opened broken symlink, allowing the
valid descriptor to be returned.
🤖 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.

Outside diff comments:
In `@internal/ufs/fs_unix.go`:
- Around line 703-724: Clear the artifact error after validating the extracted
target path in the openat fallback, before the final if err != nil check. In the
surrounding path-resolution flow, preserve genuine path-boundary failures while
discarding the benign EvalSymlinks ErrNotExist produced for a successfully
opened broken symlink, allowing the valid descriptor to be returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 507369d6-bf4c-482b-9d5c-2bf8eb347a84

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6536f and bf1015b.

📒 Files selected for processing (2)
  • .github/workflows/push.yaml
  • internal/ufs/fs_unix.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/push.yaml
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.25.11, linux, arm64)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.26.4, linux, amd64)
  • GitHub Check: Test macOS (1.25.11)
  • GitHub Check: Build and Test (ubuntu-22.04, 1.25.11, linux, amd64)
  • GitHub Check: Test macOS (1.26.4)

@lancepioch

Copy link
Copy Markdown
Member Author

Re: the "clear the artifact error to prevent failures on benign broken symlinks" comment (internal/ufs/fs_unix.go)

I looked into this and I don't think it's a real issue, so I'm leaving the code as-is.

The suggestion assumes _openat can open a broken symlink and hand back a working fd. It can't. We always force O_NOFOLLOW on and we never pass O_PATH, so opening a symlink just fails with ELOOP right away — we never even reach the ErrNotExist branch for that case.

On top of that, the old code already returned a non-nil ErrNotExist here, and every caller bails out on any error and never uses the fd. So my change returns the exact same error to callers — the only thing it changes is that it closes the fd instead of leaking it.

Setting err = nil like the suggestion would actually be a behavior change, not a fix: it would turn a case that always failed into a success, which could hide real "not found" errors. So I'm skipping this one.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants