Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ github:
# reports and no committer can override it.
contexts:
- test
# Windows recovery is a separate native crash/owner-death boundary.
# The workflow runs on every PR and main push so this context can be
# required without leaving unrelated pull requests pending forever.
- windows_recovery

rulesets:
- name: Immutable release tags
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/windows-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
name: Windows recovery

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

concurrency:
group: windows-recovery-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand All @@ -47,6 +52,21 @@ jobs:
- name: Build test artifacts
run: npm.cmd run build:test

- name: Verify managed dependency alternate streams
shell: pwsh
run: |
node.exe --test --test-reporter=tap --test-concurrency=1 `
--test-name-pattern="NTFS alternate stream" `
packages/storage/dist/__tests__/managed-dependency-environment.test.js `
2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/managed-dependency-ads.tap"
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { exit $exitCode }
$output = Get-Content "$env:RUNNER_TEMP/managed-dependency-ads.tap"
if ($output -notcontains '# tests 3' -or $output -notcontains '# pass 3' -or $output -notcontains '# skipped 0') {
Write-Error 'Managed dependency ADS gate did not run exactly three passing Windows tests'
exit 1
}

- name: Verify Runtime Host Local IPC trust boundary
shell: pwsh
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/

import fs from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { join } from 'node:path';

const [rootArgument, markerFile] = process.argv.slice(2);
Expand All @@ -44,8 +43,9 @@ fs.promises.open = (async (path, flags, mode) => {
}
return originalOpen(path, flags, mode);
}) as typeof fs.promises.open;
syncBuiltinESMExports();

// Import after the interposition: marker-file captures the intrinsic at module
// evaluation, while production code must ignore later global mutations.
const { resolveStorageRoot, StorageRootAuthorityError } = await import('../../root-authority.js');

const parentDisconnected = new Promise<void>((resolvePromise) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,81 @@ test('rejects an NTFS alternate stream created inside a dependency artifact', {
storageRoot,
producer,
});
await assert.rejects(authority.acquire(identity, source), /alternate data stream/u);
await assert.rejects(
authority.acquire(identity, source),
(error: unknown) =>
error instanceof Error &&
error.message === 'Managed dependency environment contains an alternate data stream',
);
await authority.close();
});

test('rejects an NTFS alternate stream attached to the published dependency root', {
skip: process.platform !== 'win32',
}, async (t) => {
const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-root-ads-'));
t.after(() => rm(storageRoot, { recursive: true, force: true }));
const producer = {
capability: FIXTURE_PRODUCER_CAPABILITY,
packageManagerName: 'npm' as const,
packageManagerVersion: '11.12.1',
nodeRuntime: fixtureNodeRuntime(),
async provision(input: { outputRoot: string }) {
await writeFile(join(input.outputRoot, 'index.js'), 'trusted\n', 'utf8');
},
};
const source = dependencySourceForName('root-ads');
const identity = computeManagedDependencyEnvironmentIdentity(source);
const authority = await createManagedDependencyEnvironmentAuthority({ storageRoot, producer });
const lease = await authority.acquire(identity, source);
await writeFile(`${lease.dependencyRoot}:unhashed`, 'malicious\n', 'utf8');
await lease.release();
await authority.close();

const reopened = await createManagedDependencyEnvironmentAuthority({ storageRoot, producer });
await assert.rejects(
reopened.acquire(identity, source),
(error: unknown) =>
error instanceof Error &&
error.message === 'Managed dependency environment contains an alternate data stream',
);
await reopened.close();
});

test('rejects an NTFS alternate stream attached to a published nested directory', {
skip: process.platform !== 'win32',
}, async (t) => {
const storageRoot = await mkdtemp(join(tmpdir(), 'maka-dependency-目录-ads-'));
t.after(() => rm(storageRoot, { recursive: true, force: true }));
const producer = {
capability: FIXTURE_PRODUCER_CAPABILITY,
packageManagerName: 'npm' as const,
packageManagerVersion: '11.12.1',
nodeRuntime: fixtureNodeRuntime(),
async provision(input: { outputRoot: string }) {
const packageRoot = join(input.outputRoot, 'fixture-包');
await mkdir(packageRoot);
await writeFile(join(packageRoot, 'index.js'), 'trusted\n', 'utf8');
},
};
const source = dependencySourceForName('nested-directory-ads');
const identity = computeManagedDependencyEnvironmentIdentity(source);
const authority = await createManagedDependencyEnvironmentAuthority({ storageRoot, producer });
const lease = await authority.acquire(identity, source);
await writeFile(`${join(lease.dependencyRoot, 'fixture-包')}:unhashed`, 'malicious\n', 'utf8');
await lease.release();
await authority.close();

const reopened = await createManagedDependencyEnvironmentAuthority({ storageRoot, producer });
await assert.rejects(
reopened.acquire(identity, source),
(error: unknown) =>
error instanceof Error &&
error.message === 'Managed dependency environment contains an alternate data stream',
);
await reopened.close();
});

test('accepts a POSIX package bin symlink whose target remains inside the dependency root', {
skip: process.platform === 'win32',
}, async (t) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/storage/src/__tests__/marker-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { mkdtemp, open, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand All @@ -29,6 +30,33 @@ import {
type MarkerFileHandle,
} from '../marker-file.js';

test('keeps the open primitive captured at module initialization', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-marker-file-captured-open-'));
const markerFile = '.marker.json';
const originalOpen = fs.promises.open;
let intercepted = false;
fs.promises.open = (async (path, flags, mode) => {
if (typeof path === 'string' && path.startsWith(join(root, `${markerFile}.`))) {
intercepted = true;
}
return originalOpen(path, flags, mode);
}) as typeof fs.promises.open;
try {
await publishMarkerFile({
root,
markerFile,
contents: '{"schemaVersion":1}\n',
maxBytes: 1_024,
publication: 'create',
invalidFile: () => new Error('invalid marker'),
});
assert.equal(intercepted, false);
} finally {
fs.promises.open = originalOpen;
await rm(root, { recursive: true, force: true });
}
});

for (const publication of ['create', 'replace'] as const) {
for (const failurePhase of ['write', 'sync', 'close'] as const) {
test(`${publication} removes its temporary marker after a ${failurePhase} failure`, async () => {
Expand Down
Loading