diff --git a/src/internal/syscall/unix/at_sysnum_linux.go b/src/internal/syscall/unix/at_sysnum_linux.go index fa7cd75d42653..b9b8495e32ac3 100644 --- a/src/internal/syscall/unix/at_sysnum_linux.go +++ b/src/internal/syscall/unix/at_sysnum_linux.go @@ -9,5 +9,9 @@ import "syscall" const unlinkatTrap uintptr = syscall.SYS_UNLINKAT const openatTrap uintptr = syscall.SYS_OPENAT -const AT_REMOVEDIR = 0x200 -const AT_SYMLINK_NOFOLLOW = 0x100 +const ( + AT_EACCESS = 0x200 + AT_FDCWD = -0x64 + AT_REMOVEDIR = 0x200 + AT_SYMLINK_NOFOLLOW = 0x100 +) diff --git a/src/internal/syscall/unix/constants.go b/src/internal/syscall/unix/constants.go new file mode 100644 index 0000000000000..e3245897055cc --- /dev/null +++ b/src/internal/syscall/unix/constants.go @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package unix + +const ( + R_OK = 0x4 + W_OK = 0x2 + X_OK = 0x1 +) diff --git a/src/internal/syscall/unix/eaccess_linux.go b/src/internal/syscall/unix/eaccess_linux.go new file mode 100644 index 0000000000000..5695a5e4ce7cd --- /dev/null +++ b/src/internal/syscall/unix/eaccess_linux.go @@ -0,0 +1,11 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package unix + +import "syscall" + +func Eaccess(path string, mode uint32) error { + return syscall.Faccessat(AT_FDCWD, path, mode, AT_EACCESS) +} diff --git a/src/internal/syscall/unix/eaccess_other.go b/src/internal/syscall/unix/eaccess_other.go new file mode 100644 index 0000000000000..23be11829755b --- /dev/null +++ b/src/internal/syscall/unix/eaccess_other.go @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix && !linux + +package unix + +import "syscall" + +func Eaccess(path string, mode uint32) error { + return syscall.ENOSYS +} diff --git a/src/os/exec/lp_linux_test.go b/src/os/exec/lp_linux_test.go new file mode 100644 index 0000000000000..96051b5490094 --- /dev/null +++ b/src/os/exec/lp_linux_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package exec + +import ( + "internal/syscall/unix" + "os" + "path/filepath" + "syscall" + "testing" +) + +func TestFindExecutableVsNoexec(t *testing.T) { + // This test case relies on faccessat2(2) syscall, which appeared in Linux v5.8. + if major, minor := unix.KernelVersion(); major < 5 || (major == 5 && minor < 8) { + t.Skip("requires Linux kernel v5.8 with faccessat2(2) syscall") + } + + tmp := t.TempDir() + + // Create a tmpfs mount. + err := syscall.Mount("tmpfs", tmp, "tmpfs", 0, "") + if err != nil { + if os.Geteuid() == 0 { + t.Fatalf("tmpfs mount failed: %v", err) + } + // Requires root or CAP_SYS_ADMIN. + t.Skipf("requires ability to mount tmpfs (%v)", err) + } + t.Cleanup(func() { + if err := syscall.Unmount(tmp, 0); err != nil { + t.Error(err) + } + }) + + // Create an executable. + path := filepath.Join(tmp, "program") + err = os.WriteFile(path, []byte("#!/bin/sh\necho 123\n"), 0o755) + if err != nil { + t.Fatal(err) + } + + // Check that it works as expected. + err = findExecutable(path) + if err != nil { + t.Fatalf("findExecutable: got %v, want nil", err) + } + + if err := Command(path).Run(); err != nil { + t.Fatalf("exec: got %v, want nil", err) + } + + // Remount with noexec flag. + err = syscall.Mount("", tmp, "", syscall.MS_REMOUNT|syscall.MS_NOEXEC, "") + if err != nil { + t.Fatalf("remount %s with noexec failed: %v", tmp, err) + } + + if err := Command(path).Run(); err == nil { + t.Fatal("exec on noexec filesystem: got nil, want error") + } + + err = findExecutable(path) + if err == nil { + t.Fatalf("findExecutable: got nil, want error") + } +} diff --git a/src/os/exec/lp_unix.go b/src/os/exec/lp_unix.go index b2b412c96b6f9..af68c2f268a84 100644 --- a/src/os/exec/lp_unix.go +++ b/src/os/exec/lp_unix.go @@ -9,10 +9,12 @@ package exec import ( "errors" "internal/godebug" + "internal/syscall/unix" "io/fs" "os" "path/filepath" "strings" + "syscall" ) // ErrNotFound is the error resulting if a path search failed to find an executable file. @@ -23,7 +25,18 @@ func findExecutable(file string) error { if err != nil { return err } - if m := d.Mode(); !m.IsDir() && m&0111 != 0 { + m := d.Mode() + if m.IsDir() { + return syscall.EISDIR + } + err = unix.Eaccess(file, unix.X_OK) + // ENOSYS means Eaccess is not available or not implemented. + // EPERM can be returned by Linux containers employing seccomp. + // In both cases, fall back to checking the permission bits. + if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) { + return err + } + if m&0111 != 0 { return nil } return fs.ErrPermission