diff --git a/src/cloud-hypervisor/vmm-identity.ts b/src/cloud-hypervisor/vmm-identity.ts index 2686a6372..7d319d52b 100644 --- a/src/cloud-hypervisor/vmm-identity.ts +++ b/src/cloud-hypervisor/vmm-identity.ts @@ -1,7 +1,7 @@ import { randomBytes } from 'crypto'; import { promises as fs } from 'fs'; import * as path from 'path'; -import execa from 'execa'; +import { createDefaultIdentityDependencies } from '../identity-dependencies'; const ACCOUNT_PREFIX = 'awfvmm-'; const ACCOUNT_LOCK_DIRECTORY = '/run/awf-cloud-hypervisor/.account-lock'; @@ -54,32 +54,8 @@ export interface CloudHypervisorVmmIdentityDependencies { processStartTime(pid: number): Promise; } -const defaultDependencies: CloudHypervisorVmmIdentityDependencies = { - mkdir: fs.mkdir, - writeFile: fs.writeFile, - readFile: fs.readFile, - rm: fs.rm, - rmdir: fs.rmdir, - lstat: fs.lstat, - run: async (command, args) => { - const result = await execa(command, [...args], { - reject: false, - stdio: ['ignore', 'pipe', 'pipe'], - env: { PATH: '/usr/sbin:/usr/bin:/sbin:/bin' }, - extendEnv: false, - }); - if (result.exitCode !== 0) { - throw new Error( - `${command} ${args.join(' ')} exited with code ${result.exitCode}: ` + - `${result.stderr.trim() || result.stdout.trim()}`, - ); - } - return { stdout: result.stdout, stderr: result.stderr }; - }, - sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), - pid: process.pid, - processStartTime: readProcessStartTime, -}; +const defaultDependencies: CloudHypervisorVmmIdentityDependencies = + createDefaultIdentityDependencies(readProcessStartTime); interface LockOwner { readonly pid: number; diff --git a/src/identity-dependencies.test.ts b/src/identity-dependencies.test.ts new file mode 100644 index 000000000..006f1a728 --- /dev/null +++ b/src/identity-dependencies.test.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import { createDefaultIdentityDependencies } from './identity-dependencies'; + +describe('createDefaultIdentityDependencies', () => { + it('creates the shared filesystem and process dependency scaffold', async () => { + const processStartTime = jest.fn().mockResolvedValue('123'); + const dependencies = createDefaultIdentityDependencies(processStartTime); + + expect(dependencies).toMatchObject({ + mkdir: fs.mkdir, + writeFile: fs.writeFile, + readFile: fs.readFile, + rm: fs.rm, + rmdir: fs.rmdir, + lstat: fs.lstat, + pid: process.pid, + processStartTime, + }); + const result = await dependencies.run(process.execPath, [ + '-e', + 'process.stdout.write(process.env.PATH ?? "")', + ]); + expect(result.stdout).toBe('/usr/sbin:/usr/bin:/sbin:/bin'); + await expect(dependencies.run(process.execPath, ['-e', 'process.exit(1)'])) + .rejects.toThrow(/exited with code 1/); + await expect(dependencies.sleep(0)).resolves.toBeUndefined(); + }); +}); diff --git a/src/identity-dependencies.ts b/src/identity-dependencies.ts new file mode 100644 index 000000000..f15d9dac7 --- /dev/null +++ b/src/identity-dependencies.ts @@ -0,0 +1,55 @@ +import { promises as fs } from 'fs'; +import execa from 'execa'; + +export interface DefaultIdentityDependencies { + readonly mkdir: typeof fs.mkdir; + readonly writeFile: typeof fs.writeFile; + readonly readFile: typeof fs.readFile; + readonly rm: typeof fs.rm; + readonly rmdir: typeof fs.rmdir; + readonly lstat: typeof fs.lstat; + readonly run: ( + command: string, + args: readonly string[], + ) => Promise<{ stdout: string; stderr: string }>; + readonly sleep: (milliseconds: number) => Promise; + readonly pid: number; + readonly processStartTime: (pid: number) => Promise; +} + +/** + * Creates hardened filesystem and process helpers for privileged identity and lock management. + * Commands receive only a fixed system PATH, while callers provide their process start-time reader. + */ +export function createDefaultIdentityDependencies( + processStartTime: DefaultIdentityDependencies['processStartTime'], +): DefaultIdentityDependencies { + return { + mkdir: fs.mkdir, + writeFile: fs.writeFile, + readFile: fs.readFile, + rm: fs.rm, + rmdir: fs.rmdir, + lstat: fs.lstat, + run: async (command, args) => { + // Identity tool paths are absolute paths returned by the root-only preflight. + // eslint-disable-next-line local/no-unsafe-execa + const result = await execa(command, [...args], { + reject: false, + stdio: ['ignore', 'pipe', 'pipe'], + env: { PATH: '/usr/sbin:/usr/bin:/sbin:/bin' }, + extendEnv: false, + }); + if (result.exitCode !== 0) { + throw new Error( + `${command} ${args.join(' ')} exited with code ${result.exitCode}: ` + + `${result.stderr.trim() || result.stdout.trim()}`, + ); + } + return { stdout: result.stdout, stderr: result.stderr }; + }, + sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), + pid: process.pid, + processStartTime, + }; +} diff --git a/src/nvx/runtime-lifecycle.ts b/src/nvx/runtime-lifecycle.ts index 66edf709c..abe15eb29 100644 --- a/src/nvx/runtime-lifecycle.ts +++ b/src/nvx/runtime-lifecycle.ts @@ -1,7 +1,7 @@ import { randomBytes } from 'crypto'; import { promises as fs } from 'fs'; import * as path from 'path'; -import execa from 'execa'; +import { createDefaultIdentityDependencies } from '../identity-dependencies'; import { createMicrovmNetworkPlan, generateMicrovmNftRuleset, @@ -100,31 +100,8 @@ export interface NvxPhase3dLaunchPlan { } const defaultDependencies: NvxRuntimeLifecycleDependencies = { - mkdir: fs.mkdir, - writeFile: fs.writeFile, - readFile: fs.readFile, - rm: fs.rm, + ...createDefaultIdentityDependencies(readProcessStartTime), rename: fs.rename, - rmdir: fs.rmdir, - lstat: fs.lstat, - run: async (command, args) => { - const result = await execa(command, [...args], { - reject: false, - stdio: ['ignore', 'pipe', 'pipe'], - env: { PATH: '/usr/sbin:/usr/bin:/sbin:/bin' }, - extendEnv: false, - }); - if (result.exitCode !== 0) { - throw new Error( - `${command} ${args.join(' ')} exited with code ${result.exitCode}: ` + - `${result.stderr.trim() || result.stdout.trim()}`, - ); - } - return { stdout: result.stdout, stderr: result.stderr }; - }, - sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), - pid: process.pid, - processStartTime: readProcessStartTime, }; interface NvxDeviceAclGrant {