├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── landlock-abi-version │ └── main.go ├── landlock-restrict-net │ └── main.go └── landlock-restrict │ └── main.go ├── examples ├── go-landlock-configurable │ ├── cfg.json │ └── main.go └── go-landlock-convert │ └── main.go ├── go.mod ├── go.sum └── landlock ├── abi_versions.go ├── abi_versions_test.go ├── accessfs.go ├── accessfs_test.go ├── accessnet.go ├── config.go ├── config_test.go ├── landlock.go ├── lltest └── lltest.go ├── net_opt.go ├── opt.go ├── path_opt.go ├── path_opt_linux.go ├── path_opt_nonlinux.go ├── restrict.go ├── restrict_config_test.go ├── restrict_downgrade_test.go ├── restrict_failure_test.go ├── restrict_nonlinux.go ├── restrict_nonlinux_test.go ├── restrict_test.go ├── restrict_threading_test.go └── syscall ├── landlock.go ├── landlock_test.go ├── syscall_linux.go └── syscall_nonlinux.go /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | name: Lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Set up Go 10 | uses: actions/setup-go@v3 11 | with: 12 | go-version: 1.21 13 | - name: Vet 14 | run: go vet ./... 15 | - name: Staticcheck 16 | uses: dominikh/staticcheck-action@v1.3.0 17 | with: 18 | version: "2022.1.3" 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [push, pull_request] 3 | jobs: 4 | go_tests: 5 | strategy: 6 | matrix: 7 | os: 8 | - ubuntu-latest 9 | - macos-latest 10 | - windows-latest 11 | go: 12 | - '1.18' 13 | - '1.20' 14 | name: Go Tests 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - run: go test ./... 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | bin/* 3 | *~ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Günther Noack 4 | 5 | PERMISSION is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 📚 [Godoc](https://pkg.go.dev/github.com/landlock-lsm/go-landlock/landlock) 2 | | 🌍 [landlock.io](https://landlock.io/) 3 | 4 | # Go Landlock library 5 | 6 | The Go-Landlock library restricts the current processes' ability to 7 | use files, using Linux 5.13's Landlock feature. 8 | 9 | ## TL;DR: Example 10 | 11 | In a Go program, after starting up and doing program initialization work, run: 12 | 13 | ``` 14 | err := landlock.V5.BestEffort().RestrictPaths( 15 | landlock.RODirs("/usr", "/bin"), 16 | landlock.RWDirs("/tmp"), 17 | ) 18 | ``` 19 | 20 | After this invocation, your program can only access the specified paths. 21 | 22 | Landlock is a Linux kernel feature and can restrict the following types of access: 23 | 24 | * Filesystem access 25 | * Some network operations 26 | * Some IPC operations (not implemented yet in Go: [#35](https://github.com/landlock-lsm/go-landlock/issues/35)) 27 | 28 | More details and examples in the [Go-Landlock 29 | documentation](https://pkg.go.dev/github.com/landlock-lsm/go-landlock/landlock). 30 | 31 | ## Goals 32 | 33 | Goals of Go-Landlock are: 34 | 35 | * Make unprivileged sandboxing easy to use and effective. 36 | * Keep Go-Landlock's implementation at an easily auditable size. 37 | 38 | ## Technical implementation 39 | 40 | Some implementation notes that should simplify auditing. 41 | 42 | ### Applying Landlock to all Goroutines 43 | 44 | The Landlock kernel API enabled Landlock for the current OS thread, 45 | but the mapping between Goroutines and OS threads is not 1:1, and 46 | there are few guarantees about it. 47 | 48 | Because the mapping between OS threads and Goroutines is not 49 | guaranteed anyway, the Go-Landlock API always enables the given 50 | Landlock for the entire process. 51 | 52 | This is done using the `psx` library, which is a helper library for 53 | the `libcap` library for working with Linux capabilities. `psx` 54 | exposes an API that does a system call with the given arguments on 55 | *every OS thread* in a running Go program. 56 | 57 | For pure Go programs, `psx` does the same as the 58 | [`syscall.AllThreadsSyscall` 59 | function](https://pkg.go.dev/syscall#AllThreadsSyscall) in the Go 60 | runtime (and that case is straightforward to understand). 61 | 62 | For programs linked with `cgo`, there can be more OS threads than just 63 | the ones that were started by the Go runtime. To cover these, `psx` 64 | intercepts calls to the pthread library to infer the list of all 65 | threads, and then uses a trick with Unix signals to execute the given 66 | system call on all of them. (This is unfortunately common practice for 67 | system calls that only apply to the current thread -- glibc uses the 68 | same approach for some system calls. To dig this up in the glibc 69 | source, see `sysdeps/nptl/setxid.h` and its users.) 70 | 71 | A deeper discussion of `psx` can be found at: 72 | https://sites.google.com/site/fullycapable/who-ordered-libpsx 73 | -------------------------------------------------------------------------------- /cmd/landlock-abi-version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 7 | ) 8 | 9 | func main() { 10 | v, err := ll.LandlockGetABIVersion() 11 | if err != nil { 12 | fmt.Println("0") 13 | } else { // success 14 | fmt.Println(v) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/landlock-restrict-net/main.go: -------------------------------------------------------------------------------- 1 | // landlock-restrict-net executes a process with Landlock network restrictions 2 | // 3 | // This is an example tool which does not provide backwards 4 | // compatibility guarantees. 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "log" 11 | "os" 12 | "strconv" 13 | "syscall" 14 | 15 | "github.com/landlock-lsm/go-landlock/landlock" 16 | ) 17 | 18 | func usage() { 19 | var ( 20 | out = flag.CommandLine.Output() 21 | name = os.Args[0] 22 | ) 23 | fmt.Fprintf(out, "Usage of %s:\n", name) 24 | flag.PrintDefaults() 25 | fmt.Fprintf(out, "\nExample usages:\n") 26 | fmt.Fprintf(out, " %s -tcp.bind 8080 /usr/bin/nc -l 127.0.0.1 8080\n", name) 27 | fmt.Fprintf(out, " %s -tcp.connect 8080 /usr/bin/nc 127.0.0.1 8080\n", name) 28 | } 29 | 30 | func main() { 31 | flag.Usage = usage 32 | 33 | var rules []landlock.Rule 34 | flag.Func("tcp.bind", "A TCP port where bind(2) should be permitted", func(s string) error { 35 | p, err := strconv.ParseUint(s, 10, 16) 36 | if err != nil { 37 | return err 38 | } 39 | log.Println("PERMIT TCP bind on port", p) 40 | rules = append(rules, landlock.BindTCP(uint16(p))) 41 | return nil 42 | }) 43 | flag.Func("tcp.connect", "A TCP port where connect(2) should be permitted", func(s string) error { 44 | p, err := strconv.ParseUint(s, 10, 16) 45 | if err != nil { 46 | return err 47 | } 48 | log.Println("PERMIT TCP connect to port", p) 49 | rules = append(rules, landlock.ConnectTCP(uint16(p))) 50 | return nil 51 | }) 52 | 53 | flag.Parse() 54 | 55 | var cmd []string 56 | if flag.NArg() > 1 { 57 | cmd = flag.Args() 58 | } else { 59 | log.Println("missing command to call, using /bin/bash") 60 | cmd = []string{"/bin/bash"} 61 | } 62 | 63 | if err := landlock.V4.RestrictNet(rules...); err != nil { 64 | log.Fatalf("landlock RestrictNet: %v", err) 65 | } 66 | 67 | log.Printf("Starting %v", cmd) 68 | if err := syscall.Exec(cmd[0], cmd, os.Environ()); err != nil { 69 | log.Fatalf("execve: %v", err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cmd/landlock-restrict/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "syscall" 9 | 10 | "github.com/landlock-lsm/go-landlock/landlock" 11 | ) 12 | 13 | func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlock.Rule, cmd []string) { 14 | cfg = landlock.V5 15 | 16 | takeArgs := func(makeOpt func(...string) landlock.FSRule) landlock.Rule { 17 | var paths []string 18 | needRefer := false 19 | needIoctlDev := false 20 | for len(args) > 0 && !strings.HasPrefix(args[0], "-") { 21 | switch args[0] { 22 | case "+refer": 23 | needRefer = true 24 | case "+ioctl_dev": 25 | needIoctlDev = true 26 | default: 27 | paths = append(paths, args[0]) 28 | } 29 | args = args[1:] 30 | } 31 | opt := makeOpt(paths...) 32 | if verbose { 33 | fmt.Println("Path option:", opt) 34 | } 35 | if needRefer { 36 | opt = opt.WithRefer() 37 | } 38 | if needIoctlDev { 39 | opt = opt.WithIoctlDev() 40 | } 41 | if verbose { 42 | fmt.Println("Path option:", opt) 43 | } 44 | return opt 45 | } 46 | 47 | bestEffort := true 48 | ArgParsing: 49 | for len(args) > 0 { 50 | switch args[0] { 51 | case "-5": 52 | cfg = landlock.V5 53 | args = args[1:] 54 | continue 55 | case "-4": 56 | cfg = landlock.V4 57 | args = args[1:] 58 | continue 59 | case "-3": 60 | cfg = landlock.V3 61 | args = args[1:] 62 | continue 63 | case "-2": 64 | cfg = landlock.V2 65 | args = args[1:] 66 | continue 67 | case "-1": 68 | cfg = landlock.V1 69 | args = args[1:] 70 | continue 71 | case "-strict": 72 | bestEffort = false 73 | args = args[1:] 74 | continue 75 | case "-v": 76 | verbose = true 77 | args = args[1:] 78 | continue 79 | case "-ro": 80 | args = args[1:] 81 | opts = append(opts, takeArgs(landlock.RODirs)) 82 | continue 83 | case "-rw": 84 | args = args[1:] 85 | opts = append(opts, takeArgs(landlock.RWDirs)) 86 | continue 87 | case "-rofiles": 88 | args = args[1:] 89 | opts = append(opts, takeArgs(landlock.ROFiles)) 90 | continue 91 | case "-rwfiles": 92 | args = args[1:] 93 | opts = append(opts, takeArgs(landlock.RWFiles)) 94 | continue 95 | case "--": 96 | args = args[1:] 97 | // Remaining args are the command 98 | break ArgParsing 99 | default: 100 | log.Fatalf("Unrecognized option %q", args[0]) 101 | } 102 | } 103 | 104 | cmd = args 105 | if bestEffort { 106 | cfg = cfg.BestEffort() 107 | } 108 | return verbose, cfg, opts, cmd 109 | } 110 | 111 | func main() { 112 | verbose, cfg, opts, cmdArgs := parseFlags(os.Args[1:]) 113 | if verbose { 114 | fmt.Println("Args: ", os.Args) 115 | fmt.Println() 116 | fmt.Printf("Config: %v\n", cfg) 117 | fmt.Println() 118 | fmt.Printf("Executing command %v\n", cmdArgs) 119 | } 120 | 121 | if len(cmdArgs) < 1 { 122 | fmt.Println("Usage:") 123 | fmt.Println(" landlock-restrict") 124 | fmt.Println(" [-v]") 125 | fmt.Println(" [-1] [-2] [-3] [-4] [-5] [-strict]") 126 | fmt.Println(" [-ro [+refer] PATH...] [-rw [+refer] [+ioctl_dev] PATH...]") 127 | fmt.Println(" [-rofiles [+refer] PATH] [-rwfiles [+refer] PATH]") 128 | fmt.Println(" -- COMMAND...") 129 | fmt.Println() 130 | fmt.Println("Options:") 131 | fmt.Println(" -ro, -rw, -rofiles, -rwfiles paths to restrict to") 132 | fmt.Println(" -1, -2, -3, -4, -5 select Landlock version") 133 | fmt.Println(" -strict use strict mode (instead of best effort)") 134 | fmt.Println(" -v verbose logging") 135 | fmt.Println() 136 | fmt.Println("A path list that contains the word '+refer' will additionally grant the refer access right.") 137 | fmt.Println() 138 | fmt.Println("Default mode for Landlock is V5 in best effort mode (best compatibility)") 139 | fmt.Println() 140 | 141 | log.Fatalf("Need proper command, got %v", cmdArgs) 142 | } 143 | 144 | if !strings.HasPrefix(cmdArgs[0], "/") { 145 | log.Fatalf("Need absolute binary path, got %q", cmdArgs[0]) 146 | } 147 | 148 | err := cfg.RestrictPaths(opts...) 149 | if err != nil { 150 | log.Fatalf("landlock: %v", err) 151 | } 152 | 153 | if err := syscall.Exec(cmdArgs[0], cmdArgs, os.Environ()); err != nil { 154 | log.Fatalf("execve: %v", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /examples/go-landlock-configurable/cfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "forbidden_access": ["read_dir"], 3 | "exceptions": [{ 4 | "paths": ["/tmp", "/bin", "/etc"], 5 | "permitted_access": ["read_dir"] 6 | }] 7 | } 8 | -------------------------------------------------------------------------------- /examples/go-landlock-configurable/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/landlock-lsm/go-landlock/landlock" 12 | llsys "github.com/landlock-lsm/go-landlock/landlock/syscall" 13 | ) 14 | 15 | var ( 16 | cfgFile = flag.String("cfg_file", "", "config file (JSON)") 17 | ) 18 | 19 | type PathException struct { 20 | Paths []string `json:"paths"` 21 | PermittedAccess []string `json:"permitted_access"` 22 | } 23 | 24 | type Config struct { 25 | ForbiddenAccess []string `json:"forbidden_access"` 26 | Exceptions []PathException `json:"exceptions"` 27 | BestEffort bool `json:"best_effort"` 28 | } 29 | 30 | func main() { 31 | flag.Parse() 32 | 33 | // Read configuration file. 34 | buf, err := os.ReadFile(*cfgFile) 35 | if err != nil { 36 | log.Fatalf("io.ReadAll: %v", err) 37 | } 38 | 39 | var jsonCfg Config 40 | err = json.Unmarshal(buf, &jsonCfg) 41 | if err != nil { 42 | log.Fatalf("json.Unmarshal: %v", err) 43 | } 44 | 45 | // Print config for debugging. 46 | b, err := json.MarshalIndent(jsonCfg, "", " ") 47 | if err != nil { 48 | log.Fatalf("json.MarshalIndent: %v", err) 49 | } 50 | fmt.Println("JSON config:") 51 | fmt.Println(string(b)) 52 | 53 | // Build Landlock config. 54 | forbiddenAccess := accessFSSet(jsonCfg.ForbiddenAccess) 55 | cfg, err := landlock.NewConfig(forbiddenAccess) 56 | if err != nil { 57 | log.Fatalf("landlock.NewConfig: %v", err) 58 | } 59 | if jsonCfg.BestEffort { 60 | cfg2 := cfg.BestEffort() 61 | cfg = &cfg2 62 | } 63 | 64 | // Enforce. 65 | err = cfg.RestrictPaths(exceptions(jsonCfg.Exceptions)...) 66 | if err != nil { 67 | log.Fatalf("RestrictPaths: %v", err) 68 | } 69 | 70 | // Run an executable. 71 | executable := "/bin/bash" 72 | 73 | os.Chdir("/") 74 | cmd := exec.Command(executable) 75 | cmd.Stdin = os.Stdin 76 | cmd.Stdout = os.Stdout 77 | cmd.Stderr = os.Stderr 78 | if err := cmd.Run(); err != nil { 79 | log.Fatalf("execve: %v", err) 80 | } 81 | } 82 | 83 | func accessFSSet(names []string) (a landlock.AccessFSSet) { 84 | var table = map[string]landlock.AccessFSSet{ 85 | "execute": llsys.AccessFSExecute, 86 | "write_file": llsys.AccessFSWriteFile, 87 | "read_file": llsys.AccessFSReadFile, 88 | "read_dir": llsys.AccessFSReadDir, 89 | "remove_dir": llsys.AccessFSRemoveDir, 90 | "remove_file": llsys.AccessFSRemoveFile, 91 | "make_char": llsys.AccessFSMakeChar, 92 | "make_dir": llsys.AccessFSMakeDir, 93 | "make_reg": llsys.AccessFSMakeReg, 94 | "make_sock": llsys.AccessFSMakeSock, 95 | "make_fifo": llsys.AccessFSMakeFifo, 96 | "make_block": llsys.AccessFSMakeBlock, 97 | "make_sym": llsys.AccessFSMakeSym, 98 | "refer": llsys.AccessFSRefer, 99 | "truncate": llsys.AccessFSTruncate, 100 | } 101 | for _, n := range names { 102 | x, ok := table[n] 103 | if !ok { 104 | log.Fatalf("unknown access fs flag %q", n) 105 | } 106 | a |= x 107 | } 108 | return a 109 | } 110 | 111 | func exceptions(es []PathException) (opts []landlock.Rule) { 112 | for _, e := range es { 113 | permittedAccess := accessFSSet(e.PermittedAccess) 114 | po := landlock.PathAccess(permittedAccess, e.Paths...) 115 | opts = append(opts, po) 116 | } 117 | return opts 118 | } 119 | -------------------------------------------------------------------------------- /examples/go-landlock-convert/main.go: -------------------------------------------------------------------------------- 1 | // Command convert implements a landlocked image converter. 2 | // 3 | // Usage: 4 | // ./convert < input.jpeg > output.png 5 | // 6 | // This is a basic command line utility that reads from stdin and 7 | // writes to stdout. It has no business opening any additional files, 8 | // so we forbid it with a Landlock policy. Security issues in media 9 | // parsing libraries should not let the attacker access the file 10 | // system. 11 | package main 12 | 13 | import ( 14 | "image" 15 | _ "image/gif" 16 | _ "image/jpeg" 17 | "image/png" 18 | "log" 19 | "os" 20 | 21 | "github.com/landlock-lsm/go-landlock/landlock" 22 | ) 23 | 24 | func main() { 25 | if err := landlock.V3.BestEffort().RestrictPaths(); err != nil { 26 | log.Fatal("Could not enable Landlock:", err) 27 | } 28 | 29 | imgData, _, err := image.Decode(os.Stdin) 30 | if err != nil { 31 | log.Fatal("Could not read input:", err) 32 | } 33 | 34 | if err := png.Encode(os.Stdout, imgData); err != nil { 35 | log.Fatal("Could not write output:", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/landlock-lsm/go-landlock 2 | 3 | go 1.18 4 | 5 | require ( 6 | golang.org/x/sys v0.26.0 7 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 2 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 3 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= 4 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= 5 | -------------------------------------------------------------------------------- /landlock/abi_versions.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 4 | 5 | type abiInfo struct { 6 | version int 7 | supportedAccessFS AccessFSSet 8 | supportedAccessNet AccessNetSet 9 | } 10 | 11 | var abiInfos = []abiInfo{ 12 | { 13 | version: 0, 14 | supportedAccessFS: 0, 15 | }, 16 | { 17 | version: 1, 18 | supportedAccessFS: (1 << 13) - 1, 19 | }, 20 | { 21 | version: 2, 22 | supportedAccessFS: (1 << 14) - 1, 23 | }, 24 | { 25 | version: 3, 26 | supportedAccessFS: (1 << 15) - 1, 27 | }, 28 | { 29 | version: 4, 30 | supportedAccessFS: (1 << 15) - 1, 31 | supportedAccessNet: (1 << 2) - 1, 32 | }, 33 | { 34 | version: 5, 35 | supportedAccessFS: (1 << 16) - 1, 36 | supportedAccessNet: (1 << 2) - 1, 37 | }, 38 | } 39 | 40 | func (a abiInfo) asConfig() Config { 41 | return Config{ 42 | handledAccessFS: a.supportedAccessFS, 43 | handledAccessNet: a.supportedAccessNet, 44 | } 45 | } 46 | 47 | // getSupportedABIVersion returns the kernel-supported ABI version. 48 | // 49 | // If the ABI version supported by the kernel is higher than the 50 | // newest one known to go-landlock, the highest ABI version known to 51 | // go-landlock is returned. 52 | func getSupportedABIVersion() abiInfo { 53 | v, err := ll.LandlockGetABIVersion() 54 | if err != nil { 55 | v = 0 // ABI version 0 is "no Landlock support". 56 | } 57 | if v >= len(abiInfos) { 58 | v = len(abiInfos) - 1 59 | } 60 | return abiInfos[v] 61 | } 62 | -------------------------------------------------------------------------------- /landlock/abi_versions_test.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAbiVersionsIncrementing(t *testing.T) { 8 | for i, abiInfo := range abiInfos { 9 | if i != abiInfo.version { 10 | t.Errorf("Expected ABI version %d at index %d, got version %d", i, i, abiInfo.version) 11 | } 12 | } 13 | } 14 | 15 | func TestSupportedAccessFS(t *testing.T) { 16 | got := abiInfos[5].supportedAccessFS 17 | want := supportedAccessFS 18 | 19 | if got != want { 20 | t.Errorf("V3 supported access FS: got %v, want %v", got, want) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /landlock/accessfs.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | var accessFSNames = []string{ 9 | "execute", 10 | "write_file", 11 | "read_file", 12 | "read_dir", 13 | "remove_dir", 14 | "remove_file", 15 | "make_char", 16 | "make_dir", 17 | "make_reg", 18 | "make_sock", 19 | "make_fifo", 20 | "make_block", 21 | "make_sym", 22 | "refer", 23 | "truncate", 24 | "ioctl_dev", 25 | } 26 | 27 | // AccessFSSet is a set of Landlockable file system access operations. 28 | type AccessFSSet uint64 29 | 30 | var supportedAccessFS = AccessFSSet((1 << len(accessFSNames)) - 1) 31 | 32 | func accessSetString(a uint64, names []string) string { 33 | if a == 0 { 34 | return "∅" 35 | } 36 | var b strings.Builder 37 | b.WriteByte('{') 38 | for i := 0; i < 64; i++ { 39 | if a&(1< 1 { 43 | b.WriteByte(',') 44 | } 45 | if i < len(names) { 46 | b.WriteString(names[i]) 47 | } else { 48 | b.WriteString(fmt.Sprintf("1<<%v", i)) 49 | } 50 | } 51 | b.WriteByte('}') 52 | return b.String() 53 | } 54 | 55 | func (a AccessFSSet) String() string { 56 | return accessSetString(uint64(a), accessFSNames) 57 | } 58 | 59 | func (a AccessFSSet) isSubset(b AccessFSSet) bool { 60 | return a&b == a 61 | } 62 | 63 | func (a AccessFSSet) intersect(b AccessFSSet) AccessFSSet { 64 | return a & b 65 | } 66 | 67 | func (a AccessFSSet) union(b AccessFSSet) AccessFSSet { 68 | return a | b 69 | } 70 | 71 | func (a AccessFSSet) isEmpty() bool { 72 | return a == 0 73 | } 74 | 75 | // valid returns true iff the given AccessFSSet is supported by this 76 | // version of go-landlock. 77 | func (a AccessFSSet) valid() bool { 78 | return a.isSubset(supportedAccessFS) 79 | } 80 | -------------------------------------------------------------------------------- /landlock/accessfs_test.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "testing" 5 | 6 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 7 | ) 8 | 9 | func TestSubset(t *testing.T) { 10 | for _, tc := range []struct { 11 | a, b AccessFSSet 12 | want bool 13 | }{ 14 | {0b00110011, 0b01111011, true}, 15 | {0b00000001, 0b00000000, false}, 16 | {0b01000000, 0b00011001, false}, 17 | {0b00010001, 0b00011001, true}, 18 | {0b00011001, 0b00011001, true}, 19 | } { 20 | got := tc.a.isSubset(tc.b) 21 | if got != tc.want { 22 | t.Errorf("flagSubset(0b%b, 0b%b) = %v, want %v", tc.a, tc.b, got, tc.want) 23 | } 24 | } 25 | } 26 | 27 | func TestPrettyPrint(t *testing.T) { 28 | for _, tc := range []struct { 29 | a AccessFSSet 30 | want string 31 | }{ 32 | {a: 0, want: "∅"}, 33 | {a: 0b1111111111111, want: "{execute,write_file,read_file,read_dir,remove_dir,remove_file,make_char,make_dir,make_reg,make_sock,make_fifo,make_block,make_sym}"}, 34 | {a: 0b1111100000000, want: "{make_reg,make_sock,make_fifo,make_block,make_sym}"}, 35 | {a: 0b0000011111111, want: "{execute,write_file,read_file,read_dir,remove_dir,remove_file,make_char,make_dir}"}, 36 | {a: ll.AccessFSExecute, want: "{execute}"}, 37 | {a: ll.AccessFSWriteFile, want: "{write_file}"}, 38 | {a: ll.AccessFSReadFile, want: "{read_file}"}, 39 | {a: ll.AccessFSReadDir, want: "{read_dir}"}, 40 | {a: ll.AccessFSRemoveDir, want: "{remove_dir}"}, 41 | {a: ll.AccessFSRemoveFile, want: "{remove_file}"}, 42 | {a: ll.AccessFSMakeChar, want: "{make_char}"}, 43 | {a: ll.AccessFSMakeDir, want: "{make_dir}"}, 44 | {a: ll.AccessFSMakeReg, want: "{make_reg}"}, 45 | {a: ll.AccessFSMakeSock, want: "{make_sock}"}, 46 | {a: ll.AccessFSMakeFifo, want: "{make_fifo}"}, 47 | {a: ll.AccessFSMakeBlock, want: "{make_block}"}, 48 | {a: ll.AccessFSMakeSym, want: "{make_sym}"}, 49 | {a: ll.AccessFSRefer, want: "{refer}"}, 50 | {a: ll.AccessFSTruncate, want: "{truncate}"}, 51 | {a: ll.AccessFSReadFile | 1<<63, want: "{read_file,1<<63}"}, 52 | } { 53 | got := tc.a.String() 54 | if got != tc.want { 55 | t.Errorf("AccessFSSet(%08x).String() = %q, want %q", uint64(tc.a), got, tc.want) 56 | } 57 | } 58 | } 59 | 60 | func TestValid(t *testing.T) { 61 | for _, a := range []AccessFSSet{ 62 | ll.AccessFSExecute, ll.AccessFSMakeDir, ll.AccessFSMakeSym, ll.AccessFSRefer, 63 | } { 64 | gotIsValid := a.valid() 65 | if !gotIsValid { 66 | t.Errorf("%v.valid() = false, want true", a) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /landlock/accessnet.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | // AccessNetSet is a set of Landlockable network access rights. 4 | type AccessNetSet uint64 5 | 6 | var accessNetNames = []string{ 7 | "bind_tcp", 8 | "connect_tcp", 9 | } 10 | 11 | var supportedAccessNet = AccessNetSet((1 << len(accessNetNames)) - 1) 12 | 13 | func (a AccessNetSet) String() string { 14 | return accessSetString(uint64(a), accessNetNames) 15 | } 16 | 17 | func (a AccessNetSet) isSubset(b AccessNetSet) bool { 18 | return a&b == a 19 | } 20 | 21 | func (a AccessNetSet) intersect(b AccessNetSet) AccessNetSet { 22 | return a & b 23 | } 24 | 25 | func (a AccessNetSet) isEmpty() bool { 26 | return a == 0 27 | } 28 | 29 | func (a AccessNetSet) valid() bool { 30 | return a.isSubset(supportedAccessNet) 31 | } 32 | -------------------------------------------------------------------------------- /landlock/config.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 8 | ) 9 | 10 | // Access permission sets for filesystem access. 11 | const ( 12 | // The set of access rights that only apply to files. 13 | accessFile AccessFSSet = ll.AccessFSExecute | ll.AccessFSWriteFile | ll.AccessFSTruncate | ll.AccessFSReadFile 14 | 15 | // The set of access rights associated with read access to files and directories. 16 | accessFSRead AccessFSSet = ll.AccessFSExecute | ll.AccessFSReadFile | ll.AccessFSReadDir 17 | 18 | // The set of access rights associated with write access to files and directories. 19 | accessFSWrite AccessFSSet = ll.AccessFSWriteFile | ll.AccessFSRemoveDir | ll.AccessFSRemoveFile | ll.AccessFSMakeChar | ll.AccessFSMakeDir | ll.AccessFSMakeReg | ll.AccessFSMakeSock | ll.AccessFSMakeFifo | ll.AccessFSMakeBlock | ll.AccessFSMakeSym | ll.AccessFSTruncate 20 | 21 | // The set of access rights associated with read and write access to files and directories. 22 | accessFSReadWrite AccessFSSet = accessFSRead | accessFSWrite 23 | ) 24 | 25 | // These are Landlock configurations for the currently supported 26 | // Landlock ABI versions, configured to restrict the highest possible 27 | // set of operations possible for each version. 28 | // 29 | // The higher the ABI version, the more operations Landlock will be 30 | // able to restrict. 31 | var ( 32 | // Landlock V1 support (basic file operations). 33 | V1 = abiInfos[1].asConfig() 34 | // Landlock V2 support (V1 + file reparenting between different directories) 35 | V2 = abiInfos[2].asConfig() 36 | // Landlock V3 support (V2 + file truncation) 37 | V3 = abiInfos[3].asConfig() 38 | // Landlock V4 support (V3 + networking) 39 | V4 = abiInfos[4].asConfig() 40 | // Landlock V5 support (V4 + ioctl on device files) 41 | V5 = abiInfos[5].asConfig() 42 | ) 43 | 44 | // v0 denotes "no Landlock support". Only used internally. 45 | var v0 = Config{} 46 | 47 | // The Landlock configuration describes the desired set of 48 | // landlockable operations to be restricted and the constraints on it 49 | // (e.g. best effort mode). 50 | type Config struct { 51 | handledAccessFS AccessFSSet 52 | handledAccessNet AccessNetSet 53 | bestEffort bool 54 | } 55 | 56 | // NewConfig creates a new Landlock configuration with the given parameters. 57 | // 58 | // Passing an AccessFSSet will set that as the set of file system 59 | // operations to restrict when enabling Landlock. The AccessFSSet 60 | // needs to stay within the bounds of what go-landlock supports. 61 | // (If you are getting an error, you might need to upgrade to a newer 62 | // version of go-landlock.) 63 | func NewConfig(args ...interface{}) (*Config, error) { 64 | // Implementation note: This factory is written with future 65 | // extensibility in mind. Only specific types are supported as 66 | // input, but in the future more might be added. 67 | // 68 | // This constructor ensures that callers can't construct 69 | // invalid Config values. 70 | var c Config 71 | for _, arg := range args { 72 | switch arg := arg.(type) { 73 | case AccessFSSet: 74 | if !c.handledAccessFS.isEmpty() { 75 | return nil, errors.New("only one AccessFSSet may be provided") 76 | } 77 | if !arg.valid() { 78 | return nil, errors.New("unsupported AccessFSSet value; upgrade go-landlock?") 79 | } 80 | c.handledAccessFS = arg 81 | case AccessNetSet: 82 | if !c.handledAccessNet.isEmpty() { 83 | return nil, errors.New("only one AccessNetSet may be provided") 84 | } 85 | if !arg.valid() { 86 | return nil, errors.New("unsupported AccessNetSet value; upgrade go-landlock?") 87 | } 88 | c.handledAccessNet = arg 89 | default: 90 | return nil, fmt.Errorf("unknown argument %v; only AccessFSSet-type argument is supported", arg) 91 | } 92 | } 93 | return &c, nil 94 | } 95 | 96 | // MustConfig is like NewConfig but panics on error. 97 | func MustConfig(args ...interface{}) Config { 98 | c, err := NewConfig(args...) 99 | if err != nil { 100 | panic(err) 101 | } 102 | return *c 103 | } 104 | 105 | // String builds a human-readable representation of the Config. 106 | func (c Config) String() string { 107 | abi := abiInfo{version: -1} // invalid 108 | for i := len(abiInfos) - 1; i >= 0; i-- { 109 | a := abiInfos[i] 110 | if c.compatibleWithABI(a) { 111 | abi = a 112 | } 113 | } 114 | 115 | var fsDesc = c.handledAccessFS.String() 116 | if abi.supportedAccessFS == c.handledAccessFS && c.handledAccessFS != 0 { 117 | fsDesc = "all" 118 | } 119 | 120 | var netDesc = c.handledAccessNet.String() 121 | if abi.supportedAccessNet == c.handledAccessNet && c.handledAccessNet != 0 { 122 | fsDesc = "all" 123 | } 124 | 125 | var bestEffort = "" 126 | if c.bestEffort { 127 | bestEffort = " (best effort)" 128 | } 129 | 130 | var version string 131 | if abi.version < 0 { 132 | version = "V???" 133 | } else { 134 | version = fmt.Sprintf("V%v", abi.version) 135 | } 136 | 137 | return fmt.Sprintf("{Landlock %v; FS: %v; Net: %v%v}", version, fsDesc, netDesc, bestEffort) 138 | } 139 | 140 | // BestEffort returns a config that will opportunistically enforce 141 | // the strongest rules it can, up to the given ABI version, working 142 | // with the level of Landlock support available in the running kernel. 143 | // 144 | // Warning: A best-effort call to RestrictPaths() will succeed without 145 | // error even when Landlock is not available at all on the current kernel. 146 | func (c Config) BestEffort() Config { 147 | cfg := c 148 | cfg.bestEffort = true 149 | return cfg 150 | } 151 | 152 | // RestrictPaths restricts all goroutines to only "see" the files 153 | // provided as inputs. After this call successfully returns, the 154 | // goroutines will only be able to use files in the ways as they were 155 | // specified in advance in the call to RestrictPaths. 156 | // 157 | // Example: The following invocation will restrict all goroutines so 158 | // that it can only read from /usr, /bin and /tmp, and only write to 159 | // /tmp: 160 | // 161 | // err := landlock.V3.RestrictPaths( 162 | // landlock.RODirs("/usr", "/bin"), 163 | // landlock.RWDirs("/tmp"), 164 | // ) 165 | // if err != nil { 166 | // log.Fatalf("landlock.V3.RestrictPaths(): %v", err) 167 | // } 168 | // 169 | // RestrictPaths returns an error if any of the given paths does not 170 | // denote an actual directory or file, or if Landlock can't be enforced 171 | // using the desired ABI version constraints. 172 | // 173 | // RestrictPaths also sets the "no new privileges" flag for all OS 174 | // threads managed by the Go runtime. 175 | // 176 | // # Restrictable access rights 177 | // 178 | // The notions of what "reading" and "writing" mean are limited by what 179 | // the selected Landlock version supports. 180 | // 181 | // Calling RestrictPaths with a given Landlock ABI version will 182 | // inhibit all future calls to the access rights supported by this ABI 183 | // version, unless the accessed path is in a file hierarchy that is 184 | // specifically allow-listed for a specific set of access rights. 185 | // 186 | // The overall set of operations that RestrictPaths can restrict are: 187 | // 188 | // For reading: 189 | // 190 | // - Executing a file (V1+) 191 | // - Opening a file with read access (V1+) 192 | // - Opening a directory or listing its content (V1+) 193 | // 194 | // For writing: 195 | // 196 | // - Opening a file with write access (V1+) 197 | // - Truncating file contents (V3+) 198 | // 199 | // For directory manipulation: 200 | // 201 | // - Removing an empty directory or renaming one (V1+) 202 | // - Removing (or renaming) a file (V1+) 203 | // - Creating (or renaming or linking) a character device (V1+) 204 | // - Creating (or renaming) a directory (V1+) 205 | // - Creating (or renaming or linking) a regular file (V1+) 206 | // - Creating (or renaming or linking) a UNIX domain socket (V1+) 207 | // - Creating (or renaming or linking) a named pipe (V1+) 208 | // - Creating (or renaming or linking) a block device (V1+) 209 | // - Creating (or renaming or linking) a symbolic link (V1+) 210 | // - Renaming or linking a file between directories (V2+) 211 | // 212 | // Future versions of Landlock will be able to inhibit more operations. 213 | // Quoting the Landlock documentation: 214 | // 215 | // It is currently not possible to restrict some file-related 216 | // actions accessible through these syscall families: chdir(2), 217 | // stat(2), flock(2), chmod(2), chown(2), setxattr(2), utime(2), 218 | // ioctl(2), fcntl(2), access(2). Future Landlock evolutions will 219 | // enable to restrict them. 220 | // 221 | // The access rights are documented in more depth in the 222 | // [Kernel Documentation about Access Rights]. 223 | // 224 | // # Helper functions for selecting access rights 225 | // 226 | // These helper functions help selecting common subsets of access rights: 227 | // 228 | // - [RODirs] selects access rights in the group "for reading". 229 | // In V1, this means reading files, listing directories and executing files. 230 | // - [RWDirs] selects access rights in the group "for reading", "for writing" and 231 | // "for directory manipulation". This grants the full set of access rights which are 232 | // available within the configuration. 233 | // - [ROFiles] is like [RODirs], but does not select directory-specific access rights. 234 | // In V1, this means reading and executing files. 235 | // - [RWFiles] is like [RWDirs], but does not select directory-specific access rights. 236 | // In V1, this means reading, writing and executing files. 237 | // 238 | // The [PathAccess] rule lets callers define custom subsets of these 239 | // access rights. AccessFSSets permitted using [PathAccess] must be a 240 | // subset of the [AccessFSSet] that the Config restricts. 241 | // 242 | // [Kernel Documentation about Access Rights]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights 243 | func (c Config) RestrictPaths(rules ...Rule) error { 244 | c.handledAccessNet = 0 // clear out everything but file system access 245 | return restrict(c, rules...) 246 | } 247 | 248 | // RestrictNet restricts network access in goroutines. 249 | // 250 | // Using Landlock V4, this function will disallow the use of bind(2) 251 | // and connect(2) for TCP ports, unless those TCP ports are 252 | // specifically permitted using these rules: 253 | // 254 | // - [ConnectTCP] permits connect(2) operations to a given TCP port. 255 | // - [BindTCP] permits bind(2) operations on a given TCP port. 256 | // 257 | // These network access rights are documented in more depth in the 258 | // [Kernel Documentation about Network flags]. 259 | // 260 | // [Kernel Documentation about Network flags]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#network-flags 261 | func (c Config) RestrictNet(rules ...Rule) error { 262 | c.handledAccessFS = 0 // clear out everything but network access 263 | return restrict(c, rules...) 264 | } 265 | 266 | // Restrict restricts all types of access which is restrictable with the Config. 267 | // 268 | // Using Landlock V4, this is equivalent to calling both 269 | // [RestrictPaths] and [RestrictNet] with the subset of arguments that 270 | // apply to it. 271 | // 272 | // In future Landlock versions, this function might restrict 273 | // additional kinds of operations outside of file system access and 274 | // networking, provided that the [Config] specifies these. 275 | func (c Config) Restrict(rules ...Rule) error { 276 | return restrict(c, rules...) 277 | } 278 | 279 | // PathOpt is a deprecated alias for [Rule]. 280 | // 281 | // Deprecated: This alias is only kept around for backwards 282 | // compatibility and will disappear with the next major release. 283 | type PathOpt = Rule 284 | 285 | // compatibleWith is true if c is compatible to work at the given Landlock ABI level. 286 | func (c Config) compatibleWithABI(abi abiInfo) bool { 287 | return (c.handledAccessFS.isSubset(abi.supportedAccessFS) && 288 | c.handledAccessNet.isSubset(abi.supportedAccessNet)) 289 | } 290 | 291 | // restrictTo returns a config that is a subset of c and which is compatible with the given ABI. 292 | func (c Config) restrictTo(abi abiInfo) Config { 293 | return Config{ 294 | handledAccessFS: c.handledAccessFS.intersect(abi.supportedAccessFS), 295 | handledAccessNet: c.handledAccessNet.intersect(abi.supportedAccessNet), 296 | bestEffort: true, 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /landlock/config_test.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "testing" 5 | 6 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 7 | ) 8 | 9 | func TestConfigString(t *testing.T) { 10 | for _, tc := range []struct { 11 | cfg Config 12 | want string 13 | }{ 14 | { 15 | cfg: Config{handledAccessFS: 0, handledAccessNet: 0}, 16 | want: "{Landlock V0; FS: ∅; Net: ∅}", 17 | }, 18 | { 19 | cfg: Config{handledAccessFS: ll.AccessFSWriteFile}, 20 | want: "{Landlock V1; FS: {write_file}; Net: ∅}", 21 | }, 22 | { 23 | cfg: Config{handledAccessNet: ll.AccessNetBindTCP}, 24 | want: "{Landlock V4; FS: ∅; Net: {bind_tcp}}", 25 | }, 26 | { 27 | cfg: V1, 28 | want: "{Landlock V1; FS: all; Net: ∅}", 29 | }, 30 | { 31 | cfg: V1.BestEffort(), 32 | want: "{Landlock V1; FS: all; Net: ∅ (best effort)}", 33 | }, 34 | { 35 | cfg: Config{handledAccessFS: 1 << 63}, 36 | want: "{Landlock V???; FS: {1<<63}; Net: ∅}", 37 | }, 38 | } { 39 | got := tc.cfg.String() 40 | if got != tc.want { 41 | t.Errorf("cfg.String() = %q, want %q", got, tc.want) 42 | } 43 | } 44 | } 45 | 46 | func TestNewConfig(t *testing.T) { 47 | for _, a := range []AccessFSSet{ 48 | ll.AccessFSWriteFile, ll.AccessFSRefer, 49 | } { 50 | c, err := NewConfig(a) 51 | if err != nil { 52 | t.Errorf("NewConfig(): expected success, got %v", err) 53 | } 54 | want := a 55 | if c.handledAccessFS != want { 56 | t.Errorf("c.handledAccessFS = %v, want %v", c.handledAccessFS, want) 57 | } 58 | } 59 | } 60 | 61 | func TestNewConfigEmpty(t *testing.T) { 62 | // Constructing an empty config is a bit pointless, but should work. 63 | c, err := NewConfig() 64 | if err != nil { 65 | t.Errorf("NewConfig(): expected success, got %v", err) 66 | } 67 | want := AccessFSSet(0) 68 | if c.handledAccessFS != want { 69 | t.Errorf("c.handledAccessFS = %v, want %v", c.handledAccessFS, want) 70 | } 71 | } 72 | 73 | func TestNewConfigFailures(t *testing.T) { 74 | for _, args := range [][]interface{}{ 75 | {ll.AccessFSWriteFile}, 76 | {123}, 77 | {"a string"}, 78 | {"foo", 42}, 79 | // May not specify two AccessFSSets 80 | {AccessFSSet(ll.AccessFSWriteFile), AccessFSSet(ll.AccessFSReadFile)}, 81 | // May not specify an unsupported AccessFSSet value 82 | {AccessFSSet(1 << 16)}, 83 | {AccessFSSet(1 << 63)}, 84 | } { 85 | _, err := NewConfig(args...) 86 | if err == nil { 87 | t.Errorf("NewConfig(%v) success, expected error", args) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /landlock/landlock.go: -------------------------------------------------------------------------------- 1 | // Package landlock restricts a Go program's ability to use files and networking. 2 | // 3 | // # Restricting file access 4 | // 5 | // The following invocation will restrict all goroutines so that they 6 | // can only read from /usr, /bin and /tmp, and only write to /tmp: 7 | // 8 | // err := landlock.V5.BestEffort().RestrictPaths( 9 | // landlock.RODirs("/usr", "/bin"), 10 | // landlock.RWDirs("/tmp"), 11 | // ) 12 | // 13 | // This will restrict file access using Landlock V5, if available. If 14 | // unavailable, it will attempt using earlier Landlock versions than 15 | // the one requested. If no Landlock version is available, it will 16 | // still succeed, without restricting file accesses. 17 | // 18 | // # Restricting networking 19 | // 20 | // The following invocation will restrict all goroutines so that they 21 | // can only bind to TCP port 8080 and only connect to TCP port 53: 22 | // 23 | // err := landlock.V5.BestEffort().RestrictNet( 24 | // landlock.BindTCP(8080), 25 | // landlock.ConnectTCP(53), 26 | // ) 27 | // 28 | // This functionality is available since Landlock V5. 29 | // 30 | // # Restricting file access and networking at once 31 | // 32 | // The following invocation restricts both file and network access at 33 | // once. The effect is the same as calling [Config.RestrictPaths] and 34 | // [Config.RestrictNet] one after another, but it happens in one step. 35 | // 36 | // err := landlock.V5.BestEffort().Restrict( 37 | // landlock.RODirs("/usr", "/bin"), 38 | // landlock.RWDirs("/tmp"), 39 | // landlock.BindTCP(8080), 40 | // landlock.ConnectTCP(53), 41 | // ) 42 | // 43 | // # More possible invocations 44 | // 45 | // landlock.V5.RestrictPaths(...) (without the call to 46 | // [Config.BestEffort]) enforces the given rules using the 47 | // capabilities of Landlock V5, but returns an error if that 48 | // functionality is not available on the system that the program is 49 | // running on. 50 | // 51 | // # Landlock ABI versioning 52 | // 53 | // The Landlock ABI is versioned, so that callers can probe for the 54 | // availability of different Landlock features. 55 | // 56 | // When using the Go Landlock package, callers need to identify at 57 | // which ABI level they want to use Landlock and call one of the 58 | // restriction methods (e.g. [Config.RestrictPaths]) on the 59 | // corresponding ABI constant. 60 | // 61 | // When new Landlock versions become available in landlock, users will 62 | // manually need to upgrade their usages to higher Landlock versions, 63 | // as there is a risk that new Landlock versions will break operations 64 | // that their programs rely on. 65 | // 66 | // # Graceful degradation on older kernels 67 | // 68 | // Programs that get run on different kernel versions will want to use 69 | // the [Config.BestEffort] method to gracefully degrade to using the 70 | // best available Landlock version on the current kernel. 71 | // 72 | // In this case, the Go Landlock library will enforce as much as 73 | // possible, but it will ensure that all the requested access rights 74 | // are permitted after Landlock enforcement. 75 | // 76 | // # Current limitations 77 | // 78 | // Landlock can not currently restrict all file system operations. 79 | // The operations that can and can not be restricted yet are listed in 80 | // the [Kernel Documentation about Access Rights]. 81 | // 82 | // Enabling Landlock implicitly turns off the following file system 83 | // features: 84 | // 85 | // - File reparenting: renaming or linking a file to a different parent directory is denied, 86 | // unless it is explicitly enabled on both directories with the "Refer" access modifier, 87 | // and the new target directory does not grant the file additional rights through its 88 | // Landlock access rules. 89 | // - Filesystem topology modification: arbitrary mounts are always denied. 90 | // 91 | // These are Landlock limitations that will be resolved in future 92 | // versions. See the [Kernel Documentation about Current Limitations] 93 | // for more details. 94 | // 95 | // # Multithreading Limitations 96 | // 97 | // This warning only applies to programs using cgo and linking C 98 | // libraries that start OS threads through means other than 99 | // pthread_create() before landlock is called: 100 | // 101 | // When using cgo, the landlock package relies on libpsx in order to 102 | // apply the rules across all OS threads, (rather than just the ones 103 | // managed by the Go runtime). psx achieves this by wrapping the 104 | // C-level phtread_create() API which is very commonly used on Unix to 105 | // start threads. However, C libraries calling clone(2) through other 106 | // means before landlock is called might still create threads that 107 | // won't have Landlock protections. 108 | // 109 | // [Kernel Documentation about Access Rights]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights 110 | // [Kernel Documentation about Current Limitations]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#current-limitations 111 | package landlock 112 | -------------------------------------------------------------------------------- /landlock/lltest/lltest.go: -------------------------------------------------------------------------------- 1 | // Package lltest has helpers for Landlock-enabled tests. 2 | package lltest 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | 13 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 14 | ) 15 | 16 | // isRunningInSubprocess indicates whether we are currently running in a subprocess context. 17 | var isRunningInSubprocess = false 18 | 19 | // RunInSubprocess runs the given test function in a subprocess 20 | // and forwards its output. 21 | func RunInSubprocess(t *testing.T, f func()) { 22 | t.Helper() 23 | 24 | if os.Getenv("IS_SUBPROCESS") != "" { 25 | isRunningInSubprocess = true 26 | f() 27 | return 28 | } 29 | 30 | args := append(os.Args[1:], "-test.run="+regexp.QuoteMeta(t.Name())+"$") 31 | 32 | // Make sure that the parent process cleans up the actual TempDir. 33 | // If the child process uses t.TempDir(), it'll create it in $TMPDIR. 34 | t.Setenv("TMPDIR", t.TempDir()) 35 | 36 | t.Setenv("IS_SUBPROCESS", "yes") 37 | buf, err := exec.Command(os.Args[0], args...).Output() 38 | 39 | var exitErr *exec.ExitError 40 | if err != nil && !errors.As(err, &exitErr) { 41 | t.Fatalf("Could not execute test in subprocess: %v", err) 42 | } 43 | 44 | lines := strings.Split(string(buf), "\n") 45 | for _, l := range lines { 46 | if l == "FAIL" { 47 | defer func() { t.Error("Test failed in subprocess") }() 48 | continue 49 | } 50 | if strings.HasPrefix(l, "--- SKIP") { 51 | defer func() { t.Skip("Test skipped in subprocess") }() 52 | continue 53 | } 54 | if strings.HasPrefix(l, "===") || strings.HasPrefix(l, "---") || l == "PASS" || l == "" { 55 | continue 56 | } 57 | fmt.Println(l) 58 | } 59 | } 60 | 61 | // TempDir is a replacement for t.TempDir() to be used in Landlock tests. 62 | // If we were using t.TempDir(), the test framework would try to remove it 63 | // after the test, even in Landlocked subprocess tests where this fails. 64 | // 65 | // TODO: It would be nicer if all tests could just use t.TempDir() 66 | // without the test framework trying to delete these later in the subprocesses. 67 | func TempDir(t testing.TB) string { 68 | t.Helper() 69 | 70 | if isRunningInSubprocess { 71 | dir, err := os.MkdirTemp("", "LandlockTestTempDir") 72 | if err != nil { 73 | t.Fatalf("os.MkdirTemp: %v", err) 74 | } 75 | return dir 76 | } 77 | return t.TempDir() 78 | } 79 | 80 | // RequireABI skips the test if the kernel does not provide the given ABI version. 81 | func RequireABI(t testing.TB, want int) { 82 | t.Helper() 83 | 84 | if v, err := ll.LandlockGetABIVersion(); err != nil || v < want { 85 | t.Skipf("Requires Landlock >= V%v, got V%v (err=%v)", want, v, err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /landlock/net_opt.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "fmt" 5 | 6 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 7 | ) 8 | 9 | type NetRule struct { 10 | access AccessNetSet 11 | port uint16 12 | } 13 | 14 | // ConnectTCP is a [Rule] which grants the right to connect a socket 15 | // to a given TCP port. 16 | // 17 | // In Go, the connect(2) operation is usually run as part of 18 | // net.Dial(). 19 | func ConnectTCP(port uint16) NetRule { 20 | return NetRule{ 21 | access: ll.AccessNetConnectTCP, 22 | port: port, 23 | } 24 | } 25 | 26 | // BindTCP is a [Rule] which grants the right to bind a socket to a 27 | // given TCP port. 28 | // 29 | // In Go, the bind(2) operation is usually run as part of 30 | // net.Listen(). 31 | func BindTCP(port uint16) NetRule { 32 | return NetRule{ 33 | access: ll.AccessNetBindTCP, 34 | port: port, 35 | } 36 | } 37 | 38 | func (n NetRule) String() string { 39 | return fmt.Sprintf("ALLOW %v on TCP port %v", n.access, n.port) 40 | } 41 | 42 | func (n NetRule) compatibleWithConfig(c Config) bool { 43 | return n.access.isSubset(c.handledAccessNet) 44 | } 45 | 46 | func (n NetRule) addToRuleset(rulesetFD int, c Config) error { 47 | if n.access == 0 { 48 | // Adding this to the ruleset would be a no-op 49 | // and result in an error. 50 | return nil 51 | } 52 | flags := 0 53 | attr := &ll.NetPortAttr{ 54 | AllowedAccess: uint64(n.access), 55 | Port: uint64(n.port), 56 | } 57 | return ll.LandlockAddNetPortRule(rulesetFD, attr, flags) 58 | } 59 | 60 | func (n NetRule) downgrade(c Config) (out Rule, ok bool) { 61 | return NetRule{ 62 | access: n.access.intersect(c.handledAccessNet), 63 | port: n.port, 64 | }, true 65 | } 66 | -------------------------------------------------------------------------------- /landlock/opt.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | // Rule represents one or more Landlock rules which can be added to a 4 | // Landlock ruleset. 5 | type Rule interface { 6 | // compatibleWithConfig is true if the given rule is 7 | // compatible with the configuration c. 8 | compatibleWithConfig(c Config) bool 9 | 10 | // downgrade returns a downgraded rule for "best effort" mode, 11 | // under the assumption that the kernel only supports c. 12 | // 13 | // It establishes that: 14 | // 15 | // - rule.accessFS ⊆ handledAccessFS for FSRules 16 | // - rule.accessNet ⊆ handledAccessNet for NetRules 17 | // 18 | // If the rule is unsupportable under the given Config at 19 | // all, ok is false. This happens when c represents a Landlock 20 | // V1 system but the rule wants to grant the refer right on 21 | // a path. "Refer" operations are always forbidden under 22 | // Landlock V1. 23 | downgrade(c Config) (out Rule, ok bool) 24 | 25 | // addToRuleset applies the rule to the given rulesetFD. 26 | // 27 | // This may return errors such as "file not found" depending 28 | // on the rule type. 29 | addToRuleset(rulesetFD int, c Config) error 30 | } 31 | -------------------------------------------------------------------------------- /landlock/path_opt.go: -------------------------------------------------------------------------------- 1 | package landlock 2 | 3 | import ( 4 | "fmt" 5 | 6 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 7 | ) 8 | 9 | // FSRule is a Rule which permits access to file system paths. 10 | type FSRule struct { 11 | accessFS AccessFSSet 12 | paths []string 13 | enforceSubset bool // enforce that accessFS is a subset of cfg.handledAccessFS 14 | ignoreMissing bool // ignore missing paths 15 | } 16 | 17 | // withRights adds the given access rights to the rights enforced in the FSRule 18 | // and returns the result as a new FSRule. 19 | func (r FSRule) withRights(a AccessFSSet) FSRule { 20 | r.accessFS = r.accessFS.union(a) 21 | return r 22 | } 23 | 24 | // intersectRights intersects the given access rights with the rights 25 | // enforced in the FSRule and returns the result as a new FSRule. 26 | func (r FSRule) intersectRights(a AccessFSSet) FSRule { 27 | r.accessFS = r.accessFS.intersect(a) 28 | return r 29 | } 30 | 31 | // WithRefer adds the "refer" access right to a FSRule. 32 | // 33 | // Notably, asking for the "refer" access right does not work on 34 | // kernels below 5.19. In best effort mode, this will fall back to not 35 | // using Landlock enforcement at all on these kernel versions. If you 36 | // want to use Landlock on these kernels, do not use the "refer" 37 | // access right. 38 | func (r FSRule) WithRefer() FSRule { 39 | return r.withRights(ll.AccessFSRefer) 40 | } 41 | 42 | // WithIoctlDev adds the "ioctl dev" access right to a FSRule. 43 | // 44 | // It is uncommon to need this access right, so it is not part of 45 | // [RWFiles] or [RWDirs]. 46 | func (r FSRule) WithIoctlDev() FSRule { 47 | return r.withRights(ll.AccessFSIoctlDev) 48 | } 49 | 50 | // IgnoreIfMissing gracefully ignores missing paths. 51 | // 52 | // Under normal circumstances, referring to a non-existing path in a rule would 53 | // lead to a runtime error. When the rule uses the IgnoreIfMissing modifier, 54 | // these runtime errors are ignored. This can be useful e.g. for optional 55 | // configuration paths, which are only ever read by a program. 56 | func (r FSRule) IgnoreIfMissing() FSRule { 57 | r.ignoreMissing = true 58 | return r 59 | } 60 | 61 | func (r FSRule) String() string { 62 | return fmt.Sprintf("REQUIRE %v for paths %v", r.accessFS, r.paths) 63 | } 64 | 65 | // compatibleWithConfig returns true if the given rule is compatible 66 | // for use with the config c. 67 | func (r FSRule) compatibleWithConfig(c Config) bool { 68 | a := r.accessFS 69 | if !r.enforceSubset { 70 | // If !enforceSubset, this FSRule is potentially overspecifying flags, 71 | // so we should not check the subset property. We make an exception 72 | // for the "refer" flag, which should still get checked though. 73 | a = a.intersect(ll.AccessFSRefer) 74 | } 75 | return a.isSubset(c.handledAccessFS) 76 | } 77 | 78 | // downgrade calculates the actual ruleset to be enforced given the 79 | // current config (and assuming that the config is going to work under 80 | // the running kernel). 81 | // 82 | // It establishes that rule.accessFS ⊆ c.handledAccessFS. 83 | // 84 | // If ok is false, downgrade is impossible and we need to fall back to doing nothing. 85 | func (r FSRule) downgrade(c Config) (out Rule, ok bool) { 86 | // In case that "refer" is requested on a path, we 87 | // require Landlock V2+, or we have to downgrade to V0. 88 | // You can't get the refer capability with V1, but linking/ 89 | // renaming files is always implicitly restricted. 90 | if hasRefer(r.accessFS) && !hasRefer(c.handledAccessFS) { 91 | return FSRule{}, false 92 | } 93 | return r.intersectRights(c.handledAccessFS), true 94 | } 95 | 96 | func hasRefer(a AccessFSSet) bool { 97 | return a&ll.AccessFSRefer != 0 98 | } 99 | 100 | // PathAccess is a [Rule] which grants the access rights specified by 101 | // accessFS to the file hierarchies under the given paths. 102 | // 103 | // When accessFS is larger than what is permitted by the Landlock 104 | // version in use, only the applicable subset of accessFS will be used. 105 | // 106 | // Most users should use the functions [RODirs], [RWDirs], [ROFiles] 107 | // and [RWFiles] instead, which provide canned rules for commonly 108 | // used values of accessFS. 109 | // 110 | // Filesystem access rights are represented using bits in a uint64. 111 | // The individual access rights and their meaning are defined in the 112 | // landlock/syscall package and explained further in the 113 | // [Kernel Documentation about Access Rights]. 114 | // 115 | // accessFS must be a subset of the permissions that the Config 116 | // restricts. 117 | // 118 | // [Kernel Documentation about Access Rights]: https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights 119 | func PathAccess(accessFS AccessFSSet, paths ...string) FSRule { 120 | return FSRule{ 121 | accessFS: accessFS, 122 | paths: paths, 123 | enforceSubset: true, 124 | } 125 | } 126 | 127 | // RODirs is a [Rule] which grants common read-only access to files 128 | // and directories and permits executing files. 129 | func RODirs(paths ...string) FSRule { 130 | return FSRule{ 131 | accessFS: accessFSRead, 132 | paths: paths, 133 | enforceSubset: false, 134 | } 135 | } 136 | 137 | // RWDirs is a [Rule] which grants full (read and write) access to 138 | // files and directories under the given paths. 139 | // 140 | // Noteworthy operations which are *not* covered by RWDirs: 141 | // 142 | // - RWDirs does *not* grant the right to *reparent or link* files 143 | // across different directories. If this access right is 144 | // required, use [FSRule.WithRefer]. 145 | // 146 | // - RWDirs does *not* grant the right to *use IOCTL* on device 147 | // files. If this access right is required, use 148 | // [FSRule.WithIoctlDev]. 149 | func RWDirs(paths ...string) FSRule { 150 | return FSRule{ 151 | accessFS: accessFSReadWrite, 152 | paths: paths, 153 | enforceSubset: false, 154 | } 155 | } 156 | 157 | // ROFiles is a [Rule] which grants common read access to individual 158 | // files, but not to directories, for the file hierarchies under the 159 | // given paths. 160 | func ROFiles(paths ...string) FSRule { 161 | return FSRule{ 162 | accessFS: accessFSRead & accessFile, 163 | paths: paths, 164 | enforceSubset: false, 165 | } 166 | } 167 | 168 | // RWFiles is a [Rule] which grants common read and write access to 169 | // files under the given paths, but it does not permit access to 170 | // directories. 171 | // 172 | // Noteworthy operations which are *not* covered by RWFiles: 173 | // 174 | // - RWFiles does *not* grant the right to *use IOCTL* on device 175 | // files. If this access right is required, use 176 | // [FSRule.WithIoctlDev]. 177 | func RWFiles(paths ...string) FSRule { 178 | return FSRule{ 179 | accessFS: accessFSReadWrite & accessFile, 180 | paths: paths, 181 | enforceSubset: false, 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /landlock/path_opt_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "syscall" 9 | 10 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | func (r FSRule) addToRuleset(rulesetFD int, c Config) error { 15 | effectiveAccessFS := r.accessFS 16 | if !r.enforceSubset { 17 | effectiveAccessFS = effectiveAccessFS.intersect(c.handledAccessFS) 18 | } 19 | if effectiveAccessFS == 0 { 20 | // Adding this to the ruleset would be a no-op 21 | // and result in an error. 22 | return nil 23 | } 24 | for _, path := range r.paths { 25 | if err := addPath(rulesetFD, path, effectiveAccessFS); err != nil { 26 | if r.ignoreMissing && errors.Is(err, unix.ENOENT) { 27 | continue // Skip this path. 28 | } 29 | return fmt.Errorf("populating ruleset for %q with access %v: %w", path, effectiveAccessFS, err) 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func addPath(rulesetFd int, path string, access AccessFSSet) error { 36 | fd, err := syscall.Open(path, unix.O_PATH|unix.O_CLOEXEC, 0) 37 | if err != nil { 38 | return fmt.Errorf("open: %w", err) 39 | } 40 | defer syscall.Close(fd) 41 | 42 | pathBeneath := ll.PathBeneathAttr{ 43 | ParentFd: fd, 44 | AllowedAccess: uint64(access), 45 | } 46 | err = ll.LandlockAddPathBeneathRule(rulesetFd, &pathBeneath, 0) 47 | if err != nil { 48 | if errors.Is(err, syscall.EINVAL) { 49 | // The ruleset access permissions must be a superset of the ones we restrict to. 50 | // This should never happen because the call to addPath() ensures that. 51 | err = fmt.Errorf("inconsistent access rights (using directory access rights on a regular file?): %w", err) 52 | } else if errors.Is(err, syscall.ENOMSG) && access == 0 { 53 | err = fmt.Errorf("empty access rights: %w", err) 54 | } else { 55 | // Other errors should never happen. 56 | err = bug(err) 57 | } 58 | return fmt.Errorf("landlock_add_rule: %w", err) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /landlock/path_opt_nonlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package landlock 4 | 5 | import "errors" 6 | 7 | func (r FSRule) addToRuleset(rulesetFD int, c Config) error { 8 | return errors.New("Landlock is only supported on Linux") 9 | } 10 | -------------------------------------------------------------------------------- /landlock/restrict.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "syscall" 9 | 10 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // downgrade calculates the actual ruleset to be enforced given the 15 | // current kernel's Landlock ABI level. 16 | // 17 | // It establishes that rule.compatibleWithConfig(c) and c.compatibleWithABI(abi). 18 | func downgrade(c Config, rules []Rule, abi abiInfo) (Config, []Rule) { 19 | c = c.restrictTo(abi) 20 | 21 | resRules := make([]Rule, 0, len(rules)) 22 | for _, rule := range rules { 23 | rule, ok := rule.downgrade(c) 24 | if !ok { 25 | return v0, nil // Use "ABI V0" (do nothing) 26 | } 27 | resRules = append(resRules, rule) 28 | } 29 | return c, resRules 30 | } 31 | 32 | // restrict is the actual implementation which sets up Landlock. 33 | func restrict(c Config, rules ...Rule) error { 34 | // Check validity of rules early. 35 | for _, rule := range rules { 36 | if !rule.compatibleWithConfig(c) { 37 | return fmt.Errorf("incompatible rule %v: %w", rule, unix.EINVAL) 38 | } 39 | } 40 | 41 | abi := getSupportedABIVersion() 42 | if c.bestEffort { 43 | c, rules = downgrade(c, rules, abi) 44 | } 45 | if !c.compatibleWithABI(abi) { 46 | return fmt.Errorf("missing kernel Landlock support. Got Landlock ABI v%v, wanted %v", abi.version, c) 47 | } 48 | 49 | // TODO: This might be incorrect - the "refer" permission is 50 | // always implicit, even in Landlock V1. So enabling Landlock 51 | // on a Landlock V1 kernel without any handled access rights 52 | // will still forbid linking files between directories. 53 | if c.handledAccessFS.isEmpty() && c.handledAccessNet.isEmpty() { 54 | return nil // Success: Nothing to restrict. 55 | } 56 | 57 | rulesetAttr := ll.RulesetAttr{ 58 | HandledAccessFS: uint64(c.handledAccessFS), 59 | HandledAccessNet: uint64(c.handledAccessNet), 60 | } 61 | fd, err := ll.LandlockCreateRuleset(&rulesetAttr, 0) 62 | if err != nil { 63 | if errors.Is(err, syscall.ENOSYS) || errors.Is(err, syscall.EOPNOTSUPP) { 64 | err = errors.New("landlock is not supported by kernel or not enabled at boot time") 65 | } 66 | if errors.Is(err, syscall.EINVAL) { 67 | err = errors.New("unknown flags, unknown access, or too small size") 68 | } 69 | // Bug, because these should have been caught up front with the ABI version check. 70 | return bug(fmt.Errorf("landlock_create_ruleset: %w", err)) 71 | } 72 | defer syscall.Close(fd) 73 | 74 | for _, rule := range rules { 75 | if err := rule.addToRuleset(fd, c); err != nil { 76 | return err 77 | } 78 | } 79 | 80 | if err := ll.AllThreadsPrctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil { 81 | // This prctl invocation should always work. 82 | return bug(fmt.Errorf("prctl(PR_SET_NO_NEW_PRIVS): %v", err)) 83 | } 84 | 85 | if err := ll.AllThreadsLandlockRestrictSelf(fd, 0); err != nil { 86 | if errors.Is(err, syscall.E2BIG) { 87 | // Other errors than E2BIG should never happen. 88 | return fmt.Errorf("the maximum number of stacked rulesets is reached for the current thread: %w", err) 89 | } 90 | return bug(fmt.Errorf("landlock_restrict_self: %w", err)) 91 | } 92 | return nil 93 | } 94 | 95 | // Denotes an error that should not have happened. 96 | // If such an error occurs anyway, please try upgrading the library 97 | // and file a bug to github.com/landlock-lsm/go-landlock if the issue persists. 98 | func bug(err error) error { 99 | return fmt.Errorf("BUG(go-landlock): This should not have happened: %w", err) 100 | } 101 | -------------------------------------------------------------------------------- /landlock/restrict_config_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/landlock-lsm/go-landlock/landlock" 9 | "github.com/landlock-lsm/go-landlock/landlock/lltest" 10 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 11 | ) 12 | 13 | func TestCustomConfig(t *testing.T) { 14 | lltest.RunInSubprocess(t, func() { 15 | lltest.RequireABI(t, 1) 16 | 17 | pathRO := MakeSomeFile(t) 18 | pathNoAccess := MakeSomeFile(t) 19 | 20 | readFile := landlock.AccessFSSet(ll.AccessFSReadFile) 21 | if err := landlock.MustConfig(readFile).RestrictPaths( 22 | landlock.PathAccess(readFile, pathRO), 23 | ); err != nil { 24 | t.Fatalf("Could not restrict paths: %v", err) 25 | } 26 | 27 | if err := openForRead(pathRO); err != nil { 28 | t.Errorf("openForRead(%q): %v", pathRO, err) 29 | } 30 | if err := openForRead(pathNoAccess); err == nil { 31 | t.Errorf("openForRead(%q) successful, want error", pathNoAccess) 32 | } 33 | }) 34 | } 35 | 36 | func TestAbsurdDowngradeCase(t *testing.T) { 37 | // This is a regression test for a bug where: 38 | // 39 | // - we run on a kernel that supports Landlock but does not 40 | // support the truncate access right 41 | // - Go-Landlock will "downgrade" the file system rule to "no access rights", 42 | // because the requested access right "truncate" is not supported. 43 | // - It should not try to add that rule (but it used to). 44 | if v, err := ll.LandlockGetABIVersion(); err != nil || v <= 0 || v > 2 { 45 | t.Skipf("Requires Landlock version 1 or 2, got V%v (err=%v)", v, err) 46 | } 47 | 48 | lltest.RunInSubprocess(t, func() { 49 | cfg := landlock.MustConfig( 50 | landlock.AccessFSSet(ll.AccessFSTruncate | ll.AccessFSMakeDir), 51 | ).BestEffort() 52 | 53 | path := MakeSomeFile(t) 54 | err := cfg.Restrict(landlock.PathAccess(ll.AccessFSTruncate, path)) 55 | if err != nil { 56 | t.Errorf("Landlock restriction error: %v", err) 57 | } 58 | }) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /landlock/restrict_downgrade_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 10 | ) 11 | 12 | func TestDowngradeAccessFS(t *testing.T) { 13 | for _, tc := range []struct { 14 | Name string 15 | 16 | Handled AccessFSSet 17 | Requested AccessFSSet 18 | SupportedABI int 19 | 20 | WantHandled AccessFSSet 21 | WantRequested AccessFSSet 22 | 23 | WantFallbackToV0 bool 24 | }{ 25 | { 26 | Name: "RestrictHandledToSupported", 27 | SupportedABI: 1, 28 | Handled: 0b1111, 29 | Requested: 0b111111, 30 | WantHandled: 0b1111, 31 | WantRequested: 0b1111, 32 | }, 33 | { 34 | Name: "RestrictPathAccessToHandled", 35 | SupportedABI: 1, 36 | Handled: 0b1, 37 | Requested: 0b11, 38 | WantHandled: 0b1, 39 | WantRequested: 0b1, 40 | }, 41 | { 42 | Name: "DowngradeToV0IfKernelDoesNotSupportV1", 43 | SupportedABI: 0, 44 | Handled: 0b1, 45 | Requested: 0b11, 46 | WantHandled: 0b0, 47 | WantRequested: 0b0, 48 | }, 49 | { 50 | Name: "ReferSupportedOnV2", 51 | SupportedABI: 2, 52 | Handled: ll.AccessFSRefer | ll.AccessFSReadFile, 53 | Requested: ll.AccessFSRefer | ll.AccessFSReadFile, 54 | WantHandled: ll.AccessFSRefer | ll.AccessFSReadFile, 55 | WantRequested: ll.AccessFSRefer | ll.AccessFSReadFile, 56 | }, 57 | { 58 | Name: "ReferNotSupportedOnV1", 59 | SupportedABI: 1, 60 | Handled: ll.AccessFSRefer | ll.AccessFSReadFile, 61 | Requested: ll.AccessFSRefer | ll.AccessFSReadFile, 62 | WantFallbackToV0: true, 63 | }, 64 | } { 65 | t.Run(tc.Name, func(t *testing.T) { 66 | abi := abiInfos[tc.SupportedABI] 67 | 68 | rules := []Rule{PathAccess(tc.Requested, "foo")} 69 | cfg := Config{handledAccessFS: tc.Handled} 70 | gotCfg, gotRules := downgrade(cfg, rules, abi) 71 | 72 | if tc.WantFallbackToV0 { 73 | if gotCfg != v0 { 74 | t.Errorf( 75 | "downgrade(%v, %v, ABIv%d) = %v, %v; want fallback to V0", 76 | cfg, tc.Requested, tc.SupportedABI, 77 | gotCfg, gotRules, 78 | ) 79 | } 80 | return 81 | } 82 | 83 | if len(gotRules) != 1 { 84 | t.Fatalf("wrong number of rules returned: got %d, want 1", len(gotRules)) 85 | } 86 | gotRequested := gotRules[0].(FSRule).accessFS 87 | gotHandled := gotCfg.handledAccessFS 88 | 89 | if gotHandled != tc.WantHandled || gotRequested != tc.WantRequested { 90 | t.Errorf( 91 | "Unexpected result\ndowngrade(%v, %v, ABIv%d)\n = %v, %v\n want %v, %v", 92 | cfg, tc.Requested, tc.SupportedABI, 93 | gotCfg, gotRequested, 94 | Config{handledAccessFS: tc.WantHandled}, tc.WantRequested, 95 | ) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestDowngradeNetwork(t *testing.T) { 102 | cfg := Config{handledAccessNet: ll.AccessNetConnectTCP} 103 | abi := abiInfos[3] // does not have networking support 104 | rules := []Rule{ConnectTCP(53)} 105 | gotCfg, _ := downgrade(cfg, rules, abi) 106 | 107 | if gotCfg.handledAccessNet != 0 { 108 | t.Errorf("downgrade to v3 should remove networking support, but resulted in %v", gotCfg) 109 | } 110 | } 111 | 112 | func TestDowngradeNoop(t *testing.T) { 113 | for _, abi := range abiInfos { 114 | t.Run(fmt.Sprintf("V%v", abi.version), func(t *testing.T) { 115 | cfg := abi.asConfig().BestEffort() 116 | gotCfg, _ := downgrade(cfg, []Rule{}, abi) 117 | 118 | if gotCfg != cfg { 119 | t.Errorf("downgrade should have been a no-op.\n got %v,\nwant %v", gotCfg, cfg) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /landlock/restrict_failure_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock_test 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/landlock-lsm/go-landlock/landlock" 13 | "github.com/landlock-lsm/go-landlock/landlock/lltest" 14 | "golang.org/x/sys/unix" 15 | ) 16 | 17 | func MustWriteFile(t testing.TB, path string) { 18 | t.Helper() 19 | 20 | if err := os.WriteFile(path, []byte("somecontent"), 0600); err != nil { 21 | t.Fatalf("os.WriteFile(%q, ...): %v", path, err) 22 | } 23 | } 24 | 25 | func MustMkdir(t testing.TB, path string) { 26 | t.Helper() 27 | 28 | if err := os.Mkdir(path, 0700); err != nil { 29 | t.Fatalf("os.Mkdir(%q): %v", path, err) 30 | } 31 | } 32 | 33 | func MakeSomeFile(t testing.TB) string { 34 | t.Helper() 35 | fpath := filepath.Join(lltest.TempDir(t), "somefile") 36 | MustWriteFile(t, fpath) 37 | return fpath 38 | } 39 | 40 | func TestPathDoesNotExist(t *testing.T) { 41 | lltest.RequireABI(t, 1) 42 | 43 | doesNotExistPath := filepath.Join(t.TempDir(), "does_not_exist") 44 | 45 | err := landlock.V1.RestrictPaths( 46 | landlock.RODirs(doesNotExistPath), 47 | ) 48 | if !errors.Is(err, os.ErrNotExist) { 49 | t.Errorf("expected 'not exist' error, got: %v", err) 50 | } 51 | } 52 | 53 | func TestPathDoesNotExist_Ignored(t *testing.T) { 54 | lltest.RunInSubprocess(t, func() { 55 | lltest.RequireABI(t, 1) 56 | 57 | doesNotExistPath := filepath.Join(lltest.TempDir(t), "does_not_exist") 58 | 59 | err := landlock.V1.RestrictPaths( 60 | landlock.RODirs(doesNotExistPath).IgnoreIfMissing(), 61 | ) 62 | if err != nil { 63 | t.Errorf("expected no error, got: %v", err) 64 | } 65 | }) 66 | } 67 | 68 | func TestRestrictingPlainFileWithDirectoryFlags(t *testing.T) { 69 | lltest.RequireABI(t, 1) 70 | 71 | fpath := MakeSomeFile(t) 72 | 73 | err := landlock.V1.RestrictPaths( 74 | landlock.RODirs(fpath), 75 | ) 76 | if !errors.Is(err, unix.EINVAL) { 77 | t.Errorf("expected 'invalid argument' error, got: %v", err) 78 | } 79 | if isGoLandlockBug(err) { 80 | t.Errorf("should not be marked as a go-landlock bug, but was: %v", err) 81 | } 82 | } 83 | 84 | func isGoLandlockBug(err error) bool { 85 | return strings.Contains(err.Error(), "BUG(go-landlock)") 86 | } 87 | 88 | func TestEmptyAccessRights(t *testing.T) { 89 | lltest.RequireABI(t, 1) 90 | 91 | lltest.RunInSubprocess(t, func() { 92 | fpath := MakeSomeFile(t) 93 | 94 | err := landlock.V1.RestrictPaths( 95 | landlock.PathAccess(0, fpath), 96 | ) 97 | if err != nil { 98 | t.Errorf("expected success, got: %v", err) 99 | } 100 | }) 101 | } 102 | 103 | func TestOverlyBroadFSRule(t *testing.T) { 104 | lltest.RequireABI(t, 1) 105 | 106 | handled := landlock.AccessFSSet(0b011) 107 | excempt := landlock.AccessFSSet(0b111) // superset of handled! 108 | err := landlock.MustConfig(handled).RestrictPaths( 109 | landlock.PathAccess(excempt, "/tmp"), 110 | ) 111 | if !errors.Is(err, unix.EINVAL) { 112 | t.Errorf("expected 'invalid argument' error, got: %v", err) 113 | } 114 | } 115 | 116 | func TestReferNotPermittedInStrictV1(t *testing.T) { 117 | lltest.RequireABI(t, 1) 118 | 119 | // 'refer' is incompatible with Landlock ABI V1. 120 | // Users should use Landlock V2 instead or construct a custom 121 | // config that handles the 'refer' access right. 122 | // You can technically also just enable V1 best-effort mode, 123 | // but that combination always falls back to "no enforcement". 124 | for _, rule := range []landlock.Rule{ 125 | landlock.RWDirs("/etc").WithRefer(), 126 | landlock.PathAccess(0, "/etc").WithRefer(), 127 | } { 128 | err := landlock.V1.RestrictPaths(rule) 129 | if !errors.Is(err, unix.EINVAL) { 130 | t.Errorf("expected 'invalid argument' error, got: %v", err) 131 | } 132 | if !strings.Contains(err.Error(), "incompatible rule") { 133 | t.Errorf("expected a 'incompatible rule' error, got: %v", err) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /landlock/restrict_nonlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package landlock 4 | 5 | import "fmt" 6 | 7 | func restrict(c Config, rules ...Rule) error { 8 | if c.bestEffort { 9 | return nil // Fallback to "nothing" 10 | } 11 | return fmt.Errorf("missing kernel Landlock support. Landlock is only supported on Linux") 12 | } 13 | -------------------------------------------------------------------------------- /landlock/restrict_nonlinux_test.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package landlock_test 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | 9 | "github.com/landlock-lsm/go-landlock/landlock" 10 | ) 11 | 12 | func TestRestrictNonLinux_BestEffort(t *testing.T) { 13 | err := landlock.V3.BestEffort().RestrictPaths( 14 | landlock.RODirs("/"), 15 | ) 16 | if err != nil { 17 | t.Errorf("expected success (downgraded to doing nothing)") 18 | } 19 | } 20 | 21 | func TestRestrictNonLinux_Strict(t *testing.T) { 22 | err := landlock.V3.RestrictPaths( 23 | landlock.RODirs("/"), 24 | ) 25 | errStr := "missing kernel Landlock support" 26 | if !strings.Contains(err.Error(), errStr) { 27 | t.Errorf("expected error with %q, got %v", errStr, err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /landlock/restrict_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock_test 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "syscall" 16 | "testing" 17 | 18 | "github.com/landlock-lsm/go-landlock/landlock" 19 | "github.com/landlock-lsm/go-landlock/landlock/lltest" 20 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 21 | "golang.org/x/sys/unix" 22 | ) 23 | 24 | func TestRestrictPaths(t *testing.T) { 25 | // On kernels before 5.19.8, some refer cases returned EXDEV 26 | // which now return EACCES. 27 | exdevBefore5198 := syscall.EXDEV 28 | if major, minor, patch := OSRelease(t); 1000*1000*major+1000*minor+patch >= 5019008 { 29 | exdevBefore5198 = syscall.EACCES 30 | } 31 | 32 | for _, tt := range []struct { 33 | Name string 34 | EnableLandlock func(dir, fpath string) error 35 | RequiredABI int 36 | WantOpenErr error 37 | WantReadDirErr error 38 | WantCreateErr error 39 | WantMkdirErr error 40 | WantUnlinkErr error 41 | WantMkfifoErr error 42 | WantReferErr error 43 | WantTruncateErr error 44 | }{ 45 | { 46 | Name: "EverythingForbidden", 47 | RequiredABI: 1, 48 | EnableLandlock: func(dir, fpath string) error { 49 | return landlock.V1.RestrictPaths() 50 | }, 51 | WantOpenErr: syscall.EACCES, 52 | WantReadDirErr: syscall.EACCES, 53 | WantCreateErr: syscall.EACCES, 54 | WantMkdirErr: syscall.EACCES, 55 | WantUnlinkErr: syscall.EACCES, 56 | WantMkfifoErr: syscall.EACCES, 57 | WantReferErr: exdevBefore5198, 58 | WantTruncateErr: nil, 59 | }, 60 | { 61 | Name: "ROFilesPermissionsOnFile", 62 | RequiredABI: 1, 63 | EnableLandlock: func(dir, fpath string) error { 64 | return landlock.V1.RestrictPaths(landlock.ROFiles(fpath)) 65 | }, 66 | WantOpenErr: nil, 67 | WantReadDirErr: syscall.EACCES, 68 | WantCreateErr: syscall.EACCES, 69 | WantMkdirErr: syscall.EACCES, 70 | WantUnlinkErr: syscall.EACCES, 71 | WantMkfifoErr: syscall.EACCES, 72 | WantReferErr: exdevBefore5198, 73 | WantTruncateErr: nil, 74 | }, 75 | { 76 | Name: "RWFilesPermissionsOnFile", 77 | RequiredABI: 1, 78 | EnableLandlock: func(dir, fpath string) error { 79 | return landlock.V1.RestrictPaths(landlock.RWFiles(fpath)) 80 | }, 81 | WantOpenErr: nil, 82 | WantReadDirErr: syscall.EACCES, 83 | WantCreateErr: nil, 84 | WantMkdirErr: syscall.EACCES, 85 | WantUnlinkErr: syscall.EACCES, 86 | WantMkfifoErr: syscall.EACCES, 87 | WantReferErr: exdevBefore5198, 88 | WantTruncateErr: nil, 89 | }, 90 | { 91 | Name: "ROFilesPermissionsOnDir", 92 | RequiredABI: 1, 93 | EnableLandlock: func(dir, fpath string) error { 94 | return landlock.V1.RestrictPaths(landlock.ROFiles(dir)) 95 | }, 96 | WantOpenErr: nil, 97 | WantReadDirErr: syscall.EACCES, 98 | WantCreateErr: syscall.EACCES, 99 | WantMkdirErr: syscall.EACCES, 100 | WantUnlinkErr: syscall.EACCES, 101 | WantMkfifoErr: syscall.EACCES, 102 | WantReferErr: exdevBefore5198, 103 | WantTruncateErr: nil, 104 | }, 105 | { 106 | Name: "RWFilesPermissionsOnDir", 107 | RequiredABI: 1, 108 | EnableLandlock: func(dir, fpath string) error { 109 | return landlock.V1.RestrictPaths(landlock.RWFiles(dir)) 110 | }, 111 | WantOpenErr: nil, 112 | WantReadDirErr: syscall.EACCES, 113 | WantCreateErr: nil, 114 | WantMkdirErr: syscall.EACCES, 115 | WantUnlinkErr: syscall.EACCES, 116 | WantMkfifoErr: syscall.EACCES, 117 | WantReferErr: exdevBefore5198, 118 | WantTruncateErr: nil, 119 | }, 120 | { 121 | Name: "RODirsPermissionsOnDir", 122 | RequiredABI: 1, 123 | EnableLandlock: func(dir, fpath string) error { 124 | return landlock.V1.RestrictPaths(landlock.RODirs(dir)) 125 | }, 126 | WantOpenErr: nil, 127 | WantReadDirErr: nil, 128 | WantCreateErr: syscall.EACCES, 129 | WantMkdirErr: syscall.EACCES, 130 | WantUnlinkErr: syscall.EACCES, 131 | WantMkfifoErr: syscall.EACCES, 132 | WantReferErr: exdevBefore5198, 133 | WantTruncateErr: nil, 134 | }, 135 | { 136 | Name: "RWDirsPermissionsOnDir", 137 | RequiredABI: 1, 138 | EnableLandlock: func(dir, fpath string) error { 139 | return landlock.V1.RestrictPaths(landlock.RWDirs(dir)) 140 | }, 141 | WantOpenErr: nil, 142 | WantReadDirErr: nil, 143 | WantCreateErr: nil, 144 | WantMkdirErr: nil, 145 | WantUnlinkErr: nil, 146 | WantMkfifoErr: nil, 147 | WantReferErr: syscall.EXDEV, 148 | WantTruncateErr: nil, 149 | }, 150 | { 151 | Name: "RWDirsWithRefer", 152 | RequiredABI: 2, 153 | EnableLandlock: func(dir, fpath string) error { 154 | return landlock.V2.RestrictPaths(landlock.RWDirs(dir).WithRefer()) 155 | }, 156 | WantOpenErr: nil, 157 | WantReadDirErr: nil, 158 | WantCreateErr: nil, 159 | WantMkdirErr: nil, 160 | WantUnlinkErr: nil, 161 | WantMkfifoErr: nil, 162 | WantReferErr: nil, 163 | WantTruncateErr: nil, 164 | }, 165 | { 166 | Name: "RWDirsWithoutRefer", 167 | RequiredABI: 2, 168 | EnableLandlock: func(dir, fpath string) error { 169 | return landlock.V2.RestrictPaths(landlock.RWDirs(dir) /* without refer */) 170 | }, 171 | WantOpenErr: nil, 172 | WantReadDirErr: nil, 173 | WantCreateErr: nil, 174 | WantMkdirErr: nil, 175 | WantUnlinkErr: nil, 176 | WantMkfifoErr: nil, 177 | WantReferErr: syscall.EXDEV, 178 | WantTruncateErr: nil, 179 | }, 180 | { 181 | Name: "RWDirsV3", 182 | RequiredABI: 3, 183 | EnableLandlock: func(dir, fpath string) error { 184 | return landlock.V3.RestrictPaths(landlock.RWDirs(dir)) 185 | }, 186 | WantOpenErr: nil, 187 | WantReadDirErr: nil, 188 | WantCreateErr: nil, 189 | WantMkdirErr: nil, 190 | WantUnlinkErr: nil, 191 | WantMkfifoErr: nil, 192 | WantReferErr: syscall.EXDEV, 193 | WantTruncateErr: nil, 194 | }, 195 | { 196 | Name: "EverythingForbiddenV3", 197 | RequiredABI: 3, 198 | EnableLandlock: func(dir, fpath string) error { 199 | return landlock.V3.RestrictPaths() 200 | }, 201 | WantOpenErr: syscall.EACCES, 202 | WantReadDirErr: syscall.EACCES, 203 | WantCreateErr: syscall.EACCES, 204 | WantMkdirErr: syscall.EACCES, 205 | WantUnlinkErr: syscall.EACCES, 206 | WantMkfifoErr: syscall.EACCES, 207 | WantReferErr: exdevBefore5198, 208 | WantTruncateErr: syscall.EACCES, 209 | }, 210 | } { 211 | t.Run(tt.Name, func(t *testing.T) { 212 | lltest.RunInSubprocess(t, func() { 213 | lltest.RequireABI(t, tt.RequiredABI) 214 | 215 | dir := lltest.TempDir(t) 216 | fpath := filepath.Join(dir, "lolcat.txt") 217 | MustWriteFile(t, fpath) 218 | renameMeFpath := filepath.Join(dir, "renameme.txt") 219 | MustWriteFile(t, renameMeFpath) 220 | dstDirPath := filepath.Join(dir, "dst") 221 | MustMkdir(t, dstDirPath) 222 | 223 | err := tt.EnableLandlock(dir, fpath) 224 | if err != nil { 225 | t.Fatalf("Enabling Landlock: %v", err) 226 | } 227 | 228 | if err := openForRead(fpath); !errEqual(err, tt.WantOpenErr) { 229 | t.Errorf("openForRead(%q) = «%v», want «%v»", fpath, err, tt.WantOpenErr) 230 | } 231 | 232 | if _, err := os.ReadDir(dir); !errEqual(err, tt.WantReadDirErr) { 233 | t.Errorf("os.ReadDir(%q) = «%v», want «%v»", dir, err, tt.WantReadDirErr) 234 | } 235 | 236 | if err := openForWrite(fpath); !errEqual(err, tt.WantCreateErr) { 237 | t.Errorf("os.Create(%q) = «%v», want «%v»", fpath, err, tt.WantCreateErr) 238 | } 239 | 240 | if err := os.Truncate(fpath, 3); !errEqual(err, tt.WantTruncateErr) { 241 | t.Errorf("os.Truncate(%q, ...) = «%v», want «%v»", fpath, err, tt.WantTruncateErr) 242 | } 243 | 244 | subdirPath := filepath.Join(dir, "subdir") 245 | if err := os.Mkdir(subdirPath, 0600); !errEqual(err, tt.WantMkdirErr) { 246 | t.Errorf("os.Mkdir(%q) = «%v», want «%v»", subdirPath, err, tt.WantMkdirErr) 247 | } 248 | 249 | if err := os.Remove(fpath); !errEqual(err, tt.WantUnlinkErr) { 250 | t.Errorf("os.Remove(%q) = «%v», want «%v»", fpath, err, tt.WantUnlinkErr) 251 | } 252 | 253 | fifoPath := filepath.Join(dir, "fifo") 254 | if err := unix.Mkfifo(fifoPath, 0600); !errEqual(err, tt.WantMkfifoErr) { 255 | t.Errorf("os.Mkfifo(%q, ...) = «%v», want «%v»", fifoPath, err, tt.WantMkfifoErr) 256 | } 257 | 258 | dstFpath := filepath.Join(dstDirPath, "target.txt") 259 | if err := os.Rename(renameMeFpath, dstFpath); !errEqual(err, tt.WantReferErr) { 260 | t.Errorf("os.Rename(%q, %q) = «%v», want «%v»", renameMeFpath, dstFpath, err, tt.WantReferErr) 261 | } 262 | }) 263 | }) 264 | } 265 | } 266 | 267 | func openForRead(path string) error { 268 | f, err := os.Open(path) 269 | if err != nil { 270 | return err 271 | } 272 | defer f.Close() 273 | return nil 274 | } 275 | 276 | func openForWrite(path string) error { 277 | f, err := os.Create(path) 278 | if err != nil { 279 | return err 280 | } 281 | defer f.Close() 282 | return nil 283 | } 284 | 285 | func TestRestrictNet(t *testing.T) { 286 | const ( 287 | cPort = 4242 288 | bPort = 4343 289 | ) 290 | 291 | for _, tt := range []struct { 292 | Name string 293 | EnableLandlock func() error 294 | RequiredABI int 295 | WantConnectErr error 296 | WantBindErr error 297 | }{ 298 | { 299 | Name: "ABITooOld", 300 | RequiredABI: 3, 301 | EnableLandlock: func() error { 302 | return landlock.V3.RestrictNet() 303 | }, 304 | WantConnectErr: nil, 305 | WantBindErr: nil, 306 | }, 307 | { 308 | Name: "ABITooOldWithDowngrade", 309 | RequiredABI: 3, 310 | EnableLandlock: func() error { 311 | return landlock.V3.BestEffort().RestrictNet() 312 | }, 313 | WantConnectErr: nil, 314 | WantBindErr: nil, 315 | }, 316 | { 317 | Name: "RestrictingPathsShouldNotBreakNetworking", 318 | RequiredABI: 1, 319 | EnableLandlock: func() error { 320 | return landlock.V4.BestEffort().RestrictPaths( 321 | landlock.ROFiles("/etc/hosts"), 322 | ) 323 | }, 324 | WantConnectErr: nil, 325 | WantBindErr: nil, 326 | }, 327 | { 328 | Name: "RestrictingBindButConnectShouldWork", 329 | RequiredABI: 4, 330 | EnableLandlock: func() error { 331 | return landlock.MustConfig( 332 | landlock.AccessNetSet(ll.AccessNetBindTCP), 333 | ).RestrictNet() 334 | }, 335 | WantConnectErr: nil, 336 | WantBindErr: syscall.EACCES, 337 | }, 338 | { 339 | Name: "RestrictingConnectButBindShouldWork", 340 | RequiredABI: 4, 341 | EnableLandlock: func() error { 342 | return landlock.MustConfig( 343 | landlock.AccessNetSet(ll.AccessNetConnectTCP), 344 | ).RestrictNet() 345 | }, 346 | WantConnectErr: syscall.EACCES, 347 | WantBindErr: nil, 348 | }, 349 | { 350 | Name: "PermitTheConnectPort", 351 | RequiredABI: 4, 352 | EnableLandlock: func() error { 353 | return landlock.V4.RestrictNet(landlock.ConnectTCP(cPort)) 354 | }, 355 | WantConnectErr: nil, 356 | WantBindErr: syscall.EACCES, 357 | }, 358 | { 359 | Name: "PermitTheBindPort", 360 | RequiredABI: 4, 361 | EnableLandlock: func() error { 362 | return landlock.V4.RestrictNet(landlock.BindTCP(bPort)) 363 | }, 364 | WantConnectErr: syscall.EACCES, 365 | WantBindErr: nil, 366 | }, 367 | { 368 | Name: "PermitBothPorts", 369 | RequiredABI: 4, 370 | EnableLandlock: func() error { 371 | return landlock.V4.RestrictNet( 372 | landlock.BindTCP(bPort), 373 | landlock.ConnectTCP(cPort), 374 | ) 375 | }, 376 | WantConnectErr: nil, 377 | WantBindErr: nil, 378 | }, 379 | { 380 | Name: "PermitTheWrongPorts", 381 | RequiredABI: 4, 382 | EnableLandlock: func() error { 383 | return landlock.V4.RestrictNet( 384 | landlock.BindTCP(bPort+1), 385 | landlock.ConnectTCP(cPort+1), 386 | ) 387 | }, 388 | WantConnectErr: syscall.EACCES, 389 | WantBindErr: syscall.EACCES, 390 | }, 391 | } { 392 | t.Run(tt.Name, func(t *testing.T) { 393 | lltest.RunInSubprocess(t, func() { 394 | lltest.RequireABI(t, tt.RequiredABI) 395 | 396 | // Set up a service that we can dial for the test. 397 | runBackgroundService(t, "tcp", fmt.Sprintf("localhost:%v", cPort)) 398 | 399 | err := tt.EnableLandlock() 400 | if err != nil { 401 | t.Fatalf("Enabling Landlock: %v", err) 402 | } 403 | 404 | if err := tryDial(cPort); !errEqual(err, tt.WantConnectErr) { 405 | t.Errorf("net.Dial(tcp, localhost:%v) = «%v»; want «%v»", cPort, err, tt.WantConnectErr) 406 | } 407 | if err := tryListen(bPort); !errEqual(err, tt.WantBindErr) { 408 | t.Errorf("net.Listen(tcp, localhost:%v) = «%v»; want «%v»", bPort, err, tt.WantBindErr) 409 | } 410 | }) 411 | }) 412 | } 413 | } 414 | 415 | func runBackgroundService(t *testing.T, network, addr string) { 416 | l, err := net.Listen(network, addr) 417 | if err != nil { 418 | t.Fatalf("net.Listen: Failed to set up local service to connect to: %v", err) 419 | } 420 | var wg sync.WaitGroup 421 | wg.Add(1) 422 | go func() { 423 | defer wg.Done() 424 | for { 425 | c, err := l.Accept() 426 | if err != nil { 427 | // Return on error (e.g. if l gets closed asynchronously) 428 | return 429 | } 430 | c.Close() 431 | } 432 | }() 433 | t.Cleanup(func() { 434 | l.Close() 435 | wg.Wait() 436 | }) 437 | } 438 | 439 | func tryDial(port int) error { 440 | conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", port)) 441 | if err == nil { 442 | conn.Close() 443 | } 444 | return err 445 | } 446 | 447 | func tryListen(port int) error { 448 | conn, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", port)) 449 | if err == nil { 450 | conn.Close() 451 | } 452 | return err 453 | } 454 | 455 | func TestIoctlDev(t *testing.T) { 456 | const ( 457 | path = "/dev/zero" 458 | FIONREAD = 0x541b 459 | ) 460 | for _, tt := range []struct { 461 | Name string 462 | Rule landlock.Rule 463 | WantErr error 464 | }{ 465 | { 466 | Name: "WithoutIoctlDev", 467 | Rule: landlock.RWFiles(path), 468 | WantErr: syscall.EACCES, 469 | }, 470 | { 471 | Name: "WithIoctlDev", 472 | Rule: landlock.RWFiles(path).WithIoctlDev(), 473 | // ENOTTY means that the IOCTL was dispatched 474 | // to device. (Would be nicer to find an 475 | // IOCTL that returns success here, but the 476 | // available devices on qemu are limited.) 477 | WantErr: syscall.ENOTTY, 478 | }, 479 | } { 480 | t.Run(tt.Name, func(t *testing.T) { 481 | lltest.RunInSubprocess(t, func() { 482 | lltest.RequireABI(t, 5) 483 | 484 | err := landlock.V5.BestEffort().RestrictPaths(tt.Rule) 485 | if err != nil { 486 | t.Fatalf("Enabling Landlock: %v", err) 487 | } 488 | 489 | f, err := os.Open(path) 490 | if err != nil { 491 | t.Fatalf("os.Open(%q): %v", path, err) 492 | } 493 | defer func() { f.Close() }() 494 | 495 | _, err = unix.IoctlGetInt(int(f.Fd()), FIONREAD) 496 | if !errEqual(err, tt.WantErr) { 497 | t.Errorf("ioctl(%v, FIONREAD): got err «%v», want «%v»", f, err, tt.WantErr) 498 | } 499 | }) 500 | }) 501 | } 502 | } 503 | 504 | func errEqual(got, want error) bool { 505 | if got == nil && want == nil { 506 | return true 507 | } 508 | return errors.Is(got, want) 509 | } 510 | 511 | func OSRelease(t testing.TB) (major, minor, patch int) { 512 | t.Helper() 513 | 514 | var buf unix.Utsname 515 | if err := unix.Uname(&buf); err != nil { 516 | t.Fatalf("Uname: %v", err) 517 | } 518 | release := string(buf.Release[:bytes.IndexByte(buf.Release[:], 0)]) 519 | release, _, _ = strings.Cut(release, "-") 520 | release, _, _ = strings.Cut(release, "+") 521 | 522 | parts := strings.SplitN(release, ".", 4) 523 | if len(parts) < 3 { 524 | t.Fatalf("Invalid release format %q", release) 525 | } 526 | major, err := strconv.Atoi(parts[0]) 527 | if err != nil { 528 | t.Fatalf("strconv.Atoi(%q): %v", parts[0], err) 529 | } 530 | minor, err = strconv.Atoi(parts[1]) 531 | if err != nil { 532 | t.Fatalf("strconv.Atoi(%q): %v", parts[1], err) 533 | } 534 | patch, err = strconv.Atoi(parts[2]) 535 | if err != nil { 536 | t.Fatalf("strconv.Atoi(%q): %v", parts[2], err) 537 | } 538 | return major, minor, patch 539 | } 540 | -------------------------------------------------------------------------------- /landlock/restrict_threading_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package landlock_test 4 | 5 | import ( 6 | "sync" 7 | "testing" 8 | 9 | "github.com/landlock-lsm/go-landlock/landlock" 10 | "github.com/landlock-lsm/go-landlock/landlock/lltest" 11 | ) 12 | 13 | // Verify that Landlock applies to all system threads that belong to 14 | // the current Go process. The raw landlock_restrict_self syscall only 15 | // applies to the current system thread, but these are managed by the 16 | // Go runtime and not easily controlled. The same issue has already 17 | // been discussed in the context of seccomp at 18 | // https://github.com/golang/go/issues/3405. 19 | func TestRestrictInPresenceOfThreading(t *testing.T) { 20 | lltest.RunInSubprocess(t, func() { 21 | lltest.RequireABI(t, 1) 22 | 23 | fpath := MakeSomeFile(t) 24 | 25 | err := landlock.V1.RestrictPaths() // No access permitted at all. 26 | if err != nil { 27 | t.Skipf("kernel does not support Landlock v1; tests cannot be run.") 28 | } 29 | 30 | var wg sync.WaitGroup 31 | defer wg.Wait() 32 | 33 | const ( 34 | parallelism = 3 35 | attempts = 10 36 | ) 37 | for g := 0; g < parallelism; g++ { 38 | wg.Add(1) 39 | go func(grIdx int) { 40 | defer wg.Done() 41 | for i := 0; i < attempts; i++ { 42 | if err := openForRead(fpath); err == nil { 43 | t.Errorf("openForRead(%q) successful, want error", fpath) 44 | } 45 | } 46 | }(g) 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /landlock/syscall/landlock.go: -------------------------------------------------------------------------------- 1 | // Package syscall provides a low-level interface to the Linux Landlock sandboxing feature. 2 | // 3 | // The package contains constants and syscall wrappers. The syscall 4 | // wrappers whose names start with AllThreads will execute the syscall 5 | // on all OS threads belonging to the current process, as long as 6 | // these threads have been started implicitly by the Go runtime or 7 | // using `pthread_create`. 8 | // 9 | // This package package is a stopgap solution while there is no 10 | // Landlock support in x/sys/unix. The syscall package is considered 11 | // highly unstable and may change or disappear without warning. 12 | // 13 | // The full documentation can be found at 14 | // https://www.kernel.org/doc/html/latest/userspace-api/landlock.html. 15 | package syscall 16 | 17 | // Landlock file system access rights. 18 | // 19 | // Please see the full documentation at 20 | // https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#filesystem-flags. 21 | const ( 22 | AccessFSExecute = 1 << iota 23 | AccessFSWriteFile 24 | AccessFSReadFile 25 | AccessFSReadDir 26 | AccessFSRemoveDir 27 | AccessFSRemoveFile 28 | AccessFSMakeChar 29 | AccessFSMakeDir 30 | AccessFSMakeReg 31 | AccessFSMakeSock 32 | AccessFSMakeFifo 33 | AccessFSMakeBlock 34 | AccessFSMakeSym 35 | AccessFSRefer 36 | AccessFSTruncate 37 | AccessFSIoctlDev 38 | ) 39 | 40 | // Landlock network access rights. 41 | // 42 | // Please see the full documentation at 43 | // https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#network-flags. 44 | const ( 45 | AccessNetBindTCP = 1 << iota 46 | AccessNetConnectTCP 47 | ) 48 | 49 | // RulesetAttr is the Landlock ruleset definition. 50 | // 51 | // Argument of LandlockCreateRuleset(). This structure can grow in future versions of Landlock. 52 | // 53 | // C version is in usr/include/linux/landlock.h 54 | type RulesetAttr struct { 55 | HandledAccessFS uint64 56 | HandledAccessNet uint64 57 | } 58 | 59 | // The size of the RulesetAttr struct in bytes. 60 | const rulesetAttrSize = 16 61 | 62 | // PathBeneathAttr references a file hierarchy and defines the desired 63 | // extent to which it should be usable when the rule is enforced. 64 | type PathBeneathAttr struct { 65 | // AllowedAccess is a bitmask of allowed actions for this file 66 | // hierarchy (cf. "Filesystem flags"). The enabled bits must 67 | // be a subset of the bits defined in the ruleset. 68 | AllowedAccess uint64 69 | 70 | // ParentFd is a file descriptor, opened with `O_PATH`, which identifies 71 | // the parent directory of a file hierarchy, or just a file. 72 | ParentFd int 73 | } 74 | 75 | // NetPortAttr specifies which ports can be used for what. 76 | type NetPortAttr struct { 77 | AllowedAccess uint64 78 | Port uint64 79 | } 80 | -------------------------------------------------------------------------------- /landlock/syscall/landlock_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package syscall 4 | 5 | import ( 6 | "testing" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func TestAccessRights(t *testing.T) { 12 | for _, tt := range []struct { 13 | Name string 14 | LandlockDef uint64 15 | SyscallDef uint64 16 | }{ 17 | {"FSExecute", AccessFSExecute, unix.LANDLOCK_ACCESS_FS_EXECUTE}, 18 | {"FSWriteFile", AccessFSWriteFile, unix.LANDLOCK_ACCESS_FS_WRITE_FILE}, 19 | {"FSReadFile", AccessFSReadFile, unix.LANDLOCK_ACCESS_FS_READ_FILE}, 20 | {"FSReadDir", AccessFSReadDir, unix.LANDLOCK_ACCESS_FS_READ_DIR}, 21 | {"FSRemoveDir", AccessFSRemoveDir, unix.LANDLOCK_ACCESS_FS_REMOVE_DIR}, 22 | {"FSRemoveFile", AccessFSRemoveFile, unix.LANDLOCK_ACCESS_FS_REMOVE_FILE}, 23 | {"FSMakeChar", AccessFSMakeChar, unix.LANDLOCK_ACCESS_FS_MAKE_CHAR}, 24 | {"FSMakeDir", AccessFSMakeDir, unix.LANDLOCK_ACCESS_FS_MAKE_DIR}, 25 | {"FSMakeReg", AccessFSMakeReg, unix.LANDLOCK_ACCESS_FS_MAKE_REG}, 26 | {"FSMakeSock", AccessFSMakeSock, unix.LANDLOCK_ACCESS_FS_MAKE_SOCK}, 27 | {"FSMakeFifo", AccessFSMakeFifo, unix.LANDLOCK_ACCESS_FS_MAKE_FIFO}, 28 | {"FSMakeBlock", AccessFSMakeBlock, unix.LANDLOCK_ACCESS_FS_MAKE_BLOCK}, 29 | {"FSMakeSym", AccessFSMakeSym, unix.LANDLOCK_ACCESS_FS_MAKE_SYM}, 30 | {"FSRefer", AccessFSRefer, unix.LANDLOCK_ACCESS_FS_REFER}, 31 | {"FSTruncate", AccessFSTruncate, unix.LANDLOCK_ACCESS_FS_TRUNCATE}, 32 | {"FSIoctlDev", AccessFSIoctlDev, unix.LANDLOCK_ACCESS_FS_IOCTL_DEV}, 33 | {"NetBindTCP", AccessNetBindTCP, unix.LANDLOCK_ACCESS_NET_BIND_TCP}, 34 | {"NetConnectTCP", AccessNetConnectTCP, unix.LANDLOCK_ACCESS_NET_CONNECT_TCP}, 35 | } { 36 | t.Run(tt.Name, func(t *testing.T) { 37 | if tt.LandlockDef != tt.SyscallDef { 38 | t.Errorf("Landlock definition differs from x/sys/unix definition; got %v, want %v", tt.LandlockDef, tt.SyscallDef) 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /landlock/syscall/syscall_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package syscall 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/unix" 10 | "kernel.org/pub/linux/libs/security/libcap/psx" 11 | ) 12 | 13 | // LandlockCreateRuleset creates a ruleset file descriptor with the 14 | // given attributes. 15 | func LandlockCreateRuleset(attr *RulesetAttr, flags int) (fd int, err error) { 16 | r0, _, e1 := syscall.Syscall(unix.SYS_LANDLOCK_CREATE_RULESET, uintptr(unsafe.Pointer(attr)), uintptr(rulesetAttrSize), uintptr(flags)) 17 | fd = int(r0) 18 | if e1 != 0 { 19 | err = syscall.Errno(e1) 20 | } 21 | return 22 | } 23 | 24 | // LandlockGetABIVersion returns the supported Landlock ABI version (starting at 1). 25 | func LandlockGetABIVersion() (version int, err error) { 26 | r0, _, e1 := syscall.Syscall(unix.SYS_LANDLOCK_CREATE_RULESET, 0, 0, unix.LANDLOCK_CREATE_RULESET_VERSION) 27 | version = int(r0) 28 | if e1 != 0 { 29 | err = syscall.Errno(e1) 30 | } 31 | return 32 | } 33 | 34 | // Landlock rule types. 35 | const ( 36 | RuleTypePathBeneath = unix.LANDLOCK_RULE_PATH_BENEATH 37 | RuleTypeNetPort = 2 // TODO: Use it from sys/unix when available. 38 | ) 39 | 40 | // LandlockAddPathBeneathRule adds a rule of type "path beneath" to 41 | // the given ruleset fd. attr defines the rule parameters. flags must 42 | // currently be 0. 43 | func LandlockAddPathBeneathRule(rulesetFd int, attr *PathBeneathAttr, flags int) error { 44 | return LandlockAddRule(rulesetFd, RuleTypePathBeneath, unsafe.Pointer(attr), flags) 45 | } 46 | 47 | // LandlockAddNetPortRule adds a rule of type "net port" to the given ruleset FD. 48 | // attr defines the rule parameters. flags must currently be 0. 49 | func LandlockAddNetPortRule(rulesetFD int, attr *NetPortAttr, flags int) error { 50 | return LandlockAddRule(rulesetFD, RuleTypeNetPort, unsafe.Pointer(attr), flags) 51 | } 52 | 53 | // LandlockAddRule is the generic landlock_add_rule syscall. 54 | func LandlockAddRule(rulesetFd int, ruleType int, ruleAttr unsafe.Pointer, flags int) (err error) { 55 | _, _, e1 := syscall.Syscall6(unix.SYS_LANDLOCK_ADD_RULE, uintptr(rulesetFd), uintptr(ruleType), uintptr(ruleAttr), uintptr(flags), 0, 0) 56 | if e1 != 0 { 57 | err = syscall.Errno(e1) 58 | } 59 | return 60 | } 61 | 62 | // AllThreadsLandlockRestrictSelf enforces the given ruleset on all OS 63 | // threads belonging to the current process. 64 | func AllThreadsLandlockRestrictSelf(rulesetFd int, flags int) (err error) { 65 | _, _, e1 := psx.Syscall3(unix.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), uintptr(flags), 0) 66 | if e1 != 0 { 67 | err = syscall.Errno(e1) 68 | } 69 | return 70 | } 71 | 72 | // AllThreadsPrctl is like unix.Prctl, but gets applied on all OS threads at the same time. 73 | func AllThreadsPrctl(option int, arg2, arg3, arg4, arg5 uintptr) (err error) { 74 | _, _, e1 := psx.Syscall6(syscall.SYS_PRCTL, uintptr(option), uintptr(arg2), uintptr(arg3), uintptr(arg4), uintptr(arg5), 0) 75 | if e1 != 0 { 76 | err = syscall.Errno(e1) 77 | } 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /landlock/syscall/syscall_nonlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package syscall 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | func LandlockCreateRuleset(attr *RulesetAttr, flags int) (fd int, err error) { 11 | return -1, syscall.ENOSYS 12 | } 13 | 14 | func LandlockGetABIVersion() (version int, err error) { 15 | return -1, syscall.ENOSYS 16 | } 17 | 18 | func LandlockAddRule(rulesetFd int, ruleType int, ruleAttr unsafe.Pointer, flags int) (err error) { 19 | return syscall.ENOSYS 20 | } 21 | 22 | func LandlockAddPathBeneathRule(rulesetFd int, attr *PathBeneathAttr, flags int) error { 23 | return syscall.ENOSYS 24 | } 25 | 26 | func LandlockAddNetPortRule(rulesetFD int, attr *NetPortAttr, flags int) error { 27 | return syscall.ENOSYS 28 | } 29 | 30 | func AllThreadsLandlockRestrictSelf(rulesetFd int, flags int) (err error) { 31 | return syscall.ENOSYS 32 | } 33 | 34 | func AllThreadsPrctl(option int, arg2, arg3, arg4, arg5 uintptr) (err error) { 35 | return syscall.ENOSYS 36 | } 37 | --------------------------------------------------------------------------------