-
Notifications
You must be signed in to change notification settings - Fork 16
seccomp: Use an allow-list for socket domains #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,134 @@ | |
| package seccomp | ||
|
|
||
| import ( | ||
| "slices" | ||
|
|
||
| "github.com/opencontainers/runtime-spec/specs-go" | ||
| "golang.org/x/sys/unix" | ||
| ) | ||
|
|
||
| // The socket rules in DefaultProfile rely on AF_ALG and AF_VSOCK being | ||
| // exactly two apart (38 and 40), with a single family (39) between them. | ||
| var ( | ||
| _ [38]byte = [unix.AF_ALG]byte{} | ||
| _ [40]byte = [unix.AF_VSOCK]byte{} | ||
| _ [1]byte = [unix.AF_VSOCK - unix.AF_ALG - 1]byte{} | ||
| ) | ||
| // These are the Linux socket domains currently defined by the UAPI: | ||
| // https://github.com/torvalds/linux/blob/master/include/linux/socket.h | ||
| // AF_UNSPEC is not a creatable domain, AF_MAX is a sentinel, and AF_LOCAL/AF_FILE | ||
| // and AF_ROUTE are aliases of AF_UNIX and AF_NETLINK respectively. | ||
| var allowedSocketDomains = []uint64{ | ||
| unix.AF_UNIX, | ||
| unix.AF_INET, | ||
| unix.AF_AX25, | ||
| unix.AF_IPX, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Who still uses IPX, AppleTalk, etc. ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IPX was actually removed in 2018
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that would be the next step for me. The current state of this PR is to just switch to allow-list without behavior change yet. |
||
| unix.AF_APPLETALK, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed from the kernel recently |
||
| unix.AF_NETROM, | ||
| unix.AF_BRIDGE, | ||
| unix.AF_ATMPVC, | ||
| unix.AF_X25, | ||
| unix.AF_INET6, | ||
| unix.AF_ROSE, | ||
| unix.AF_DECnet, | ||
| unix.AF_NETBEUI, | ||
| unix.AF_SECURITY, | ||
| unix.AF_KEY, | ||
| unix.AF_NETLINK, | ||
| unix.AF_PACKET, | ||
| unix.AF_ASH, | ||
| unix.AF_ECONET, | ||
| unix.AF_ATMSVC, | ||
| unix.AF_RDS, | ||
| unix.AF_SNA, | ||
| unix.AF_IRDA, | ||
| unix.AF_PPPOX, | ||
| unix.AF_WANPIPE, | ||
| unix.AF_LLC, | ||
| unix.AF_IB, | ||
| unix.AF_MPLS, | ||
| unix.AF_CAN, | ||
| unix.AF_TIPC, | ||
| unix.AF_BLUETOOTH, | ||
| unix.AF_IUCV, | ||
| unix.AF_RXRPC, | ||
| unix.AF_ISDN, | ||
| unix.AF_PHONET, | ||
| unix.AF_IEEE802154, | ||
| unix.AF_CAIF, | ||
|
|
||
| // AF_ALG gives userspace direct access to the kernel cryptography API. The | ||
| // vulnerabilities demonstrated by https://copy.fail/ have been fixed, but | ||
| // general-purpose containers have no practical need for this interface. | ||
| // Keep it blocked to avoid exposing an unnecessary kernel attack surface. | ||
| // | ||
| // unix.AF_ALG, | ||
|
|
||
| unix.AF_NFC, | ||
|
|
||
| // AF_VSOCK provides host/guest communication. Before Linux 7.0 it was global | ||
| // across network namespaces, allowing a container to reach any visible VM by | ||
| // CID. Linux 7.0 added opt-in namespace isolation for vhost-vsock and | ||
| // loopback, but global mode remains the default. Keep it blocked for older | ||
| // kernels and default-global configurations; intentional users can provide a | ||
| // custom seccomp profile. See https://docs.kernel.org/admin-guide/sysctl/net.html. | ||
| // | ||
| // unix.AF_VSOCK, | ||
|
|
||
| unix.AF_KCM, | ||
| unix.AF_QIPCRTR, | ||
| unix.AF_SMC, | ||
| unix.AF_XDP, | ||
| unix.AF_MCTP, | ||
| } | ||
|
|
||
| func socketSyscalls() []*Syscall { | ||
| // Keep range detection independent of the declaration order above. | ||
| s := slices.Clone(allowedSocketDomains) | ||
| slices.Sort(s) | ||
| return socketSyscallsForDomains(s) | ||
| } | ||
|
vvoland marked this conversation as resolved.
|
||
|
|
||
| func socketSyscallsForDomains(domains []uint64) []*Syscall { | ||
| syscalls := make([]*Syscall, 0, len(domains)) | ||
| // runc treats repeated comparisons for one argument as separate OR rules, | ||
| // so bounded ranges cannot use both a lower and an upper comparison. | ||
| // See https://github.com/opencontainers/runc/issues/2735. | ||
| // | ||
| // A one-sided range starting at AF_UNIX is safe because AF_UNSPEC is not a | ||
| // creatable domain. Collapse it only when doing so removes equality rules. | ||
| rangeEnd := 0 | ||
| if len(domains) > 1 && domains[0] == unix.AF_UNIX && domains[1] == domains[0]+1 { | ||
| // The first domains are represented by the range instead of equalities. | ||
| rangeEnd = 2 | ||
| lastDomain := domains[1] | ||
| for _, domain := range domains[2:] { | ||
| // Stop at the first gap so the range does not include a blocked domain. | ||
| if domain != lastDomain+1 { | ||
| break | ||
| } | ||
| lastDomain = domain | ||
| rangeEnd++ | ||
| } | ||
| // OpLessThan is exclusive, so compare with the value after the range. | ||
| syscalls = append(syscalls, socketSyscall(lastDomain+1, specs.OpLessThan)) | ||
| } | ||
|
|
||
| // Every later run is bounded, so preserve it with one equality per domain. | ||
| for _, domain := range domains[rangeEnd:] { | ||
| syscalls = append(syscalls, socketSyscall(domain, specs.OpEqualTo)) | ||
| } | ||
| return syscalls | ||
| } | ||
|
|
||
| func socketSyscall(value uint64, op specs.LinuxSeccompOperator) *Syscall { | ||
| return &Syscall{ | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"socket"}, | ||
| Action: specs.ActAllow, | ||
| Args: []specs.LinuxSeccompArg{ | ||
| { | ||
| Index: 0, | ||
| Value: value, | ||
| Op: op, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func arches() []Architecture { | ||
| return []Architecture{ | ||
|
|
@@ -442,51 +559,14 @@ func DefaultProfile() *Seccomp { | |
| MinKernel: &KernelVersion{4, 8}, | ||
| }, | ||
| }, | ||
| // Allow socket(2) for all address families except AF_VSOCK and AF_ALG. | ||
| // NOTE: on 32-bit x86, socket() goes through socketcall(2) which is | ||
| // allowed unconditionally above, so AF_VSOCK/AF_ALG is still reachable | ||
| // via the socketcall-based socket() path. These arg filters only apply | ||
| // to the direct socket syscall, and do not protect 32-bit x86 unless | ||
| // socketcall(2) is also addressed. | ||
| { | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"socket"}, | ||
| Action: specs.ActAllow, | ||
| Args: []specs.LinuxSeccompArg{ | ||
| { | ||
| Index: 0, | ||
| Value: unix.AF_ALG, | ||
| Op: specs.OpLessThan, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"socket"}, | ||
| Action: specs.ActAllow, | ||
| Args: []specs.LinuxSeccompArg{ | ||
| { | ||
| Index: 0, | ||
| Value: unix.AF_ALG + 1, | ||
| Op: specs.OpEqualTo, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"socket"}, | ||
| Action: specs.ActAllow, | ||
| Args: []specs.LinuxSeccompArg{ | ||
| { | ||
| Index: 0, | ||
| Value: unix.AF_VSOCK, | ||
| Op: specs.OpGreaterThan, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| // Allow socket(2) for the address families listed in allowedSocketDomains. | ||
| // On ABIs that use socketcall(2), the socket arguments are behind a pointer | ||
| // and cannot be filtered by seccomp. Because socketcall(2) is allowed above, | ||
| // this domain allow-list applies only to the direct socket syscall. | ||
| syscalls = append(syscalls, socketSyscalls()...) | ||
| syscalls = append(syscalls, []*Syscall{ | ||
| { | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"personality"}, | ||
|
|
@@ -874,7 +954,7 @@ func DefaultProfile() *Seccomp { | |
| Caps: []string{"CAP_PERFMON"}, | ||
| }, | ||
| }, | ||
| } | ||
| }...) | ||
|
|
||
| errnoRet := uint(unix.EPERM) | ||
| return &Seccomp{ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // SPDX-FileCopyrightText: Copyright The Moby Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package seccomp | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/opencontainers/runtime-spec/specs-go" | ||
| "golang.org/x/sys/unix" | ||
| ) | ||
|
|
||
| func socketTestSyscall(args ...specs.LinuxSeccompArg) *Syscall { | ||
| return &Syscall{ | ||
| LinuxSyscall: specs.LinuxSyscall{ | ||
| Names: []string{"socket"}, | ||
| Action: specs.ActAllow, | ||
| Args: args, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func TestSocketSyscallsForDomains(t *testing.T) { | ||
| fixtures := []struct { | ||
| name string | ||
| domains []uint64 | ||
| want []*Syscall | ||
| }{ | ||
| { | ||
| name: "singleton", | ||
| domains: []uint64{39}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 39, Op: specs.OpEqualTo}), | ||
| }, | ||
| }, | ||
| { | ||
| name: "later consecutive run", | ||
| domains: []uint64{1, 2, 4, 5}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 3, Op: specs.OpLessThan}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 4, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 5, Op: specs.OpEqualTo}), | ||
| }, | ||
| }, | ||
| { | ||
| name: "multiple gaps", | ||
| domains: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 16, 20}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 11, Op: specs.OpLessThan}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 12, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 13, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 14, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 16, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 20, Op: specs.OpEqualTo}), | ||
| }, | ||
| }, | ||
| { | ||
| name: "two-domain initial range", | ||
| domains: []uint64{1, 2}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 3, Op: specs.OpLessThan}), | ||
| }, | ||
| }, | ||
| { | ||
| name: "gap after initial domain", | ||
| domains: []uint64{1, 3, 4}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 1, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 3, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 4, Op: specs.OpEqualTo}), | ||
| }, | ||
| }, | ||
| { | ||
| name: "range not at initial domain", | ||
| domains: []uint64{10, 11, 12}, | ||
| want: []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 10, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 11, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: 12, Op: specs.OpEqualTo}), | ||
| }, | ||
| }, | ||
|
thaJeztah marked this conversation as resolved.
|
||
| } | ||
|
|
||
| for _, test := range fixtures { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| assertDeepEqual(t, test.want, socketSyscallsForDomains(test.domains)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestDefaultSyscalls(t *testing.T) { | ||
| want := []*Syscall{ | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_ALG, Op: specs.OpLessThan}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_NFC, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_KCM, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_QIPCRTR, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_SMC, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_XDP, Op: specs.OpEqualTo}), | ||
| socketTestSyscall(specs.LinuxSeccompArg{Index: 0, Value: unix.AF_MCTP, Op: specs.OpEqualTo}), | ||
| } | ||
|
|
||
| assertDeepEqual(t, want, socketSyscalls()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: AX25 was removed from the kernel
https://daily.hamweekly.com/2026/04/linux-kernel-removes-ax25-hamradio-subsystem-support/