filed
Job queue using FUSE
git clone git://mccd.space/filed
| Log | Files | Refs | README | LICENSE |
commit 61b5e55411a834a8338c72dc1944dd0e5e3786a3 parent abe3ee2a86df367934e1768b52303efcd7c8d551 Author: Marc Coquand <marc@coquand.email> Date: Thu, 18 Dec 2025 14:14:51 +0100 Add initial sandboxing support functions Diffstat:
| M | .gitignore | | | 2 | ++ |
| M | README.md | | | 10 | +++++++--- |
| A | cmd/filed-launch.go | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | filed-launch.1.scd | | | 19 | +++++++++++++++++++ |
| M | filed.5.scd | | | 8 | +++++++- |
| M | go.mod | | | 2 | ++ |
| M | go.sum | | | 4 | ++++ |
| M | manager.go | | | 40 | +++++----------------------------------- |
| M | store/jobs.go | | | 2 | +- |
9 files changed, 124 insertions(+), 40 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,6 @@
filed
filed.[1-8]
filed.config.[1-8]
+filed-launch
+filed-launch.[1-8]
diff --git a/README.md b/README.md
@@ -13,17 +13,19 @@ File d'attente is built in Go and depends on sqlite and fuse (make sure fusermou
```sh
$ git clone https://sr.ht/~marcc/filed/
$ cd filed
-$ go build
$ go install
+$ go install cmd/filed-launch.go
```
To build the docs you need [scdoc]
```sh
-$ scdoc < filed.5.scd > filed.5
-$ scdoc < filed.config.5.scd > filed.config.5
+$ for f in filed*.scd; do
+ scdoc < "$f" > "${f%.scd}"
+done
# mv filed.5 /usr/local/man/man5
# mv filed.config.5 /usr/local/man/man5
+# mv filed-launch.1 /usr/local/man/man1
```
## Getting started
@@ -91,6 +93,8 @@ I was inspired by 9p, and files proved to be a great abstraction since directori
- [x] Customizable backoff and timeout before retries
- [x] Last modified and created at are correctly rendered for jobs
- [ ] "Landlock"-mode, or sandboxed jobs - Requires a design
+ - [x] Add filed-launch - a script that can be used to restrict command access
+ - [ ] Add command arguments to filed to lock it down, but still allow it access to state files, and remove that access in filed-launch
- [ ] A reusable systemd unit file
- [ ] Notification on failure. Unfortunately [inotify does not work with fuse], which would have been elegant otherwise.
- [ ] Notify forget and other updates.
diff --git a/cmd/filed-launch.go b/cmd/filed-launch.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "syscall"
+
+ "github.com/landlock-lsm/go-landlock/landlock"
+)
+
+func main() {
+ var roPaths, roFilePaths, rwFilePaths, rwPaths []string
+ flag.Func("ro", "Read-only path", func(s string) error {
+ roPaths = append(roPaths, s)
+ return nil
+ })
+
+ flag.Func("rof", "Read-only file", func(s string) error {
+ roFilePaths = append(roFilePaths, s)
+ return nil
+ })
+
+ flag.Func("rwf", "Read-write file", func(s string) error {
+ rwFilePaths = append(rwFilePaths, s)
+ return nil
+ })
+
+ flag.Func("rw", "Read-write path", func(s string) error {
+ rwPaths = append(rwPaths, s)
+ return nil
+ })
+ flag.Parse()
+
+ if flag.NArg() < 1 {
+ fmt.Fprintf(os.Stderr, "Usage: %s [flags] -- command [args...]\n", os.Args[0])
+ os.Exit(1)
+ }
+ target := flag.Arg(0)
+ args := flag.Args()[1:]
+
+ var rules []landlock.Rule
+ if len(roPaths) > 0 {
+ rules = append(rules, landlock.RODirs(roPaths...))
+ }
+ if len(roFilePaths) > 0 {
+ rules = append(rules, landlock.ROFiles(roFilePaths...))
+ }
+ if len(rwPaths) > 0 {
+ rules = append(rules, landlock.RWDirs(rwPaths...))
+ }
+ if len(rwFilePaths) > 0 {
+ rules = append(rules, landlock.RWFiles(rwFilePaths...))
+ }
+ if len(rules) > 0 {
+ if err := landlock.V5.BestEffort().RestrictPaths(rules...); err != nil {
+ log.Fatalf("failed to apply landlock: %v", err)
+ }
+ }
+
+ if len(rules) > 0 {
+ if err := landlock.V5.BestEffort().RestrictPaths(rules...); err != nil {
+ log.Fatalf("failed to apply landlock: %v", err)
+ }
+ }
+
+ fullPath, err := exec.LookPath(target)
+ if err != nil {
+ log.Fatalf("command not found: %v", err)
+ }
+
+ if err := syscall.Exec(fullPath, append([]string{target}, args...), os.Environ()); err != nil {
+ log.Fatalf("failed to exec target: %v", err)
+ }
+}
diff --git a/filed-launch.1.scd b/filed-launch.1.scd
@@ -0,0 +1,19 @@
+FILED-LAUNCH(1)
+
+# NAME
+
+filed-launch - launch programs with restricted access
+
+# SYNOPSIS
+
+*filed-launch* [-rw _dir_] [-ro _dir_] [-rwf _file_] [-rof _file_] -- _executable_
+
+# DESCRIPTION
+
+*filed-launch* is used by *filed*(5) for launching programs with restricted file
+access using *landlock*(7).
+
+If no arguments are applied, the _executable_ is launched with full access
+to the file system.
+
+
diff --git a/filed.5.scd b/filed.5.scd
@@ -14,6 +14,9 @@ filed (file d'attente) is a pseudo-filesystem which provides an inspectable
job queue that operates on files. It mounts a directory _mdir_, which is
where the user can add and inspect jobs.
+All jobs are executed using _filed-launch_(1), allowing you to restrict job
+accesses.
+
## Overview
Underneath _mdir_, there are the following groups of files and subdirectories:
@@ -60,7 +63,9 @@ principle of least access.
Importantly, the system is intended for only trusted scripts: the job user
has access to the state, and is thus able to rewrite access rights. It is
recommended for the running scripts to use *namespaces*(7) or *Landlock*(7) to
-drop further drop privileges.
+drop further privileges. You can also use *bwrap*(1) or *landrun* to run filed
+with dropped privileges, ensuring it can only access the desired files and
+directories.
Another aspect to be aware of is that File d'attente stores logs of all jobs.
Care should be taken to ensure that no secrets are printed.
@@ -99,6 +104,7 @@ Inspect a currently running job:
# SEE ALSO
- *filed.config*(5)
+- *filed-launch*(1)
- Periodic jobs can be set up using *cron*(8).
- Monitoring failures can be done with *watch*(1)
- Limiting job privileges can be done with *bwrap*(1) or *landrun*
diff --git a/go.mod b/go.mod
@@ -6,11 +6,13 @@ require (
bazil.org/fuse v0.0.0-20230120002735-62a210ff1fd5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.36.0 // indirect
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
diff --git a/go.sum b/go.sum
@@ -4,6 +4,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c h1:QcKqiunpt7hooa/xIx0iyepA6Cs2BgKexaYOxHvHNCs=
+github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c/go.mod h1:stwyhp9tfeEy3A4bRJLdOEvjW/CetRJg/vcijNG8M5A=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -17,6 +19,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
diff --git a/manager.go b/manager.go
@@ -4,16 +4,14 @@ import (
"bytes"
"context"
"filed/store"
+ "strings"
"fmt"
"log/slog"
"math"
- "os"
"os/exec"
"sync"
- "syscall"
"time"
- "unsafe"
)
type JobManager struct {
@@ -58,37 +56,6 @@ func (jm *JobManager) StartWorker(ctx context.Context) {
}
}()
}
-func (jm *JobManager) runBinaryFromMemory(id string, data []byte) ([]byte, error) {
- // 1. Create a "Memory File" (Linux only)
- // This creates a file descriptor that exists only in RAM.
- fdName := "qj_bin_" + id
- fd, _, err := syscall.Syscall(syscall.SYS_MEMFD_CREATE, uintptr(unsafe.Pointer(syscall.StringBytePtr(fdName))), 0, 0)
- if err != 0 {
- return nil, fmt.Errorf("memfd_create failed: %w", err)
- }
-
- // Convert FD to an *os.File
- f := os.NewFile(fd, fdName)
- defer f.Close()
-
- // 2. Write the binary data to the memory file
- if _, err := f.Write(data); err != nil {
- return nil, fmt.Errorf("failed to write binary to memory: %w", err)
- }
-
- // 3. Execute it using the /proc/self/fd path
- // Linux maps file descriptors into the filesystem under /proc/self/fd/
- cmdPath := fmt.Sprintf("/proc/self/fd/%d", fd)
- cmd := exec.Command(cmdPath)
-
- // Capture output just like before
- var output bytes.Buffer
- cmd.Stdout = &output
- cmd.Stderr = &output
-
- errRun := cmd.Run()
- return output.Bytes(), errRun
-}
func (jm *JobManager) processPendingJobs() {
conf := jm.store.GetConfig()
@@ -147,7 +114,10 @@ func (jm *JobManager) runJob(id, commandStr string) {
jm.activeJobs.Store(id, active)
defer jm.activeJobs.Delete(id)
- cmd := exec.CommandContext(ctx, "sh", "-c", commandStr)
+ args := strings.Fields(commandStr)
+
+ cmd := exec.CommandContext(ctx, "filed-launch", "--")
+ cmd.Args = append(cmd.Args, args...)
writer := &SafeBuffer{target: active}
cmd.Stdout = writer
diff --git a/store/jobs.go b/store/jobs.go
@@ -40,7 +40,7 @@ func NewStore(filepath string) (*Store, error) {
return nil, err
}
- if _, err := db.Exec("PRAGMA journal_mode=WAL;PRAGMA busy_timeout=5000;"); err != nil {
+ if _, err := db.Exec("PRAGMA journal_mode=WAL;PRAGMA busy_timeout=20000;"); err != nil {
return nil, err
}