From 042013bb04819f931e5ebd3d784e7282821833bd Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 6 Nov 2025 00:57:32 +0900 Subject: [PATCH] container/std: syscall JSON adapter This provides cross-platform JSON adapter for syscall number. Signed-off-by: Ophestra --- container/std/seccomp.go | 50 +++++++++++++++++++++++---- container/std/seccomp_test.go | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 container/std/seccomp_test.go diff --git a/container/std/seccomp.go b/container/std/seccomp.go index f3189ca..05bc426 100644 --- a/container/std/seccomp.go +++ b/container/std/seccomp.go @@ -1,5 +1,10 @@ package std +import ( + "encoding/json" + "strconv" +) + type ( // ScmpUint is equivalent to C.uint. ScmpUint uint32 @@ -19,20 +24,53 @@ type ( // ScmpArgCmp is equivalent to struct scmp_arg_cmp. ScmpArgCmp struct { // argument number, starting at 0 - Arg ScmpUint + Arg ScmpUint `json:"arg"` // the comparison op, e.g. SCMP_CMP_* - Op ScmpCompare + Op ScmpCompare `json:"op"` - DatumA, DatumB ScmpDatum + DatumA ScmpDatum `json:"a,omitempty"` + DatumB ScmpDatum `json:"b,omitempty"` } // A NativeRule specifies an arch-specific action taken by seccomp under certain conditions. NativeRule struct { // Syscall is the arch-dependent syscall number to act against. - Syscall ScmpSyscall + Syscall ScmpSyscall `json:"syscall"` // Errno is the errno value to return when the condition is satisfied. - Errno ScmpErrno + Errno ScmpErrno `json:"errno"` // Arg is the optional struct scmp_arg_cmp passed to libseccomp. - Arg *ScmpArgCmp + Arg *ScmpArgCmp `json:"arg,omitempty"` } ) + +// MarshalJSON resolves the name of [ScmpSyscall] and encodes it as a [json] string. +// If such a name does not exist, the syscall number is encoded instead. +func (num *ScmpSyscall) MarshalJSON() ([]byte, error) { + n := int(*num) + for name, cur := range Syscalls() { + if cur == n { + return json.Marshal(name) + } + } + return json.Marshal(n) +} + +// SyscallNameError is returned when trying to unmarshal an invalid syscall name into [ScmpSyscall]. +type SyscallNameError string + +func (e SyscallNameError) Error() string { return "invalid syscall name " + strconv.Quote(string(e)) } + +// UnmarshalJSON looks up the syscall number corresponding to name encoded in data +// by calling [SyscallResolveName]. +func (num *ScmpSyscall) UnmarshalJSON(data []byte) error { + var name string + if err := json.Unmarshal(data, &name); err != nil { + return err + } + if n, ok := SyscallResolveName(name); !ok { + return SyscallNameError(name) + } else { + *num = ScmpSyscall(n) + return nil + } +} diff --git a/container/std/seccomp_test.go b/container/std/seccomp_test.go new file mode 100644 index 0000000..91a718d --- /dev/null +++ b/container/std/seccomp_test.go @@ -0,0 +1,63 @@ +package std_test + +import ( + "encoding/json" + "errors" + "math" + "reflect" + "syscall" + "testing" + + "hakurei.app/container/std" +) + +func TestScmpSyscall(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + want std.ScmpSyscall + err error + }{ + {"select", `"select"`, syscall.SYS_SELECT, nil}, + {"clone3", `"clone3"`, std.SYS_CLONE3, nil}, + + {"oob", `-2147483647`, -math.MaxInt32, + &json.UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: 11}}, + {"name", `"nonexistent_syscall"`, -math.MaxInt32, + std.SyscallNameError("nonexistent_syscall")}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + t.Run("decode", func(t *testing.T) { + var got std.ScmpSyscall + if err := json.Unmarshal([]byte(tc.data), &got); !reflect.DeepEqual(err, tc.err) { + t.Fatalf("Unmarshal: error = %#v, want %#v", err, tc.err) + } else if err == nil && got != tc.want { + t.Errorf("Unmarshal: %v, want %v", got, tc.want) + } + }) + if errors.As(tc.err, new(std.SyscallNameError)) { + return + } + + t.Run("encode", func(t *testing.T) { + if got, err := json.Marshal(&tc.want); err != nil { + t.Fatalf("Marshal: error = %v", err) + } else if string(got) != tc.data { + t.Errorf("Marshal: %s, want %s", string(got), tc.data) + } + }) + }) + } + + t.Run("error", func(t *testing.T) { + const want = `invalid syscall name "\x00"` + if got := std.SyscallNameError("\x00").Error(); got != want { + t.Fatalf("Error: %q, want %q", got, want) + } + }) +}