aboutsummaryrefslogtreecommitdiffhomepage
path: root/system
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-09-08 03:22:10 +0900
committerOphestra <cat@gensokyo.uk>2025-09-08 03:22:10 +0900
commitda2b9c01ceac2d55c271de46d7d18e5b3f19b89b (patch)
tree94cf9dadbe7b49eef36b2b57b799840496b671f1 /system
parent323d132c40a74c5ac7a91731a0c43def2a8be7f1 (diff)
system/tmpfiles: do not fail for smaller files
The limit is meant to be an upper bound. Handle EOF and print verbose message for it instead of failing. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'system')
-rw-r--r--system/dispatcher.go14
-rw-r--r--system/dispatcher_test.go53
-rw-r--r--system/tmpfiles.go25
-rw-r--r--system/tmpfiles_test.go168
4 files changed, 218 insertions, 42 deletions
diff --git a/system/dispatcher.go b/system/dispatcher.go
index 038231bb..502c8e92 100644
--- a/system/dispatcher.go
+++ b/system/dispatcher.go
@@ -1,12 +1,20 @@
package system
import (
+ "io"
+ "io/fs"
"os"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
)
+type osFile interface {
+ Name() string
+ io.Writer
+ fs.File
+}
+
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
// syscallDispatcher is embedded in [I], so all methods must be unexported.
type syscallDispatcher interface {
@@ -15,6 +23,10 @@ type syscallDispatcher interface {
// just synchronising access is not enough, as this is for test instrumentation.
new(f func(k syscallDispatcher))
+ // stat provides os.Stat.
+ stat(name string) (os.FileInfo, error)
+ // open provides [os.Open].
+ open(name string) (osFile, error)
// mkdir provides os.Mkdir.
mkdir(name string, perm os.FileMode) error
// chmod provides os.Chmod.
@@ -48,6 +60,8 @@ type direct struct{}
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
+func (k direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
+func (k direct) open(name string) (osFile, error) { return os.Open(name) }
func (k direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
func (k direct) chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) }
func (k direct) link(oldname, newname string) error { return os.Link(oldname, newname) }
diff --git a/system/dispatcher_test.go b/system/dispatcher_test.go
index 34973879..f798047c 100644
--- a/system/dispatcher_test.go
+++ b/system/dispatcher_test.go
@@ -1,10 +1,13 @@
package system
import (
+ "io"
+ "io/fs"
"os"
"reflect"
"slices"
"testing"
+ "time"
"unsafe"
"hakurei.app/container/stub"
@@ -180,6 +183,34 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
})
}
+type stubFi struct {
+ size int64
+ isDir bool
+}
+
+func (stubFi) Name() string { panic("unreachable") }
+func (fi stubFi) Size() int64 { return fi.size }
+func (stubFi) Mode() fs.FileMode { panic("unreachable") }
+func (stubFi) ModTime() time.Time { panic("unreachable") }
+func (fi stubFi) IsDir() bool { return fi.isDir }
+func (stubFi) Sys() any { panic("unreachable") }
+
+type readerOsFile struct {
+ closed bool
+ io.Reader
+}
+
+func (*readerOsFile) Name() string { panic("unreachable") }
+func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
+func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
+func (r *readerOsFile) Close() error {
+ if r.closed {
+ return os.ErrClosed
+ }
+ r.closed = true
+ return nil
+}
+
// InternalNew initialises [I] with a stub syscallDispatcher.
func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) {
k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)
@@ -192,6 +223,28 @@ type kstub struct{ *stub.Stub[syscallDispatcher] }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
+func (k *kstub) stat(name string) (fi os.FileInfo, err error) {
+ k.Helper()
+ expect := k.Expects("stat")
+ err = expect.Error(
+ stub.CheckArg(k.Stub, "name", name, 0))
+ if err == nil {
+ fi = expect.Ret.(os.FileInfo)
+ }
+ return
+}
+
+func (k *kstub) open(name string) (f osFile, err error) {
+ k.Helper()
+ expect := k.Expects("open")
+ err = expect.Error(
+ stub.CheckArg(k.Stub, "name", name, 0))
+ if err == nil {
+ f = expect.Ret.(osFile)
+ }
+ return
+}
+
func (k *kstub) mkdir(name string, perm os.FileMode) error {
k.Helper()
return k.Expects("mkdir").Error(
diff --git a/system/tmpfiles.go b/system/tmpfiles.go
index bd865d44..d2a6c93a 100644
--- a/system/tmpfiles.go
+++ b/system/tmpfiles.go
@@ -28,15 +28,15 @@ type tmpfileOp struct {
func (t *tmpfileOp) Type() Enablement { return Process }
-func (t *tmpfileOp) apply(*I) error {
- msg.Verbose("copying", t)
-
+func (t *tmpfileOp) apply(sys *I) error {
if t.payload == nil {
- // this is a misuse of the API; do not return an error message
+ // this is a misuse of the API; do not return a wrapped error
return errors.New("invalid payload")
}
- if b, err := os.Stat(t.src); err != nil {
+ sys.verbose("copying", t)
+
+ if b, err := sys.stat(t.src); err != nil {
return newOpError("tmpfile", err, false)
} else {
if b.IsDir() {
@@ -47,9 +47,20 @@ func (t *tmpfileOp) apply(*I) error {
}
}
- if f, err := os.Open(t.src); err != nil {
+ var r io.ReadCloser
+ if f, err := sys.open(t.src); err != nil {
return newOpError("tmpfile", err, false)
- } else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
+ } else {
+ r = f
+ }
+ if n, err := io.CopyN(t.buf, r, t.n); err != nil {
+ if !errors.Is(err, io.EOF) {
+ _ = r.Close()
+ return newOpError("tmpfile", err, false)
+ }
+ sys.verbosef("copied %d bytes from %q", n, t.src)
+ }
+ if err := r.Close(); err != nil {
return newOpError("tmpfile", err, false)
}
diff --git a/system/tmpfiles_test.go b/system/tmpfiles_test.go
index a06242ee..5ee5e0d1 100644
--- a/system/tmpfiles_test.go
+++ b/system/tmpfiles_test.go
@@ -1,44 +1,142 @@
package system
import (
- "strconv"
+ "bytes"
+ "errors"
+ "os"
+ "strings"
+ "syscall"
"testing"
+
+ "hakurei.app/container/stub"
)
-func TestCopyFile(t *testing.T) {
- testCases := []struct {
- tcOp
- cap int
- n int64
- }{
- {tcOp{Process, "/home/ophestra/xdg/config/pulse/cookie"}, 256, 256},
- }
- for _, tc := range testCases {
- t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) {
- sys := New(t.Context(), 150)
- sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n)
- tc.test(t, sys.ops, []Op{
- &tmpfileOp{nil, tc.path, tc.n, nil},
- }, "CopyFile")
- })
- }
-}
+type errorReader struct{}
-func TestTmpfile_String(t *testing.T) {
- testCases := []struct {
- src string
- n int64
- want string
- }{
- {"/home/ophestra/xdg/config/pulse/cookie", 256,
- `up to 256 bytes from "/home/ophestra/xdg/config/pulse/cookie"`},
- }
+func (errorReader) Read([]byte) (int, error) { return 0, stub.UniqueError(0xdeadbeef) }
+
+func TestTmpfileOp(t *testing.T) {
+ // 255 bytes
+ const paSample = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+
+ checkOpBehaviour(t, []opBehaviourTestCase{
+ {"payload", 0xdead, 0xff, &tmpfileOp{
+ nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, nil, errors.New("invalid payload"), nil, nil},
+
+ {"stat", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(1)),
+ }, &OpError{Op: "tmpfile", Err: stub.UniqueError(1)}, nil, nil},
+
+ {"stat EISDIR", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, true}, nil),
+ }, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.EISDIR}}, nil, nil},
+
+ {"stat ENOMEM", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1<<8 + 1, false}, nil),
+ }, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.ENOMEM}}, nil, nil},
+
+ {"open", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
+ call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(0)),
+ }, &OpError{Op: "tmpfile", Err: stub.UniqueError(0)}, nil, nil},
+
+ {"reader", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
+ call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, errorReader{}}, nil),
+ }, &OpError{Op: "tmpfile", Err: stub.UniqueError(0xdeadbeef)}, nil, nil},
- for _, tc := range testCases {
- t.Run(tc.want, func(t *testing.T) {
- if got := (&tmpfileOp{src: tc.src, n: tc.n}).String(); got != tc.want {
- t.Errorf("String() = %v, want %v", got, tc.want)
- }
- })
- }
+ {"closed", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
+ call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, strings.NewReader(paSample + "=")}, nil),
+ }, &OpError{Op: "tmpfile", Err: os.ErrClosed}, nil, nil},
+
+ {"success full", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
+ call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample + "=")}, nil),
+ }, nil, nil, nil},
+
+ {"success", 0xdead, 0xff, &tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }, []stub.Call{
+ call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil),
+ call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil),
+ call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample)}, nil),
+ call("verbosef", stub.ExpectArgs{"copied %d bytes from %q", []any{int64(1<<8 - 1), "/home/ophestra/xdg/config/pulse/cookie"}}, nil, nil),
+ }, nil, nil, nil},
+ })
+
+ checkOpsBuilder(t, "CopyFile", []opsBuilderTestCase{
+ {"pulse", 0xcafebabe, func(_ *testing.T, sys *I) {
+ sys.CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1<<8, 1<<8)
+ }, []Op{&tmpfileOp{
+ new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8,
+ func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(),
+ }}, stub.Expect{}},
+ })
+
+ checkOpIs(t, []opIsTestCase{
+ {"nil", (*tmpfileOp)(nil), (*tmpfileOp)(nil), false},
+ {"zero", new(tmpfileOp), new(tmpfileOp), true},
+
+ {"n differs", &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse/cookie",
+ n: 1 << 7,
+ }, &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse/cookie",
+ n: 1 << 8,
+ }, false},
+
+ {"src differs", &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse",
+ n: 1 << 8,
+ }, &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse/cookie",
+ n: 1 << 8,
+ }, false},
+
+ {"equals", &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse/cookie",
+ n: 1 << 8,
+ }, &tmpfileOp{
+ src: "/home/ophestra/xdg/config/pulse/cookie",
+ n: 1 << 8,
+ }, true},
+ })
+
+ checkOpMeta(t, []opMetaTestCase{
+ {"pulse", &tmpfileOp{nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, nil},
+ Process, "/home/ophestra/xdg/config/pulse/cookie",
+ `up to 256 bytes from "/home/ophestra/xdg/config/pulse/cookie"`},
+ })
}