[GH-ISSUE #6064] [Bug] Selected keyed-archive read denial is classified as artifact execution failure #3707

Closed
opened 2026-10-31 00:43:15 +00:00 by bsakaguchi · 0 comments
Owner

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

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

inspect-keyed-archive reports a real read-permission denial on a selected bundle entry as artifact_operation_failed / execution_failure, reason io. It provides generic file-access advice instead of the existing access_denied classification and has no structured system_code or filesystem-read boundary. The path and EACCES remain in details.detail as text.

The app root and Info.plist stay readable. Resource admission succeeds; the denial occurs when the selected archive is opened. Readable and permission-restored controls both succeed. This reproduced through the built CLI using real mode-bit denial; no filesystem errors or providers were simulated.

Reproduction

Run from a prepared source checkout as a non-root POSIX user. Set REPRO_ROOT to an existing disposable writable directory outside the checkout, save this executed driver as archive-permission-reproduction.mjs, then run node archive-permission-reproduction.mjs. It creates inert XML data, isolates CLI state, restores permissions and removes its fixture in finally.

import { mkdir, mkdtemp, writeFile, chmod, readFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import assert from 'node:assert/strict';
const root = await mkdtemp(join(process.env.REPRO_ROOT, 'permission-case-'));
const app = join(root, 'Fixture.app');
const resources = join(app, 'Contents', 'Resources');
await mkdir(resources, { recursive: true });
await writeFile(join(app, 'Contents', 'Info.plist'), '<plist><dict><key>CFBundleIdentifier</key><string>org.example.fixture</string></dict></plist>');
const env = { ...process.env, REA_LOG_LEVEL: 'silent' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR']) {
  env[key] = join(root, key.toLowerCase());
  await mkdir(env[key]);
}
const path = join(resources, 'Archive.plist');
await writeFile(path, '<plist><dict><key>$archiver</key><string>NSKeyedArchiver</string><key>$version</key><integer>100000</integer><key>$objects</key><array><string>$null</string></array><key>$top</key><dict><key>root</key><dict><key>CF$UID</key><integer>0</integer></dict></dict></dict></plist>');
const args = ['scripts/rea.mjs', 'inspect-keyed-archive', app, 'Contents/Resources/Archive.plist', '--json'];
const run = (label) => {
  const child = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 45000 });
  if (child.error) throw child.error;
  assert.equal(child.stderr, '');
  const output = JSON.parse(child.stdout);
  const result = output.normalized_result ?? output;
  console.log(JSON.stringify({ label, exit: child.status, result }).replaceAll(root, '<fixture-root>'));
  return { exit: child.status, result };
};
try {
  const control = run('readable');
  assert.equal(control.exit, 0);
  await chmod(path, 0);
  await assert.rejects(readFile(path), { code: 'EACCES' });
  const denied = run('denied');
  assert.equal(denied.exit, 1);
  assert.equal(denied.result.code, 'artifact_operation_failed');
  assert.equal(denied.result.category, 'execution_failure');
  assert.equal(denied.result.details.reason, 'io');
  await chmod(path, 0o600);
  const restored = run('restored');
  assert.equal(restored.exit, 0);
} finally {
  await chmod(path, 0o600);
  await rm(root, { recursive: true, force: true });
}

Expected behavior

Preserve host read-access denial as access_denied / unavailable, with the selected entry path, EACCES or EPERM, and existing read-access remediation. Keep ordinary malformed content, missing selections and unsupported formats distinguishable.

AnalysisAccessDeniedError explicitly models a denied selected read; the documented keyed-archive operation selects an entry relative to the active bundle.

Impact and scope

P2: clients branching on typed codes receive an execution-failure classification for a host permission problem. This is consequential recovery/diagnostic behavior, with no successful incorrect decode or input modification. The raw EACCES permits manual diagnosis. Restoring authorized read permission was an observed workaround and made the same request succeed.

Executed scope: one selected XML keyed archive on macOS through CLI. EPERM, denied directory traversal, other platforms, live MCP transport and naturally occurring downstream repair behavior were not tested.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. Built CLI and source MCP tests at this revision.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0, non-root account. Repository Node pin: 24.18.0. No Hopper, Ghidra, IDA, ibtool or target application execution.

Target and artifact context

Self-authored resource-only app bundle with readable XML Info.plist and a Foundation NSKeyedArchiver XML entry at Contents/Resources/Archive.plist. Only the archive permission was changed to mode 000, then restored.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The readable/restored requests exited 0. An independent readFile rejected with EACCES before the denied request. The denied request exited 1 with empty stderr and these fields:

{
  "code": "artifact_operation_failed",
  "category": "execution_failure",
  "retryable": false,
  "message": "Artifact could not be read or written. Check file access and try again.",
  "details": {
    "operation": "inspect_keyed_archive",
    "reason": "io",
    "detail": "Could not open entry at <selected-archive> (EACCES): EACCES: permission denied, open '<selected-archive>'"
  }
}

The path in the excerpt is a placeholder. No structured system_code is present.

Typed outcome or recovery expected

access_denied, category unavailable, retaining the actual read path and OS code with boundary: "filesystem-read" and existing read-access guidance. Retrying an unchanged denied read need not become automatically retryable.

Analysis or suspected cause

Source-derived diagnosis: DirectoryArtifactReader opens the selected entry and directoryIoFailure converts every raw filesystem error to reason io. The selected-archive reader forwards that failure. Artifact failure translation retains io, so the shared projection chooses the generic artifact execution outcome. Classify the physical permission failure at its owning boundary and preserve its facts through translation, rather than infer it later from message text.

Additional context

Fetched origin/main at d4c637f4cfcb7ae3b48d0c3047f0e9171d43b26d. The relevant producer and classification lines are unchanged. Changes elsewhere in the two shared projection/presentation files concern captured-output inclusion and fixed resource allowance remediation. Later main was source-inspected, not executed. No repaired software or known-good revision is established.

Related reports require different repairs: #5953 firmware admission; #5970 JavaScript recovery snapshots; #5991 JavaScriptApplicationService filesystem fallback; #5997 bound Mach-O dispatch reads; #6004 model-reader host classification; #5873 resource-only bundle root double wrapping; #5949 selected-plist path-resolution errors. #5513 was fixed by merged PR #5577 at DesktopApplicationReader's root-selection boundary; that PR explicitly excludes Apple bundle readers and unreadable child entries. #5229 / PR #5230 concern retaining readable directory inventory when other entries cannot be read, and opt-in omissions; they do not classify this selected keyed-archive denial. #1188 / PR #1197 concern target admission through BinaryTargetError, which this later artifact read does not use.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6064 ### 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 `inspect-keyed-archive` reports a real read-permission denial on a selected bundle entry as `artifact_operation_failed` / `execution_failure`, reason `io`. It provides generic file-access advice instead of the existing `access_denied` classification and has no structured `system_code` or `filesystem-read` boundary. The path and EACCES remain in `details.detail` as text. The app root and Info.plist stay readable. Resource admission succeeds; the denial occurs when the selected archive is opened. Readable and permission-restored controls both succeed. This reproduced through the built CLI using real mode-bit denial; no filesystem errors or providers were simulated. ### Reproduction Run from a prepared source checkout as a non-root POSIX user. Set `REPRO_ROOT` to an existing disposable writable directory outside the checkout, save this executed driver as `archive-permission-reproduction.mjs`, then run `node archive-permission-reproduction.mjs`. It creates inert XML data, isolates CLI state, restores permissions and removes its fixture in `finally`. ```js import { mkdir, mkdtemp, writeFile, chmod, readFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import assert from 'node:assert/strict'; const root = await mkdtemp(join(process.env.REPRO_ROOT, 'permission-case-')); const app = join(root, 'Fixture.app'); const resources = join(app, 'Contents', 'Resources'); await mkdir(resources, { recursive: true }); await writeFile(join(app, 'Contents', 'Info.plist'), '<plist><dict><key>CFBundleIdentifier</key><string>org.example.fixture</string></dict></plist>'); const env = { ...process.env, REA_LOG_LEVEL: 'silent' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR']) { env[key] = join(root, key.toLowerCase()); await mkdir(env[key]); } const path = join(resources, 'Archive.plist'); await writeFile(path, '<plist><dict><key>$archiver</key><string>NSKeyedArchiver</string><key>$version</key><integer>100000</integer><key>$objects</key><array><string>$null</string></array><key>$top</key><dict><key>root</key><dict><key>CF$UID</key><integer>0</integer></dict></dict></dict></plist>'); const args = ['scripts/rea.mjs', 'inspect-keyed-archive', app, 'Contents/Resources/Archive.plist', '--json']; const run = (label) => { const child = spawnSync(process.execPath, args, { env, encoding: 'utf8', timeout: 45000 }); if (child.error) throw child.error; assert.equal(child.stderr, ''); const output = JSON.parse(child.stdout); const result = output.normalized_result ?? output; console.log(JSON.stringify({ label, exit: child.status, result }).replaceAll(root, '<fixture-root>')); return { exit: child.status, result }; }; try { const control = run('readable'); assert.equal(control.exit, 0); await chmod(path, 0); await assert.rejects(readFile(path), { code: 'EACCES' }); const denied = run('denied'); assert.equal(denied.exit, 1); assert.equal(denied.result.code, 'artifact_operation_failed'); assert.equal(denied.result.category, 'execution_failure'); assert.equal(denied.result.details.reason, 'io'); await chmod(path, 0o600); const restored = run('restored'); assert.equal(restored.exit, 0); } finally { await chmod(path, 0o600); await rm(root, { recursive: true, force: true }); } ``` ### Expected behavior Preserve host read-access denial as `access_denied` / `unavailable`, with the selected entry path, `EACCES` or `EPERM`, and existing read-access remediation. Keep ordinary malformed content, missing selections and unsupported formats distinguishable. [AnalysisAccessDeniedError explicitly models a denied selected read](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorCore.ts#L188-L204); [the documented keyed-archive operation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/native-investigation.md#L368-L373) selects an entry relative to the active bundle. ### Impact and scope P2: clients branching on typed codes receive an execution-failure classification for a host permission problem. This is consequential recovery/diagnostic behavior, with no successful incorrect decode or input modification. The raw EACCES permits manual diagnosis. Restoring authorized read permission was an observed workaround and made the same request succeed. Executed scope: one selected XML keyed archive on macOS through CLI. EPERM, denied directory traversal, other platforms, live MCP transport and naturally occurring downstream repair behavior were not tested. ### REA version `rea-agents` 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. Built CLI and source MCP tests at this revision. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0, non-root account. Repository Node pin: 24.18.0. No Hopper, Ghidra, IDA, ibtool or target application execution. ### Target and artifact context Self-authored resource-only app bundle with readable XML Info.plist and a Foundation NSKeyedArchiver XML entry at `Contents/Resources/Archive.plist`. Only the archive permission was changed to mode 000, then restored. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The readable/restored requests exited 0. An independent `readFile` rejected with EACCES before the denied request. The denied request exited 1 with empty stderr and these fields: ```json { "code": "artifact_operation_failed", "category": "execution_failure", "retryable": false, "message": "Artifact could not be read or written. Check file access and try again.", "details": { "operation": "inspect_keyed_archive", "reason": "io", "detail": "Could not open entry at <selected-archive> (EACCES): EACCES: permission denied, open '<selected-archive>'" } } ``` The path in the excerpt is a placeholder. No structured `system_code` is present. ### Typed outcome or recovery expected `access_denied`, category `unavailable`, retaining the actual read path and OS code with `boundary: "filesystem-read"` and existing read-access guidance. Retrying an unchanged denied read need not become automatically retryable. ### Analysis or suspected cause Source-derived diagnosis: [DirectoryArtifactReader opens the selected entry](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/DirectoryArtifactReader.ts#L264-L269) and [directoryIoFailure converts every raw filesystem error to reason io](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/DirectoryArtifactReader.ts#L501-L521). [The selected-archive reader forwards that failure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/apple/KeyedArchiveReader.ts#L219-L244). [Artifact failure translation](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactOperationFailure.ts#L20-L39) retains io, so the shared projection chooses the generic artifact execution outcome. Classify the physical permission failure at its owning boundary and preserve its facts through translation, rather than infer it later from message text. ### Additional context Fetched `origin/main` at `d4c637f4cfcb7ae3b48d0c3047f0e9171d43b26d`. The relevant producer and classification lines are unchanged. Changes elsewhere in the two shared projection/presentation files concern captured-output inclusion and fixed resource allowance remediation. Later main was source-inspected, not executed. No repaired software or known-good revision is established. Related reports require different repairs: #5953 firmware admission; #5970 JavaScript recovery snapshots; #5991 JavaScriptApplicationService filesystem fallback; #5997 bound Mach-O dispatch reads; #6004 model-reader host classification; #5873 resource-only bundle root double wrapping; #5949 selected-plist path-resolution errors. #5513 was fixed by merged PR #5577 at DesktopApplicationReader's root-selection boundary; that PR explicitly excludes Apple bundle readers and unreadable child entries. #5229 / PR #5230 concern retaining readable directory inventory when other entries cannot be read, and opt-in omissions; they do not classify this selected keyed-archive denial. #1188 / PR #1197 concern target admission through BinaryTargetError, which this later artifact read does not use.
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#3707
No description provided.