import { execFileSync } from 'node:child_process'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { buildLinuxLauncherScript, resolveLinuxLauncherNames } from './linux-launcher.rules'; interface KernelFlags { apparmorRestriction: string; unprivilegedUsernsClone: string; maxUserNamespaces: string; } const PERMISSIVE_KERNEL: KernelFlags = { apparmorRestriction: '0', unprivilegedUsernsClone: '1', maxUserNamespaces: '15000' }; let workspace = ''; function writeKernelFlags(flags: KernelFlags): Record { const paths = { apparmorRestriction: join(workspace, 'apparmor_restrict_unprivileged_userns'), unprivilegedUsernsClone: join(workspace, 'unprivileged_userns_clone'), maxUserNamespaces: join(workspace, 'max_user_namespaces') }; for (const key of Object.keys(paths) as (keyof KernelFlags)[]) { writeFileSync(paths[key], `${flags[key]}\n`, 'utf8'); } return paths; } function runLauncher(flags: KernelFlags, args: string[] = []): string { const paths = writeKernelFlags(flags); const { launcherFileName, binaryFileName } = resolveLinuxLauncherNames('toju'); const launcherPath = join(workspace, launcherFileName); const binaryPath = join(workspace, binaryFileName); writeFileSync(binaryPath, '#!/bin/sh\nprintf \'%s\\n\' "$@"\n', { encoding: 'utf8', mode: 0o755 }); writeFileSync( launcherPath, buildLinuxLauncherScript({ binaryFileName, apparmorRestrictionPath: paths.apparmorRestriction, unprivilegedUsernsClonePath: paths.unprivilegedUsernsClone, maxUserNamespacesPath: paths.maxUserNamespaces }), { encoding: 'utf8', mode: 0o755 } ); return execFileSync(launcherPath, args, { encoding: 'utf8' }).trim(); } describe('buildLinuxLauncherScript', () => { beforeEach(() => { workspace = mkdtempSync(join(tmpdir(), 'toju-launcher-')); }); afterEach(() => { rmSync(workspace, { force: true, recursive: true }); }); it('keeps the sandbox on when the kernel allows unprivileged user namespaces', () => { expect(runLauncher(PERMISSIVE_KERNEL)).toBe(''); }); it('disables the sandbox when AppArmor confines unprivileged user namespaces', () => { const output = runLauncher({ ...PERMISSIVE_KERNEL, apparmorRestriction: '1' }); expect(output).toBe('--no-sandbox'); }); it('disables the sandbox when the kernel forbids unprivileged namespace cloning', () => { const output = runLauncher({ ...PERMISSIVE_KERNEL, unprivilegedUsernsClone: '0' }); expect(output).toBe('--no-sandbox'); }); it('disables the sandbox when no user namespaces are available at all', () => { const output = runLauncher({ ...PERMISSIVE_KERNEL, maxUserNamespaces: '0' }); expect(output).toBe('--no-sandbox'); }); it('forwards launch arguments to the real binary', () => { const output = runLauncher(PERMISSIVE_KERNEL, ['toju://invite/abc', '--ozone-platform=wayland']); expect(output.split('\n')).toEqual(['toju://invite/abc', '--ozone-platform=wayland']); }); it('never repeats a sandbox switch the caller already supplied', () => { const output = runLauncher( { ...PERMISSIVE_KERNEL, apparmorRestriction: '1' }, ['--no-sandbox', '%U'] ); expect(output.split('\n')).toEqual(['--no-sandbox', '%U']); }); it('assumes a blocked sandbox is fine when the kernel switches are unreadable', () => { const { launcherFileName, binaryFileName } = resolveLinuxLauncherNames('toju'); const launcherPath = join(workspace, launcherFileName); writeFileSync( join(workspace, binaryFileName), '#!/bin/sh\nprintf \'%s\\n\' "$@"\n', { encoding: 'utf8', mode: 0o755 } ); writeFileSync( launcherPath, buildLinuxLauncherScript({ binaryFileName, apparmorRestrictionPath: join(workspace, 'missing-apparmor'), unprivilegedUsernsClonePath: join(workspace, 'missing-clone'), maxUserNamespacesPath: join(workspace, 'missing-max') }), { encoding: 'utf8', mode: 0o755 } ); expect(execFileSync(launcherPath, { encoding: 'utf8' }).trim()).toBe(''); }); }); describe('resolveLinuxLauncherNames', () => { it('keeps the published executable name for the launcher and renames the binary', () => { expect(resolveLinuxLauncherNames('toju')).toEqual({ launcherFileName: 'toju', binaryFileName: 'toju-bin' }); }); });