| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; |
| import { tmpdir } from 'node:os'; |
| import { isAbsolute, join, resolve, sep } from 'node:path'; |
| import test from 'node:test'; |
| import { |
| assertExpectedEpochRelation, |
| durableStateLocations, |
| parseQualificationArgs, |
| qualificationSandboxArgs, |
| qualificationSandboxInvocation, |
| sha256File, |
| } from './qualify-released-cli-state-root.mjs'; |
| |
| const SHA_A = 'a'.repeat(64); |
| const SHA_B = 'b'.repeat(64); |
| |
| test('parses two exact artifacts and an epoch relation', () => { |
| const source = resolve(tmpdir(), 'source.tgz'); |
| const target = resolve(tmpdir(), 'target.tgz'); |
| assert.deepEqual( |
| parseQualificationArgs([ |
| '--source', |
| source, |
| '--source-sha256', |
| SHA_A, |
| '--target', |
| target, |
| '--target-sha256', |
| SHA_B, |
| '--expect-epoch-relation', |
| 'same', |
| ]), |
| { |
| source, |
| sourceSha256: SHA_A, |
| target, |
| targetSha256: SHA_B, |
| expectedEpochRelation: 'same', |
| }, |
| ); |
| }); |
| |
| test('rejects ambiguous artifact identity and unknown arguments', () => { |
| const target = resolve(tmpdir(), 'target.tgz'); |
| assert.throws( |
| () => |
| parseQualificationArgs([ |
| '--source', |
| 'source.tgz', |
| '--source-sha256', |
| SHA_A, |
| '--target', |
| target, |
| '--target-sha256', |
| SHA_B, |
| '--expect-epoch-relation', |
| 'any', |
| ]), |
| /source must be an absolute path/u, |
| ); |
| assert.throws( |
| () => |
| parseQualificationArgs([ |
| '--source', |
| resolve(tmpdir(), 'source.tgz'), |
| '--source-sha256', |
| SHA_A, |
| '--target', |
| target, |
| '--target-sha256', |
| SHA_B, |
| '--extra', |
| 'value', |
| ]), |
| /Unknown qualification argument/u, |
| ); |
| }); |
| |
| test('uses privilege only for mount setup and drops every privilege before Node', () => { |
| const args = ['--die-with-parent', '--', '/usr/bin/node']; |
| assert.deepEqual( |
| qualificationSandboxInvocation({ args, account: { uid: 1001, gid: 1002 }, useSudo: false }), |
| { command: 'bwrap', args }, |
| ); |
| assert.deepEqual( |
| qualificationSandboxInvocation({ args, account: { uid: 1001, gid: 1002 }, useSudo: true }), |
| { |
| command: 'sudo', |
| args: [ |
| '--non-interactive', |
| '--', |
| '/usr/bin/bwrap', |
| '--die-with-parent', |
| '--cap-add', |
| 'CAP_SETUID', |
| '--cap-add', |
| 'CAP_SETGID', |
| '--cap-add', |
| 'CAP_SETPCAP', |
| '--', |
| '/usr/bin/setpriv', |
| '--regid', |
| '1002', |
| '--reuid', |
| '1001', |
| '--clear-groups', |
| '--inh-caps=-all', |
| '--ambient-caps=-all', |
| '--bounding-set=-all', |
| '--no-new-privs', |
| '/usr/bin/node', |
| ], |
| }, |
| ); |
| assert.throws( |
| () => |
| qualificationSandboxInvocation({ args: ['--die-with-parent'], account: {}, useSudo: true }), |
| /sandbox command is missing/u, |
| ); |
| }); |
| |
| test('creates a private Host IPC temp root before mounting a scope that may live below it', () => { |
| const args = qualificationSandboxArgs({ |
| innerInputPath: '/qualification/input.json', |
| scope: '/qualification', |
| sandbox: { |
| home: '/qualification/home', |
| temp: '/qualification/tmp', |
| passwd: '/qualification/etc/passwd', |
| group: '/qualification/etc/group', |
| environment: { |
| XDG_CACHE_HOME: '/qualification/home/.cache', |
| XDG_CONFIG_HOME: '/qualification/home/.config', |
| XDG_DATA_HOME: '/qualification/home/.local/share', |
| }, |
| }, |
| }); |
| const tmpfsIndex = args.indexOf('--tmpfs'); |
| assert.deepEqual(args.slice(tmpfsIndex, tmpfsIndex + 8), [ |
| '--tmpfs', |
| '/tmp', |
| '--chmod', |
| '1777', |
| '/tmp', |
| '--bind', |
| '/qualification', |
| '/qualification', |
| ]); |
| }); |
| |
| test('classifies and fences the expected epoch relationship', () => { |
| assert.equal(assertExpectedEpochRelation(74, 76, 'different'), 'different'); |
| assert.equal(assertExpectedEpochRelation(76, 76, 'same'), 'same'); |
| assert.equal(assertExpectedEpochRelation(76, 78, 'any'), 'different'); |
| assert.throws( |
| () => assertExpectedEpochRelation(76, 78, 'same'), |
| /Expected same compatibility epochs/u, |
| ); |
| }); |
| |
| test('computes the exact artifact SHA-256', () => { |
| const root = mkdtempSync(join(tmpdir(), 'maka-release-digest-')); |
| try { |
| const path = join(root, 'artifact.tgz'); |
| writeFileSync(path, 'released bytes'); |
| assert.equal( |
| sha256File(path), |
| '2f9e0acbd320f87ceff2b9d259c99ec87830fc87d99bf914cef87394294a6682', |
| ); |
| } finally { |
| rmSync(root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('durable state covers the control namespace, not only the State Root', () => { |
| // The access file the Host opens before its Kernel starts lives beside the |
| // State Root, not inside it. A golden copy scoped to the State Root alone |
| // restored a workspace whose control records had already moved on, so the |
| // transition it proved was never the one a user performs. |
| const scope = resolve(tmpdir(), 'qualification-scope'); |
| const locations = durableStateLocations(scope); |
| assert.ok(locations.length >= 2); |
| assert.ok(locations.some(({ live }) => live === join(scope, 'state-root'))); |
| assert.ok( |
| locations.some(({ live }) => live.endsWith(join('.cache', 'maka', 'runtime-hosts'))), |
| 'the account-local control namespace must be captured and restored', |
| ); |
| for (const { live, golden } of locations) { |
| assert.ok(isAbsolute(live) && isAbsolute(golden)); |
| assert.ok( |
| !golden.startsWith(`${live}${sep}`), |
| 'a golden copy must not nest inside its live path', |
| ); |
| } |
| }); |
| |
| test('a workspace target replaces tarball identity instead of weakening it', () => { |
| const source = resolve(tmpdir(), 'source.tgz'); |
| const repo = resolve(tmpdir(), 'checkout'); |
| assert.deepEqual( |
| parseQualificationArgs([ |
| '--source', |
| source, |
| '--source-sha256', |
| SHA_A, |
| '--target-workspace', |
| repo, |
| ]), |
| { source, sourceSha256: SHA_A, targetWorkspace: repo, expectedEpochRelation: 'any' }, |
| ); |
| // The source stays an exact published artifact either way: the point of the |
| // run is that state written by a real release still opens. |
| assert.throws( |
| () => parseQualificationArgs(['--target-workspace', repo]), |
| /--source must be an absolute path/u, |
| ); |
| assert.throws( |
| () => |
| parseQualificationArgs([ |
| '--source', |
| source, |
| '--source-sha256', |
| SHA_A, |
| '--target-workspace', |
| repo, |
| '--target-sha256', |
| SHA_B, |
| ]), |
| /cannot also name a tarball target/u, |
| ); |
| }); |