1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package exec_test
6
7import (
8	"errors"
9	"internal/syscall/unix"
10	"internal/testenv"
11	"os"
12	"os/exec"
13	"path/filepath"
14	"syscall"
15	"testing"
16)
17
18func TestFindExecutableVsNoexec(t *testing.T) {
19	t.Parallel()
20
21	// This test case relies on faccessat2(2) syscall, which appeared in Linux v5.8.
22	if major, minor := unix.KernelVersion(); major < 5 || (major == 5 && minor < 8) {
23		t.Skip("requires Linux kernel v5.8 with faccessat2(2) syscall")
24	}
25
26	tmp := t.TempDir()
27
28	// Create a tmpfs mount.
29	err := syscall.Mount("tmpfs", tmp, "tmpfs", 0, "")
30	if testenv.SyscallIsNotSupported(err) {
31		// Usually this means lack of CAP_SYS_ADMIN, but there might be
32		// other reasons, especially in restricted test environments.
33		t.Skipf("requires ability to mount tmpfs (%v)", err)
34	} else if err != nil {
35		t.Fatalf("mount %s failed: %v", tmp, err)
36	}
37	t.Cleanup(func() {
38		if err := syscall.Unmount(tmp, 0); err != nil {
39			t.Error(err)
40		}
41	})
42
43	// Create an executable.
44	path := filepath.Join(tmp, "program")
45	err = os.WriteFile(path, []byte("#!/bin/sh\necho 123\n"), 0o755)
46	if err != nil {
47		t.Fatal(err)
48	}
49
50	// Check that it works as expected.
51	_, err = exec.LookPath(path)
52	if err != nil {
53		t.Fatalf("LookPath: got %v, want nil", err)
54	}
55
56	for {
57		err = exec.Command(path).Run()
58		if err == nil {
59			break
60		}
61		if errors.Is(err, syscall.ETXTBSY) {
62			// A fork+exec in another process may be holding open the FD that we used
63			// to write the executable (see https://go.dev/issue/22315).
64			// Since the descriptor should have CLOEXEC set, the problem should resolve
65			// as soon as the forked child reaches its exec call.
66			// Keep retrying until that happens.
67		} else {
68			t.Fatalf("exec: got %v, want nil", err)
69		}
70	}
71
72	// Remount with noexec flag.
73	err = syscall.Mount("", tmp, "", syscall.MS_REMOUNT|syscall.MS_NOEXEC, "")
74	if testenv.SyscallIsNotSupported(err) {
75		t.Skipf("requires ability to re-mount tmpfs (%v)", err)
76	} else if err != nil {
77		t.Fatalf("remount %s with noexec failed: %v", tmp, err)
78	}
79
80	if err := exec.Command(path).Run(); err == nil {
81		t.Fatal("exec on noexec filesystem: got nil, want error")
82	}
83
84	_, err = exec.LookPath(path)
85	if err == nil {
86		t.Fatalf("LookPath: got nil, want error")
87	}
88}
89