Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ rmdebug:
go build -gcflags "all=-N -l" -ldflags="-X github.com/pelican-dev/wings/system.Version=$(GIT_HEAD)" -race
sudo dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./wings -- --debug --ignore-certificate-errors --config config.yml

cross-build: clean build compress
build-darwin:
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_darwin_arm64 -v wings.go
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_darwin_amd64 -v wings.go

cross-build: clean build build-darwin

clean:
rm -rf build/wings_*

.PHONY: all build compress clean
.PHONY: all build build-darwin cross-build clean test debug rmdebug
51 changes: 16 additions & 35 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"sync/atomic"
"text/template"
"time"

Expand All @@ -22,7 +22,6 @@ import (
"github.com/apex/log"
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"golang.org/x/sys/unix"
"gopkg.in/yaml.v2"

"github.com/pelican-dev/wings/system"
Expand Down Expand Up @@ -503,6 +502,18 @@ func EnsurePelicanUser() error {
return err
}

// macOS doesn't have useradd, use the current user like rootless mode.
if sysName == "darwin" {
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
}

// Our way of detecting if wings is running inside of Docker.
if sysName == "distroless" {
_config.System.Username = system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pelican")
Expand Down Expand Up @@ -791,6 +802,9 @@ func ConfigureTimezone() error {

// Gets the system release name.
func getSystemName() (string, error) {
if runtime.GOOS == "darwin" {
return "darwin", nil
}
// use osrelease to get release version and ID
release, err := osrelease.Read()
if err != nil {
Expand All @@ -799,39 +813,6 @@ func getSystemName() (string, error) {
return release["ID"], nil
}

var (
openat2 atomic.Bool
openat2Set atomic.Bool
)

func UseOpenat2() bool {
if openat2Set.Load() {
return openat2.Load()
}
defer openat2Set.Store(true)

c := Get()
openatMode := c.System.OpenatMode
switch openatMode {
case "openat2":
openat2.Store(true)
return true
case "openat":
openat2.Store(false)
return false
default:
fd, err := unix.Openat2(unix.AT_FDCWD, "/", &unix.OpenHow{})
if err != nil {
log.WithError(err).Warn("error occurred while checking for openat2 support, falling back to openat")
openat2.Store(false)
return false
}
_ = unix.Close(fd)
openat2.Store(true)
return true
}
}

// Expand expands an input string by calling [os.ExpandEnv] to expand all
// environment variables, then checks if the value is prefixed with `file://`
// to support reading the value from a file.
Expand Down
7 changes: 7 additions & 0 deletions config/config_openat_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

// UseOpenat2 always returns false on Darwin as the openat2 syscall is
// Linux-specific (kernel 5.6+).
func UseOpenat2() bool {
return false
}
41 changes: 41 additions & 0 deletions config/config_openat_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package config

import (
"sync/atomic"

"github.com/apex/log"
"golang.org/x/sys/unix"
)

var (
openat2 atomic.Bool
openat2Set atomic.Bool
)

func UseOpenat2() bool {
if openat2Set.Load() {
return openat2.Load()
}
defer openat2Set.Store(true)

c := Get()
openatMode := c.System.OpenatMode
switch openatMode {
case "openat2":
openat2.Store(true)
return true
case "openat":
openat2.Store(false)
return false
default:
fd, err := unix.Openat2(unix.AT_FDCWD, "/", &unix.OpenHow{})
if err != nil {
log.WithError(err).Warn("error occurred while checking for openat2 support, falling back to openat")
openat2.Store(false)
return false
}
_ = unix.Close(fd)
openat2.Store(true)
return true
}
}
26 changes: 26 additions & 0 deletions config/config_openat_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

import "testing"

func TestUseOpenat2ConfigOverride(t *testing.T) {
Set(&Configuration{
AuthenticationToken: "test",
System: SystemConfiguration{
OpenatMode: "openat",
},
})
openat2Set.Store(false)

if UseOpenat2() {
t.Error("expected UseOpenat2() to return false when mode is 'openat'")
}

openat2Set.Store(false)
Update(func(c *Configuration) {
c.System.OpenatMode = "openat2"
})

if !UseOpenat2() {
t.Error("expected UseOpenat2() to return true when mode is 'openat2'")
}
}
29 changes: 29 additions & 0 deletions config/config_openat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package config

import (
"runtime"
"testing"
)

func TestUseOpenat2(t *testing.T) {
// Ensure UseOpenat2 doesn't panic.
Set(&Configuration{
AuthenticationToken: "test",
System: SystemConfiguration{
OpenatMode: "auto",
},
})

result := UseOpenat2()

switch runtime.GOOS {
case "darwin":
if result {
t.Error("expected UseOpenat2() to return false on Darwin")
}
case "linux":
// On Linux it may be true or false depending on kernel version.
// Just verify it returns without error.
t.Logf("UseOpenat2() returned %v on Linux", result)
}
}
6 changes: 5 additions & 1 deletion environment/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package environment
import (
"fmt"
"math"
"runtime"
"strconv"

"github.com/apex/log"
Expand Down Expand Up @@ -112,11 +113,14 @@ func (l Limits) AsContainerResources() container.Resources {
Memory: l.BoundedMemoryLimit(),
MemoryReservation: l.MemoryLimit * 1024 * 1024,
MemorySwap: l.ConvertedSwap(),
BlkioWeight: l.IoWeight,
OomKillDisable: boolPtr(!l.OOMKiller),
PidsLimit: &pids,
}

if runtime.GOOS == "linux" {
resources.BlkioWeight = l.IoWeight
}

// If the CPU Limit is not set, don't send any of these fields through. Providing
// them seems to break some Java services that try to read the available processors.
//
Expand Down
4 changes: 1 addition & 3 deletions internal/ufs/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,10 @@ const (
O_DIRECTORY = unix.O_DIRECTORY
// O_NOFOLLOW opens the exact path given without following symlinks.
O_NOFOLLOW = unix.O_NOFOLLOW
O_CLOEXEC = unix.O_CLOEXEC
O_LARGEFILE = unix.O_LARGEFILE
O_CLOEXEC = unix.O_CLOEXEC
)

const (
AT_SYMLINK_NOFOLLOW = unix.AT_SYMLINK_NOFOLLOW
AT_REMOVEDIR = unix.AT_REMOVEDIR
AT_EMPTY_PATH = unix.AT_EMPTY_PATH
)
4 changes: 4 additions & 0 deletions internal/ufs/file_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ufs

// O_LARGEFILE is a no-op on Darwin as all files support large offsets.
const O_LARGEFILE = 0
5 changes: 5 additions & 0 deletions internal/ufs/file_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ufs

import "golang.org/x/sys/unix"

const O_LARGEFILE = unix.O_LARGEFILE
59 changes: 59 additions & 0 deletions internal/ufs/fs_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ufs

import (
"bytes"
"path/filepath"
"runtime"
"time"
"unsafe"

"golang.org/x/sys/unix"
)

// fdPath returns the filesystem path associated with a file descriptor using
// the F_GETPATH fcntl command on macOS.
func fdPath(fd int) (string, error) {
buf := make([]byte, unix.PathMax)
_, _, errno := unix.Syscall(unix.SYS_FCNTL, uintptr(fd), uintptr(unix.F_GETPATH), uintptr(unsafe.Pointer(&buf[0])))
runtime.KeepAlive(buf)
if errno != 0 {
return "", errno
}
n := bytes.IndexByte(buf, 0)
if n < 0 {
n = len(buf)
}
return filepath.EvalSymlinks(string(buf[:n]))
}

// _openat2 is a stub on Darwin. The openat2 syscall is Linux-specific (kernel
// 5.6+). On Darwin, this always returns ENOSYS to signal that the caller
// should fall back to the regular openat path.
func (fs *UnixFS) _openat2(dirfd int, name string, flag, mode uint64) (int, error) {
return 0, unix.ENOSYS
}

// Chtimesat is like Chtimes but allows passing an existing directory file
// descriptor rather than needing to resolve one. On Darwin, UTIME_OMIT is not
// available, so zero times are handled by reading the current timestamps and
// preserving them.
func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
if atime.IsZero() || mtime.IsZero() {
var st unix.Stat_t
if err := unix.Fstatat(dirfd, name, &st, 0); err != nil {
return ensurePathError(err, "chtimes", name)
}
if atime.IsZero() {
atime = time.Unix(st.Atim.Unix())
}
if mtime.IsZero() {
mtime = time.Unix(st.Mtim.Unix())
}
}

utimes := [2]unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(mtime.UnixNano()),
}
return ensurePathError(unix.UtimesNanoAt(dirfd, name, utimes[0:], 0), "chtimes", name)
Comment on lines +40 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check what version of golang.org/x/sys/unix is being used
fd -t f "go.mod" -o "go.sum" | head -5 | xargs grep -E "golang.org/x/sys"

Repository: pelican-dev/wings

Length of output: 194


🌐 Web query:

golang.org/x/sys/unix Stat_t Darwin Atimespec Mtimespec field names

💡 Result:

On Darwin (GOOS=darwin), golang.org/x/sys/unix.Stat_t uses the BSD-style timestamp fields:

  • Atimespec (access time)
  • Mtimespec (modification time)
  • Ctimespec (inode change time)
  • Birthtimespec (creation/birth time) (git.linux.ucla.edu)

Each of those fields is a unix.Timespec, whose field names are:

So you access them like: st.Atimespec.Sec, st.Atimespec.Nsec, st.Mtimespec.Sec, etc. (docs.activestate.com)

Citations:


🏁 Script executed:

# Check the actual code in fs_darwin.go
cat -n internal/ufs/fs_darwin.go | sed -n '40,58p'

Repository: pelican-dev/wings

Length of output: 781


🏁 Script executed:

# Also check if there's a go.mod to understand dependency versions
cat go.mod | grep -A 5 "require"

Repository: pelican-dev/wings

Length of output: 826


Fix field names for Darwin Stat_t struct — Atim/Mtim don't exist on macOS.

On Darwin, unix.Stat_t uses Atimespec and Mtimespec (not Atim/Mtim as on Linux). Lines 47 and 50 reference nonexistent fields and will fail to compile on macOS.

Fix
 		if atime.IsZero() {
-			atime = time.Unix(st.Atim.Unix())
+			atime = time.Unix(st.Atimespec.Unix())
 		}
 		if mtime.IsZero() {
-			mtime = time.Unix(st.Mtim.Unix())
+			mtime = time.Unix(st.Mtimespec.Unix())
 		}
📝 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 (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
if atime.IsZero() || mtime.IsZero() {
var st unix.Stat_t
if err := unix.Fstatat(dirfd, name, &st, 0); err != nil {
return ensurePathError(err, "chtimes", name)
}
if atime.IsZero() {
atime = time.Unix(st.Atim.Unix())
}
if mtime.IsZero() {
mtime = time.Unix(st.Mtim.Unix())
}
}
utimes := [2]unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(mtime.UnixNano()),
}
return ensurePathError(unix.UtimesNanoAt(dirfd, name, utimes[0:], 0), "chtimes", name)
func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
if atime.IsZero() || mtime.IsZero() {
var st unix.Stat_t
if err := unix.Fstatat(dirfd, name, &st, 0); err != nil {
return ensurePathError(err, "chtimes", name)
}
if atime.IsZero() {
atime = time.Unix(st.Atimespec.Unix())
}
if mtime.IsZero() {
mtime = time.Unix(st.Mtimespec.Unix())
}
}
utimes := [2]unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(mtime.UnixNano()),
}
return ensurePathError(unix.UtimesNanoAt(dirfd, name, utimes[0:], 0), "chtimes", name)
🤖 Prompt for AI Agents
In `@internal/ufs/fs_darwin.go` around lines 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.

}
72 changes: 72 additions & 0 deletions internal/ufs/fs_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ufs

import (
"path/filepath"
"strconv"
"time"

"golang.org/x/sys/unix"
)

// fdPath returns the filesystem path associated with a file descriptor by
// reading the /proc/self/fd/ symlink.
func fdPath(fd int) (string, error) {
return filepath.EvalSymlinks(filepath.Join("/proc/self/fd/", strconv.Itoa(fd)))
}

// _openat2 is a wonderful syscall that supersedes the `openat` syscall. It has
// improved validation and security characteristics that weren't available or
// considered when `openat` was originally implemented. As such, it is only
// present in Kernel 5.6 and above.
//
// This method should never be directly called, use `openat` instead.
func (fs *UnixFS) _openat2(dirfd int, name string, flag, mode uint64) (int, error) {
// Ensure the O_CLOEXEC flag is set.
// Go sets this when using the os package, but since we are directly using
// the unix package we need to set it ourselves.
if flag&O_CLOEXEC == 0 {
flag |= O_CLOEXEC
}
// Ensure the O_LARGEFILE flag is set.
// Go sets this for unix.Open, unix.Openat, but not unix.Openat2.
if flag&O_LARGEFILE == 0 {
flag |= O_LARGEFILE
}
fd, err := unix.Openat2(dirfd, name, &unix.OpenHow{
Flags: flag,
Mode: mode,
// This is the bread and butter of preventing a symlink escape, without
// this option, we have to handle path validation fully on our own.
//
// This is why using Openat2 over Openat is preferred if available.
Resolve: unix.RESOLVE_BENEATH,
})
switch {
case err == nil:
return fd, nil
case err == unix.EINTR:
return fd, err
case err == unix.EAGAIN:
return fd, err
default:
return fd, ensurePathError(err, "openat2", name)
}
}

// Chtimesat is like Chtimes but allows passing an existing directory file
// descriptor rather than needing to resolve one.
func (fs *UnixFS) Chtimesat(dirfd int, name string, atime, mtime time.Time) error {
var utimes [2]unix.Timespec
set := func(i int, t time.Time) {
if t.IsZero() {
utimes[i] = unix.Timespec{Sec: unix.UTIME_OMIT, Nsec: unix.UTIME_OMIT}
} else {
utimes[i] = unix.NsecToTimespec(t.UnixNano())
}
}
set(0, atime)
set(1, mtime)

// This does support `AT_SYMLINK_NOFOLLOW` as well if needed.
return ensurePathError(unix.UtimesNanoAt(dirfd, name, utimes[0:], 0), "chtimes", name)
}
Loading
Loading