From 821f8a75c37a67b04e58e3a7a98d33aafc98a7dc Mon Sep 17 00:00:00 2001 From: Andrii Date: Fri, 30 Oct 2026 00:29:38 +0000 Subject: [PATCH] Linux: allowUnixSocketCreation keeps the seccomp helper network.allowAllUnixSockets is the only way to let a sandboxed command create Unix sockets on Linux, and it removes apply-seccomp altogether. Besides AF_UNIX, that drops the AF_VSOCK and io_uring blocks, the helper's PID namespace, the namespaces filter and write-violation monitoring. allowUnixSocketCreation keeps the helper and leaves out only the AF_UNIX rule. The generator gains a `vsock` rule set (`unix` without AF_UNIX), which apply-seccomp installs in place of `unix` when SRT_ALLOW_UNIX_SOCKETS=1. The variable is set and cleared the same way as SRT_ALLOW_NESTED_USERNS. The `unix` and `namespaces` filters are byte-for-byte unchanged. A sandbox nested inside srt needs this: srt itself, or Claude Code with its sandbox on. The inner srt creates the Unix sockets its proxies are bridged through. Co-Authored-By: Claude Opus 5.5 --- README.md | 13 +- src/sandbox/linux-sandbox-utils.ts | 50 +++-- src/sandbox/sandbox-config.ts | 11 + src/sandbox/sandbox-manager.ts | 5 + test/config-validation.test.ts | 21 ++ .../seccomp-unix-socket-creation.test.ts | 197 ++++++++++++++++++ vendor/seccomp-src/apply-seccomp.c | 23 +- vendor/seccomp-src/seccomp-unix-block.c | 27 ++- vendor/seccomp/build.ts | 12 +- 9 files changed, 321 insertions(+), 38 deletions(-) create mode 100644 test/sandbox/seccomp-unix-socket-creation.test.ts diff --git a/README.md b/README.md index 61714c9a1..4773ee913 100644 --- a/README.md +++ b/README.md @@ -512,15 +512,16 @@ While `filterRequest` is set, `TRACE` and `TRACK` requests are answered 405 befo **Unix Socket Settings** (platform-specific behavior): -| Setting | macOS | Linux | -| ------------------------------ | ------------------------- | ---------------------------------------- | -| `allowUnixSockets: string[]` | Allowlist of socket paths | _Ignored_ (seccomp can't filter by path) | -| `allowAllUnixSockets: boolean` | Allow all sockets | Disable seccomp blocking | +| Setting | macOS | Linux | +| ---------------------------------- | ------------------------- | ------------------------------------------------------ | +| `allowUnixSockets: string[]` | Allowlist of socket paths | _Ignored_ (seccomp can't filter by path) | +| `allowAllUnixSockets: boolean` | Allow all sockets | Disable seccomp blocking | +| `allowUnixSocketCreation: boolean` | _Ignored_ | Allow all sockets, keep the rest of the seccomp helper | Unix sockets are **blocked by default** on both platforms. - **macOS**: Use `allowUnixSockets` to allow specific paths (e.g., `["/var/run/docker.sock"]`), or `allowAllUnixSockets: true` to allow all. -- **Linux**: Blocking uses seccomp filters (x64/arm64 only). If seccomp isn't available, sockets are unrestricted and a warning is shown. Use `allowAllUnixSockets: true` to explicitly disable blocking. +- **Linux**: Blocking uses seccomp filters (x64/arm64 only). If seccomp isn't available, sockets are unrestricted and a warning is shown. Use `allowAllUnixSockets: true` to explicitly disable blocking. That removes the seccomp helper and everything else it does: VM sockets (`AF_VSOCK`) and io_uring become usable, and the PID namespace isolation, the namespaces filter and violation monitoring are gone. To allow Unix sockets and keep all of those, use `allowUnixSocketCreation: true` instead. Either way, every socket the command can reach in the filesystem becomes usable (see [Security Limitations](#security-limitations)). #### Filesystem Configuration @@ -1211,7 +1212,7 @@ On Linux, the sandbox uses **seccomp BPF (Berkeley Packet Filter)** to block Uni **PID namespace isolation**: The nested PID namespace ensures the user command cannot see or address any process that runs without the seccomp filter (bwrap's init, the shell wrapper, or the socat helpers). This keeps the seccomp boundary intact regardless of `kernel.yama.ptrace_scope`, since unfiltered helpers are not reachable via `ptrace` or `/proc/N/mem`. The inner PID 1 sets `PR_SET_DUMPABLE=0` so it is not ptraceable either. If nested namespace creation fails, apply-seccomp aborts rather than running without isolation. -**Security limitations**: The filter blocks `socket(AF_UNIX, ...)` and the `io_uring_setup`/`io_uring_enter`/`io_uring_register` syscalls (the latter three because `IORING_OP_SOCKET` on Linux 5.19+ would otherwise bypass the `socket()` rule). It does not prevent operations on Unix socket file descriptors inherited from parent processes or passed via `SCM_RIGHTS`. For most sandboxing scenarios, blocking socket creation is sufficient to prevent unauthorized IPC. +**Security limitations**: The filter blocks `socket(AF_UNIX, ...)` and the `io_uring_setup`/`io_uring_enter`/`io_uring_register` syscalls (the latter three because `IORING_OP_SOCKET` on Linux 5.19+ would otherwise bypass the `socket()` rule). It does not prevent operations on Unix socket file descriptors inherited from parent processes or passed via `SCM_RIGHTS`. For most sandboxing scenarios, blocking socket creation is sufficient to prevent unauthorized IPC. Under `allowUnixSocketCreation` the helper applies the same filter without the `AF_UNIX` rule. **Zero runtime dependencies**: Pre-built static apply-seccomp binaries and pre-generated BPF filters are included for x64 and arm64 architectures. No compilation tools or external dependencies required at runtime. diff --git a/src/sandbox/linux-sandbox-utils.ts b/src/sandbox/linux-sandbox-utils.ts index 66f9db747..f63efaa14 100644 --- a/src/sandbox/linux-sandbox-utils.ts +++ b/src/sandbox/linux-sandbox-utils.ts @@ -80,6 +80,8 @@ export interface LinuxSandboxParams { /** Lets the command undo the write denies: see {@link NESTED_USERNS_ENV}. */ allowNestedUserNamespaces?: boolean allowAllUnixSockets?: boolean + /** Unix sockets under the helper: see {@link UNIX_SOCKETS_ENV}. */ + allowUnixSocketCreation?: boolean binShell?: string ripgrepConfig?: RipgrepConfig /** Maximum directory depth to search for dangerous files (default: 3) */ @@ -1676,13 +1678,27 @@ export async function initializeLinuxNetworkBridge( const NESTED_USERNS_ENV = 'SRT_ALLOW_NESTED_USERNS' /** - * The assignment the helper's command line starts with, so that the variable - * holds what the configuration says: the wrap reaches the helper through a - * shell, whose start-up files (BASH_ENV) could set it after bubblewrap cleared - * it, and an assignment before a command is made after those. + * Read by the seccomp helper, never by this process. `1` swaps its Unix-socket + * filter for one without the AF_UNIX rule, so the command can make Unix + * sockets while VM sockets and io_uring stay refused and the rest of what the + * helper does is kept. allowAllUnixSockets instead runs no helper at all. */ -function helperEnvironmentPrefix(allowNestedUserNamespaces: boolean): string { - return `${NESTED_USERNS_ENV}=${allowNestedUserNamespaces ? '1' : '0'} ` +const UNIX_SOCKETS_ENV = 'SRT_ALLOW_UNIX_SOCKETS' + +/** + * The assignments the helper's command line starts with, so that the variables + * hold what the configuration says: the wrap reaches the helper through a + * shell, whose start-up files (BASH_ENV) could set them after bubblewrap + * cleared them, and an assignment before a command is made after those. + */ +function helperEnvironmentPrefix( + allowNestedUserNamespaces: boolean, + allowUnixSocketCreation: boolean, +): string { + return ( + `${UNIX_SOCKETS_ENV}=${allowUnixSocketCreation ? '1' : '0'} ` + + `${NESTED_USERNS_ENV}=${allowNestedUserNamespaces ? '1' : '0'} ` + ) } // Keyed by path, like the uid-0 probe. Only an answer is kept: a probe that @@ -3550,6 +3566,7 @@ export async function wrapCommandWithSandboxLinux( enableWeakerNestedSandbox, allowNestedUserNamespaces, allowAllUnixSockets, + allowUnixSocketCreation, binShell, ripgrepConfig = { command: 'rg' }, mandatoryDenySearchDepth = DEFAULT_MANDATORY_DENY_SEARCH_DEPTH, @@ -3614,7 +3631,8 @@ export async function wrapCommandWithSandboxLinux( try { // ========== SECCOMP FILTER (Unix Socket Blocking) ========== // apply-seccomp wraps the workload and applies the baked-in BPF filter - // that blocks socket(AF_UNIX, ...). Skipped when allowAllUnixSockets is true. + // that blocks socket(AF_UNIX, ...). Skipped when allowAllUnixSockets is true; + // allowUnixSocketCreation keeps it, without the AF_UNIX rule. if (!allowAllUnixSockets) { applySeccompPrefix = resolveApplySeccompPrefix( seccompConfig?.applyPath, @@ -3629,10 +3647,14 @@ export async function wrapCommandWithSandboxLinux( ) } else { applySeccompPrefix = - helperEnvironmentPrefix(allowNestedUserNamespaces === true) + - applySeccompPrefix + helperEnvironmentPrefix( + allowNestedUserNamespaces === true, + allowUnixSocketCreation === true, + ) + applySeccompPrefix logForDebugging( - '[Sandbox Linux] Applying seccomp filter for Unix socket blocking', + allowUnixSocketCreation === true + ? '[Sandbox Linux] Applying seccomp filter - Unix sockets allowed (allowUnixSocketCreation)' + : '[Sandbox Linux] Applying seccomp filter for Unix socket blocking', ) } } else { @@ -3839,10 +3861,12 @@ export async function wrapCommandWithSandboxLinux( bwrapArgs.push('--bind', '/proc', '/proc') } - // The caller's environment decides nothing about the command's namespaces: - // the variable is cleared after every other environment operation, which - // bubblewrap applies in argument order, and helperEnvironmentPrefix sets it. + // The caller's environment decides nothing about the command's namespaces + // or Unix sockets: the variables are cleared after every other environment + // operation, which bubblewrap applies in argument order, and + // helperEnvironmentPrefix sets them. bwrapArgs.push('--unsetenv', NESTED_USERNS_ENV) + bwrapArgs.push('--unsetenv', UNIX_SOCKETS_ENV) // With no helper to refuse user namespaces, bubblewrap is asked to. Never // beside the helper, which makes one of its own, and not under diff --git a/src/sandbox/sandbox-config.ts b/src/sandbox/sandbox-config.ts index 3c4f96974..30de70d78 100644 --- a/src/sandbox/sandbox-config.ts +++ b/src/sandbox/sandbox-config.ts @@ -723,6 +723,17 @@ export const NetworkConfigSchema = z.object({ .describe( 'If true, allow all Unix sockets (disables blocking on both platforms). On Linux this also unblocks VM sockets (AF_VSOCK).', ), + allowUnixSocketCreation: z + .boolean() + .optional() + .describe( + 'Linux only: let the sandboxed command create Unix sockets while the seccomp helper stays in ' + + 'place, so that VM sockets (AF_VSOCK) and io_uring stay blocked and the PID namespace, the ' + + 'namespaces filter and violation monitoring are kept. allowAllUnixSockets removes the helper ' + + 'and all of these with it. As with allowAllUnixSockets, every Unix socket the command can ' + + 'reach in the filesystem (Docker, the D-Bus session bus, ssh-agent) becomes usable. ' + + 'Ignored when allowAllUnixSockets is true. Default: false.', + ), allowLocalBinding: z .boolean() .optional() diff --git a/src/sandbox/sandbox-manager.ts b/src/sandbox/sandbox-manager.ts index 6b6e0197a..de91a93ec 100644 --- a/src/sandbox/sandbox-manager.ts +++ b/src/sandbox/sandbox-manager.ts @@ -2220,6 +2220,10 @@ function getAllowAllUnixSockets(): boolean | undefined { return config?.network?.allowAllUnixSockets } +function getAllowUnixSocketCreation(): boolean | undefined { + return config?.network?.allowUnixSocketCreation +} + function getAllowLocalBinding(): boolean | undefined { return config?.network?.allowLocalBinding } @@ -2744,6 +2748,7 @@ async function wrapWithSandboxOnce( enableWeakerNestedSandbox: getEnableWeakerNestedSandbox(), allowNestedUserNamespaces, allowAllUnixSockets: getAllowAllUnixSockets(), + allowUnixSocketCreation: getAllowUnixSocketCreation(), binShell, ripgrepConfig: getRipgrepConfig(), mandatoryDenySearchDepth: getMandatoryDenySearchDepth(), diff --git a/test/config-validation.test.ts b/test/config-validation.test.ts index c0f353325..fbc5bd35b 100644 --- a/test/config-validation.test.ts +++ b/test/config-validation.test.ts @@ -138,6 +138,27 @@ describe('Config Validation', () => { ).toBe(false) }) + test('network.allowUnixSocketCreation is a boolean, and absent unless given', () => { + const filesystem = { denyRead: [], allowWrite: [], denyWrite: [] } + const network = { allowedDomains: [], deniedDomains: [] } + expect( + SandboxRuntimeConfigSchema.safeParse({ network, filesystem }).data + ?.network.allowUnixSocketCreation, + ).toBeUndefined() + expect( + SandboxRuntimeConfigSchema.safeParse({ + network: { ...network, allowUnixSocketCreation: true }, + filesystem, + }).data?.network.allowUnixSocketCreation, + ).toBe(true) + expect( + SandboxRuntimeConfigSchema.safeParse({ + network: { ...network, allowUnixSocketCreation: 'yes' }, + filesystem, + }).success, + ).toBe(false) + }) + test('should reject missing required fields', () => { const config = { network: { diff --git a/test/sandbox/seccomp-unix-socket-creation.test.ts b/test/sandbox/seccomp-unix-socket-creation.test.ts new file mode 100644 index 000000000..36ca3cb38 --- /dev/null +++ b/test/sandbox/seccomp-unix-socket-creation.test.ts @@ -0,0 +1,197 @@ +import { afterAll, beforeAll, describe, expect, it } from 'bun:test' +import { spawnSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { getApplySeccompBinaryPath } from '../../src/sandbox/generate-seccomp-filter.js' +import { + checkLinuxDependencies, + wrapCommandWithSandboxLinux, + type LinuxSandboxParams, +} from '../../src/sandbox/linux-sandbox-utils.js' +import { startLinuxSandboxViolationMonitor } from '../../src/sandbox/linux-violation-monitor.js' +import { SandboxManager } from '../../src/sandbox/sandbox-manager.js' +import { sanitizeUnregisteredCommandKey } from '../../src/sandbox/sandbox-violation-store.js' +import { quote } from '../../src/utils/shell-quote.js' +import { bwrapCanNamespaceNetwork } from '../helpers/bwrap-namespace.js' +import { isLinux } from '../helpers/platform.js' +import { spawnAsync } from '../helpers/spawn.js' + +/** + * allowUnixSocketCreation keeps the seccomp helper and leaves only its AF_UNIX + * rule out. Each assertion is on the errno, as in seccomp-vsock-block.test.ts, + * and INIT names PID 1 of the command's PID namespace: the helper when it is + * in the chain, bubblewrap when it is not. + */ +const PROBE = [ + 'import ctypes, errno, os, socket', + 'def attempt(label, family, kind):', + ' try:', + ' socket.socket(family, kind).close()', + ' print(label + "=OK")', + ' except OSError as e:', + ' print(label + "=" + errno.errorcode.get(e.errno, str(e.errno)))', + 'attempt("UNIX", socket.AF_UNIX, socket.SOCK_STREAM)', + 'attempt("UNIX_DGRAM", socket.AF_UNIX, socket.SOCK_DGRAM)', + 'attempt("VSOCK", socket.AF_VSOCK, socket.SOCK_STREAM)', + // io_uring_setup(4, ¶ms): 425 on both x86_64 and aarch64. + 'libc = ctypes.CDLL(None, use_errno=True)', + 'params = ctypes.create_string_buffer(120)', + 'fd = libc.syscall(ctypes.c_long(425), ctypes.c_uint(4), params)', + 'print("IO_URING=" + ("OK" if fd >= 0 else errno.errorcode.get(ctypes.get_errno(), "?")))', + 'print("ENV=" + os.environ.get("SRT_ALLOW_UNIX_SOCKETS", "unset"))', + 'print("INIT=" + open("/proc/1/comm").read().strip())', +].join('\n') + +const ALLOWED = + 'UNIX=OK\nUNIX_DGRAM=OK\nVSOCK=EPERM\nIO_URING=EPERM\nENV=unset\n' +const REFUSED = + 'UNIX=EPERM\nUNIX_DGRAM=EPERM\nVSOCK=EPERM\nIO_URING=EPERM\nENV=unset\n' + +describe.if(isLinux)('apply-seccomp with SRT_ALLOW_UNIX_SOCKETS', () => { + let applySeccomp: string + + const underHelper = (value: string | undefined) => { + const env = { ...process.env } + delete env.SRT_ALLOW_UNIX_SOCKETS + if (value !== undefined) env.SRT_ALLOW_UNIX_SOCKETS = value + return spawnSync(applySeccomp, ['python3', '-c', PROBE], { + encoding: 'utf8', + env, + timeout: 10000, + }) + } + + beforeAll(() => { + applySeccomp = getApplySeccompBinaryPath()! + expect(applySeccomp).toBeTruthy() + }) + + it('with 1, allows Unix sockets and keeps VM sockets and io_uring refused', () => { + const r = underHelper('1') + expect(r.stdout).toContain(ALLOWED) + expect(r.stdout).toContain('INIT=apply-seccomp') + expect(r.status).toBe(0) + }) + + it('with any other value, or none, refuses Unix sockets as before', () => { + for (const value of [undefined, '0', '', 'yes', 'true', ' 1']) { + const r = underHelper(value) + expect(r.stdout).toContain(REFUSED) + expect(r.status).toBe(0) + } + }) +}) + +describe.if(isLinux)('allowUnixSocketCreation in the Linux sandbox', () => { + const wrap = (params: Partial) => + wrapCommandWithSandboxLinux({ + command: quote(['python3', '-c', PROBE]), + needsNetworkRestriction: false, + writeConfig: { allowOnly: ['/tmp'], denyWithinAllow: [] }, + ...params, + }) + + const run = (wrapped: string) => + spawnAsync(wrapped, { shell: true, encoding: 'utf8', timeout: 15000 }) + + beforeAll(async () => { + expect(checkLinuxDependencies().errors).toEqual([]) + await SandboxManager.reset() + }) + + afterAll(async () => { + await SandboxManager.reset() + }) + + it('tells the helper what the configuration says, whatever was inherited', async () => { + expect(await wrap({ allowUnixSocketCreation: true })).toContain( + 'SRT_ALLOW_UNIX_SOCKETS=1 SRT_ALLOW_NESTED_USERNS=0 ', + ) + expect(await wrap({})).toContain( + 'SRT_ALLOW_UNIX_SOCKETS=0 SRT_ALLOW_NESTED_USERNS=0 ', + ) + expect(await wrap({})).toContain('--unsetenv SRT_ALLOW_UNIX_SOCKETS') + }) + + it('lets the command make Unix sockets, with the helper still in the chain', async () => { + const r = await run(await wrap({ allowUnixSocketCreation: true })) + expect(r.stdout).toContain(ALLOWED) + expect(r.stdout).toContain('INIT=apply-seccomp') + expect(r.status).toBe(0) + }) + + it('without it, refuses them as before', async () => { + const r = await run(await wrap({})) + expect(r.stdout).toContain(REFUSED) + expect(r.stdout).toContain('INIT=apply-seccomp') + expect(r.status).toBe(0) + }) + + it('is ignored beside allowAllUnixSockets, which runs no helper', async () => { + const wrapped = await wrap({ + allowAllUnixSockets: true, + allowUnixSocketCreation: true, + }) + expect(wrapped).not.toContain('SRT_ALLOW_UNIX_SOCKETS=1') + const r = await run(wrapped) + expect(r.stdout).toContain('UNIX=OK') + expect(r.stdout).not.toContain('INIT=apply-seccomp') + expect(r.status).toBe(0) + }) + + it('still reports blocked writes', async () => { + const work = mkdtempSync(join(tmpdir(), 'srt-unix-')) + const allow = join(work, 'rw') + const deny = join(work, 'ro') + mkdirSync(allow) + mkdirSync(deny) + const lines: string[] = [] + const monitor = startLinuxSandboxViolationMonitor(v => lines.push(v.line), { + allowWritePaths: [allow, '/dev'], + denyWritePaths: [deny], + resolveCommandText: sanitizeUnregisteredCommandKey, + }) + await monitor.ready + try { + const wrapped = await wrap({ + command: `echo a > ${quote([`${allow}/ok`])}; echo b > ${quote([`${deny}/bad`])}`, + writeConfig: { allowOnly: [allow], denyWithinAllow: [] }, + allowUnixSocketCreation: true, + observeSocketPath: monitor.observeSocketPath, + }) + await run(wrapped) + await new Promise(r => setTimeout(r, 100)) + expect(lines).toContain(`deny openat ${deny}/bad`) + expect(lines.some(l => l.includes(`${allow}/ok`))).toBe(false) + } finally { + monitor.stop() + rmSync(work, { recursive: true, force: true }) + } + }) + + it.skipIf(!bwrapCanNamespaceNetwork())( + 'is read from the network configuration, in a network-restricted sandbox', + async () => { + await SandboxManager.initialize({ + network: { + allowedDomains: [], + deniedDomains: [], + allowUnixSocketCreation: true, + }, + filesystem: { denyRead: [], allowWrite: [], denyWrite: [] }, + }) + const wrapped = await SandboxManager.wrapWithSandbox( + quote(['python3', '-c', PROBE]), + ) + expect(wrapped).toContain('--unshare-net') + expect(wrapped).toContain('SRT_ALLOW_UNIX_SOCKETS=1 ') + + const r = await run(wrapped) + expect(r.stdout).toContain(ALLOWED) + expect(r.stdout).toContain('INIT=apply-seccomp') + expect(r.status).toBe(0) + }, + 40000, + ) +}) diff --git a/vendor/seccomp-src/apply-seccomp.c b/vendor/seccomp-src/apply-seccomp.c index c7c020bc6..26d7bcc2c 100644 --- a/vendor/seccomp-src/apply-seccomp.c +++ b/vendor/seccomp-src/apply-seccomp.c @@ -38,6 +38,11 @@ * filter. SRT_ALLOW_NESTED_USERNS=1, read before the command exists and * removed from its environment, turns both off. * + * SRT_ALLOW_UNIX_SOCKETS=1, read and removed the same way, lets the command + * make Unix sockets: the `vsock` filter goes in where the `unix` one would, + * so VM sockets and io_uring stay refused, and everything else this program + * does (the PID namespace, the namespaces filter, observation) is unchanged. + * * Compile: gcc -static -O2 -o apply-seccomp apply-seccomp.c */ @@ -682,15 +687,24 @@ int main(int argc, char *argv[]) { * removed below. */ const char *allow_env = getenv("SRT_ALLOW_NESTED_USERNS"); int allow_nested_userns = allow_env && strcmp(allow_env, "1") == 0; + const char *unix_env = getenv("SRT_ALLOW_UNIX_SOCKETS"); + int allow_unix_sockets = unix_env && strcmp(unix_env, "1") == 0; /* Set when path (b) below runs, the only one the limit is written in. */ int made_userns = 0; _Static_assert(sizeof(unix_block_bpf) % sizeof(struct sock_filter) == 0, "BPF filter size must be a multiple of sock_filter"); - struct sock_fprog prog = { - .len = (unsigned short)(sizeof(unix_block_bpf) / sizeof(struct sock_filter)), - .filter = (struct sock_filter *)unix_block_bpf, - }; + _Static_assert(sizeof(vsock_block_bpf) % sizeof(struct sock_filter) == 0, + "BPF filter size must be a multiple of sock_filter"); + struct sock_fprog prog = allow_unix_sockets + ? (struct sock_fprog){ + .len = (unsigned short)(sizeof(vsock_block_bpf) / sizeof(struct sock_filter)), + .filter = (struct sock_filter *)vsock_block_bpf, + } + : (struct sock_fprog){ + .len = (unsigned short)(sizeof(unix_block_bpf) / sizeof(struct sock_filter)), + .filter = (struct sock_filter *)unix_block_bpf, + }; _Static_assert(sizeof(namespace_block_bpf) % sizeof(struct sock_filter) == 0, "BPF filter size must be a multiple of sock_filter"); struct sock_fprog namespace_prog = { @@ -940,6 +954,7 @@ int main(int argc, char *argv[]) { unsetenv("SRT_OBSERVE_SOCK"); unsetenv("SRT_ENCODED_CMD"); unsetenv("SRT_ALLOW_NESTED_USERNS"); + unsetenv("SRT_ALLOW_UNIX_SOCKETS"); if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { die("apply-seccomp: prctl(PR_SET_NO_NEW_PRIVS)"); } diff --git a/vendor/seccomp-src/seccomp-unix-block.c b/vendor/seccomp-src/seccomp-unix-block.c index b17999159..e092f40f9 100644 --- a/vendor/seccomp-src/seccomp-unix-block.c +++ b/vendor/seccomp-src/seccomp-unix-block.c @@ -27,7 +27,7 @@ * gcc -o seccomp-unix-block seccomp-unix-block.c -lseccomp * * Usage: - * ./seccomp-unix-block [arch] [unix|namespaces] + * ./seccomp-unix-block [arch] [unix|vsock|namespaces] * * If arch is given (x86_64 or aarch64), the filter is generated for that * architecture instead of the native one. Lets a single-arch builder emit @@ -38,7 +38,9 @@ * above; `namespaces` is a second one, which apply-seccomp stacks on it to * keep the command in the namespaces the sandbox made for it (apply-seccomp.c * says why). Two filters, so that a command opted out of the second keeps the - * first. + * first. `vsock` is `unix` without the AF_UNIX rule, which apply-seccomp + * takes in its place for a command allowed to make Unix sockets: VM sockets + * and io_uring stay refused. * * Dependencies: * - libseccomp (libseccomp-dev package on Debian/Ubuntu) @@ -149,7 +151,7 @@ int main(int argc, char *argv[]) { if (argc < 2 || argc > 4) { fprintf(stderr, - "Usage: %s [x86_64|aarch64] [unix|namespaces]\n", + "Usage: %s [x86_64|aarch64] [unix|vsock|namespaces]\n", argv[0]); return 1; } @@ -157,7 +159,8 @@ int main(int argc, char *argv[]) { const char *output_file = argv[1]; const char *arch_name = (argc >= 3) ? argv[2] : NULL; const char *rule_set = (argc == 4) ? argv[3] : "unix"; - if (strcmp(rule_set, "unix") != 0 && strcmp(rule_set, "namespaces") != 0) { + if (strcmp(rule_set, "unix") != 0 && strcmp(rule_set, "vsock") != 0 + && strcmp(rule_set, "namespaces") != 0) { fprintf(stderr, "Error: Unknown rule set '%s'\n", rule_set); return 1; } @@ -202,19 +205,21 @@ int main(int argc, char *argv[]) { return export_filter(ctx, output_file); } - /* Add rule to block socket(AF_UNIX, ...) */ + /* Add rule to block socket(AF_UNIX, ...), in `unix` only. */ /* socket() syscall signature: int socket(int domain, int type, int protocol) */ /* arg0 = domain (AF_UNIX = 1) */ /* Use SCMP_CMP_MASKED_EQ with a 32-bit mask: the domain argument is a 32-bit * int, so the kernel ignores the upper 32 bits of the register. A plain * SCMP_CMP_EQ would compare all 64 bits and miss calls where the upper bits * are set. */ - rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1, - SCMP_A0(SCMP_CMP_MASKED_EQ, 0xffffffff, AF_UNIX)); - if (rc < 0) { - fprintf(stderr, "Error: Failed to add seccomp rule: %s\n", strerror(-rc)); - seccomp_release(ctx); - return 1; + if (strcmp(rule_set, "unix") == 0) { + rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_MASKED_EQ, 0xffffffff, AF_UNIX)); + if (rc < 0) { + fprintf(stderr, "Error: Failed to add seccomp rule: %s\n", strerror(-rc)); + seccomp_release(ctx); + return 1; + } } /* Add rule to block socket(AF_VSOCK, ...) */ diff --git a/vendor/seccomp/build.ts b/vendor/seccomp/build.ts index cd93d61ee..a833edd02 100644 --- a/vendor/seccomp/build.ts +++ b/vendor/seccomp/build.ts @@ -30,12 +30,13 @@ run([ ]) // The two filters apply-seccomp stacks (see seccomp-unix-block.c), as separate -// arrays so that a command opted out of `namespaces` keeps `unix`. For the -// builder's own architecture only: the generator can put a call its libseccomp -// cannot name into a filter by number for that architecture alone. +// arrays so that a command opted out of `namespaces` keeps `unix`; and `vsock`, +// which it stacks in place of `unix` for a command allowed Unix sockets. For +// the builder's own architecture only: the generator can put a call its +// libseccomp cannot name into a filter by number for that architecture alone. const target = arch === 'x64' ? 'x86_64' : 'aarch64' const bpf: Record = {} -for (const rules of ['unix', 'namespaces']) { +for (const rules of ['unix', 'vsock', 'namespaces']) { const tmp = join(OUT, `${target}.${rules}.bpf`) run([gen, tmp, target, rules]) bpf[rules] = readFileSync(tmp) @@ -50,6 +51,9 @@ writeFileSync( 'static const unsigned char unix_block_bpf[] = {\n' + toCArray(bpf.unix) + '\n};\n' + + 'static const unsigned char vsock_block_bpf[] = {\n' + + toCArray(bpf.vsock) + + '\n};\n' + 'static const unsigned char namespace_block_bpf[] = {\n' + toCArray(bpf.namespaces) + '\n};\n' +