[GH-ISSUE #3874] [Bug] Literal POSIX names beginning with a letter and colon abort artifact inventory and extraction #1493

Closed
opened 2026-10-16 21:06:09 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3874

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

On a POSIX host, inspect-artifact and extract-artifact abort with artifact_operation_failed (reason path) when a top-level member's literal name begins with an ASCII letter followed by a colon, because REA treats it as a Windows drive prefix. A valid ASAR produced by the official @electron/asar package containing the ordinary POSIX file a:b.txt cannot be inventoried or extracted, even though the official ASAR reader lists and reads it. Controls ab:c.txt and nested/a:b.txt are accepted, so the rejection keys on the start of the whole logical path.

The valid ASAR member a:b.txt with bytes hello makes both commands exit 1 with artifact_operation_failed, reason path, detail Artifact path is absolute or unsafe: "a:b.txt", and neither returns its result; app-bundle inspection fails identically when the file is at the bundle root. The official ASAR reader lists /a:b.txt and reads its bytes. The controls ab:c.txt and nested/a:b.txt are accepted, showing the rejection keys on the start of the whole logical path, not on an invalid component or an unsupported colon.

Reproduction

Run from a source checkout root so the fixture producer can resolve the project's bundled @electron/asar; the rea CLI commands can run from an installed copy. The probe builds the fixtures with the official package and inspects the result.

export REA_CHECKOUT="$PWD"
export FIXTURE_ROOT="$(mktemp -d)"
node --input-type=module <<'JS'
import { mkdir, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
const require = createRequire(`${process.env.REA_CHECKOUT}/package.json`);
const { createPackage, listPackage, extractFile } = require('@electron/asar');
const root = process.env.FIXTURE_ROOT;
await mkdir(`${root}/source`);
await writeFile(`${root}/source/a:b.txt`, 'hello');
await createPackage(`${root}/source`, `${root}/fixture.asar`);
console.log('original members', listPackage(`${root}/fixture.asar`));
console.log('original bytes', extractFile(`${root}/fixture.asar`, 'a:b.txt').toString());
await mkdir(`${root}/control/nested`, { recursive: true });
await writeFile(`${root}/control/ab:c.txt`, 'hello');
await writeFile(`${root}/control/nested/a:b.txt`, 'hello');
await createPackage(`${root}/control`, `${root}/control.asar`);
console.log('control members', listPackage(`${root}/control.asar`));
JS
rea inspect-artifact "$FIXTURE_ROOT/fixture.asar" --format json
rea extract-artifact "$FIXTURE_ROOT/fixture.asar" --format json
rea inspect-artifact "$FIXTURE_ROOT/control.asar" --format json

Official reader output:

original members [ '/a:b.txt' ]
original bytes hello
control members [ '/ab:c.txt', '/nested', '/nested/a:b.txt' ]

Failing inspection, exit 1:

{"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact path is absolute or unsafe: \"a:b.txt\"","retryable":false,"remediation":{"action":"Artifact path is absolute or unsafe: \"a:b.txt\""},"details":{"operation":"inspect_artifact","reason":"path","detail":"Artifact path is absolute or unsafe: \"a:b.txt\""}}

Extraction returns the same envelope with details.operation equal to extract_artifact. The control inspection exits 0:

{"root_format":"asar","paths":[[".","verified"],["ab:c.txt","verified"],["nested","verified"],["nested/a:b.txt","verified"]],"coverage":{"scope":"container-entries","status":"complete"}}

Expected behavior

Preserve the literal names supplied by the owning filesystem or ASAR reader. src/artifacts/AsarListing.ts:65 treats an ASAR member as one component rather than a host path and admits the component a:b.txt at lines 67-78; src/artifacts/DirectoryArtifactReader.ts:115-127 emits the actual relative filesystem name. Destination-specific colon restrictions belong to the Windows output boundary at src/artifacts/SafeOutputTree.ts:588 and should not block POSIX inventory or extraction. AGENTS.md requires preserving caller-selected paths and consuming the owner's parsed facts.

Impact and scope

A top-level regular file or directory whose literal name begins with an ASCII letter and colon blocks inventory and extraction of the entire selected artifact on a POSIX host; this is an input refusal with the source left intact, not observed data corruption.

Workaround: available. Inspecting the unchanged ASAR inside a filesystem-backed app bundle gives its members a normal logical prefix and returns a verified occurrence, though it changes the inventory root, and the official ASAR reader can read the member bytes; no direct REA extraction workaround was verified.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

ASAR archive and macOS app bundle on a POSIX host

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

  • Expected source: src/artifacts/AsarListing.ts:65-78 admits the ASAR component a:b.txt, src/artifacts/DirectoryArtifactReader.ts:115-127 emits the literal filesystem name, and src/artifacts/SafeOutputTree.ts:588 already scopes colon restrictions to the Windows output boundary.
  • Failure source: the observed exit-1 envelopes above, with root cause at src/artifacts/ArtifactPaths.ts:7 defining DRIVE_PREFIX = /^[A-Za-z]:/u and line 25 rejecting every matching logical name on every host, applied to the reader's logical path at src/artifacts/inventory/scanReader.ts:179 before recording or hashing; extraction inventories first and fails at the same boundary.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

inspect-artifact and extract-artifact should have inventoried and extracted the member under its literal name a:b.txt on a POSIX host, instead of exiting 1 with artifact_operation_failed and reason path.

Restoration check:

Failing observable: inspect-artifact and extract-artifact on the ASAR containing a:b.txt exit 1 with artifact_operation_failed and reason path. Passing observable: both commands inventory and extract the member under its literal name a:b.txt on a POSIX host, as the official ASAR reader does, while the Windows output boundary still rejects such a name at extraction destinations. Control: the control.asar inspection (members ab:c.txt and nested/a:b.txt) already exits 0 and lists its members, and inspecting the unchanged failing ASAR inside a filesystem-backed app bundle returns a verified occurrence under a normal logical prefix.

Analysis or suspected cause

src/artifacts/ArtifactPaths.ts:7 defines DRIVE_PREFIX = /^[A-Za-z]:/u and line 25 rejects every matching logical name on every host; src/artifacts/inventory/scanReader.ts:179 applies it to the reader's logical path before recording or hashing, so a POSIX component named a:b.txt is misread as a Windows drive. Colon restrictions already belong to the Windows output boundary at src/artifacts/SafeOutputTree.ts:588.

Additional context

Closed #2619 concerned literal POSIX backslashes and closed #1574 concerned Windows NTFS alternate-stream extraction for a colon name; neither reports the leading single-letter-colon rejection on POSIX, and this finding does not claim those fixes regressed.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3874 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior On a POSIX host, `inspect-artifact` and `extract-artifact` abort with `artifact_operation_failed` (reason `path`) when a top-level member's literal name begins with an ASCII letter followed by a colon, because REA treats it as a Windows drive prefix. A valid ASAR produced by the official `@electron/asar` package containing the ordinary POSIX file `a:b.txt` cannot be inventoried or extracted, even though the official ASAR reader lists and reads it. Controls `ab:c.txt` and `nested/a:b.txt` are accepted, so the rejection keys on the start of the whole logical path. The valid ASAR member `a:b.txt` with bytes `hello` makes both commands exit 1 with `artifact_operation_failed`, reason `path`, detail `Artifact path is absolute or unsafe: "a:b.txt"`, and neither returns its result; app-bundle inspection fails identically when the file is at the bundle root. The official ASAR reader lists `/a:b.txt` and reads its bytes. The controls `ab:c.txt` and `nested/a:b.txt` are accepted, showing the rejection keys on the start of the whole logical path, not on an invalid component or an unsupported colon. ### Reproduction Run from a source checkout root so the fixture producer can resolve the project's bundled `@electron/asar`; the `rea` CLI commands can run from an installed copy. The probe builds the fixtures with the official package and inspects the result. ```sh export REA_CHECKOUT="$PWD" export FIXTURE_ROOT="$(mktemp -d)" node --input-type=module <<'JS' import { mkdir, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; const require = createRequire(`${process.env.REA_CHECKOUT}/package.json`); const { createPackage, listPackage, extractFile } = require('@electron/asar'); const root = process.env.FIXTURE_ROOT; await mkdir(`${root}/source`); await writeFile(`${root}/source/a:b.txt`, 'hello'); await createPackage(`${root}/source`, `${root}/fixture.asar`); console.log('original members', listPackage(`${root}/fixture.asar`)); console.log('original bytes', extractFile(`${root}/fixture.asar`, 'a:b.txt').toString()); await mkdir(`${root}/control/nested`, { recursive: true }); await writeFile(`${root}/control/ab:c.txt`, 'hello'); await writeFile(`${root}/control/nested/a:b.txt`, 'hello'); await createPackage(`${root}/control`, `${root}/control.asar`); console.log('control members', listPackage(`${root}/control.asar`)); JS rea inspect-artifact "$FIXTURE_ROOT/fixture.asar" --format json rea extract-artifact "$FIXTURE_ROOT/fixture.asar" --format json rea inspect-artifact "$FIXTURE_ROOT/control.asar" --format json ``` Official reader output: ```text original members [ '/a:b.txt' ] original bytes hello control members [ '/ab:c.txt', '/nested', '/nested/a:b.txt' ] ``` Failing inspection, exit 1: ```json {"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact path is absolute or unsafe: \"a:b.txt\"","retryable":false,"remediation":{"action":"Artifact path is absolute or unsafe: \"a:b.txt\""},"details":{"operation":"inspect_artifact","reason":"path","detail":"Artifact path is absolute or unsafe: \"a:b.txt\""}} ``` Extraction returns the same envelope with `details.operation` equal to `extract_artifact`. The control inspection exits 0: ```json {"root_format":"asar","paths":[[".","verified"],["ab:c.txt","verified"],["nested","verified"],["nested/a:b.txt","verified"]],"coverage":{"scope":"container-entries","status":"complete"}} ``` ### Expected behavior Preserve the literal names supplied by the owning filesystem or ASAR reader. src/artifacts/AsarListing.ts:65 treats an ASAR member as one component rather than a host path and admits the component `a:b.txt` at lines 67-78; src/artifacts/DirectoryArtifactReader.ts:115-127 emits the actual relative filesystem name. Destination-specific colon restrictions belong to the Windows output boundary at src/artifacts/SafeOutputTree.ts:588 and should not block POSIX inventory or extraction. AGENTS.md requires preserving caller-selected paths and consuming the owner's parsed facts. ### Impact and scope A top-level regular file or directory whose literal name begins with an ASCII letter and colon blocks inventory and extraction of the entire selected artifact on a POSIX host; this is an input refusal with the source left intact, not observed data corruption. Workaround: available. Inspecting the unchanged ASAR inside a filesystem-backed app bundle gives its members a normal logical prefix and returns a verified occurrence, though it changes the inventory root, and the official ASAR reader can read the member bytes; no direct REA extraction workaround was verified. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context ASAR archive and macOS app bundle on a POSIX host ### Interface CLI ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence - Expected source: src/artifacts/AsarListing.ts:65-78 admits the ASAR component `a:b.txt`, src/artifacts/DirectoryArtifactReader.ts:115-127 emits the literal filesystem name, and src/artifacts/SafeOutputTree.ts:588 already scopes colon restrictions to the Windows output boundary. - Failure source: the observed exit-1 envelopes above, with root cause at src/artifacts/ArtifactPaths.ts:7 defining `DRIVE_PREFIX = /^[A-Za-z]:/u` and line 25 rejecting every matching logical name on every host, applied to the reader's logical path at src/artifacts/inventory/scanReader.ts:179 before recording or hashing; extraction inventories first and fails at the same boundary. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ### Typed outcome or recovery expected inspect-artifact and extract-artifact should have inventoried and extracted the member under its literal name a:b.txt on a POSIX host, instead of exiting 1 with artifact_operation_failed and reason path. Restoration check: Failing observable: `inspect-artifact` and `extract-artifact` on the ASAR containing `a:b.txt` exit 1 with `artifact_operation_failed` and reason `path`. Passing observable: both commands inventory and extract the member under its literal name `a:b.txt` on a POSIX host, as the official ASAR reader does, while the Windows output boundary still rejects such a name at extraction destinations. Control: the `control.asar` inspection (members `ab:c.txt` and `nested/a:b.txt`) already exits 0 and lists its members, and inspecting the unchanged failing ASAR inside a filesystem-backed app bundle returns a verified occurrence under a normal logical prefix. ### Analysis or suspected cause src/artifacts/ArtifactPaths.ts:7 defines DRIVE_PREFIX = /^[A-Za-z]:/u and line 25 rejects every matching logical name on every host; src/artifacts/inventory/scanReader.ts:179 applies it to the reader's logical path before recording or hashing, so a POSIX component named a:b.txt is misread as a Windows drive. Colon restrictions already belong to the Windows output boundary at src/artifacts/SafeOutputTree.ts:588. ### Additional context Closed #2619 concerned literal POSIX backslashes and closed #1574 concerned Windows NTFS alternate-stream extraction for a colon name; neither reports the leading single-letter-colon rejection on POSIX, and this finding does not claim those fixes regressed. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
Author
Owner

@x1mm4nu3l commented on GitHub (Oct 20, 2026):

Confirmed on Linux/Node 24.18.0 at main 2538214e2 using ASARs generated by the official writer. Its reader lists a:b.txt and reads hello, but both inspect-artifact and extract-artifact reject that archive with reason path. The ab:c.txt and nested/a:b.txt controls inspect and extract successfully, with exact hello bytes preserved. This extends the reproduction to Linux; Windows behavior was not tested.

<!-- gh-comment-id:6286505892 --> @x1mm4nu3l commented on GitHub (Oct 20, 2026): Confirmed on Linux/Node 24.18.0 at main 2538214e2 using ASARs generated by the official writer. Its reader lists a:b.txt and reads hello, but both `inspect-artifact` and `extract-artifact` reject that archive with reason path. The ab:c.txt and nested/a:b.txt controls inspect and extract successfully, with exact hello bytes preserved. This extends the reproduction to Linux; Windows behavior was not tested.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

normalizeArtifactPath now rejects letter-colon-separator absolute drive paths rather than every initial letter-colon name. a:b.txt, ab:c.txt and nested/a:b.txt preserve exact spelling in the helper replay; C:/outside remains refused. SafeOutputTree retains colon refusal specifically for Windows destinations.

Implementation: ArtifactPaths.ts, ArtifactPaths.ts.

The relevant regression checks in ArtifactPaths.test.ts, artifactProvider.archiveSafety.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380732236 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. normalizeArtifactPath now rejects letter-colon-separator absolute drive paths rather than every initial letter-colon name. a:b.txt, ab:c.txt and nested/a:b.txt preserve exact spelling in the helper replay; C:/outside remains refused. SafeOutputTree retains colon refusal specifically for Windows destinations. Implementation: [ArtifactPaths.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactPaths.ts#L7), [ArtifactPaths.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactPaths.ts#L51). The relevant regression checks in [ArtifactPaths.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactPaths.test.ts), [artifactProvider.archiveSafety.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1493
No description provided.