[GH-ISSUE #7524] [Bug] Unreadable child directory prevents extraction of readable inventoried files #4473

Closed
opened 2026-10-31 00:50:06 +00:00 by bsakaguchi · 0 comments
Owner

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

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

With a readable app bundle containing one unreadable child directory, node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json exits 1 with artifact_operation_failed, reason io, and an EACCES directory-opening diagnostic. It returns no output_root or extraction result for the readable files.

On the same unchanged bundle, node scripts/rea.mjs inspect-artifact "$WORK/Demo.app" --json exits 0. Its inventory retains three regular files with verified hashes, records the unreadable child as a traversal omission, and reports unknown container-entry coverage. The hidden file below that directory is not inventoried or selected for extraction.

Reproduction

  1. Use the built baseline below with Node 24.22.1, on macOS as an ordinary non-root user. Create an inert fixture and isolated state directories:

    WORK=$(mktemp -d)
    mkdir -p "$WORK/Demo.app/Contents/MacOS" \
      "$WORK/Demo.app/Contents/Resources" "$WORK/Demo.app/Contents/private" \
      "$WORK/home" "$WORK/config" "$WORK/cache" "$WORK/data" "$WORK/state" "$WORK/temp"
    cp /bin/ls "$WORK/Demo.app/Contents/MacOS/Demo"
    printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
      '<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Demo</string><key>CFBundleIdentifier</key><string>com.example.demo</string></dict></plist>' \
      > "$WORK/Demo.app/Contents/Info.plist"
    printf readable > "$WORK/Demo.app/Contents/Resources/ok.txt"
    printf hidden > "$WORK/Demo.app/Contents/private/hidden.txt"
    chmod 000 "$WORK/Demo.app/Contents/private"
    trap 'chmod 755 "$WORK/Demo.app/Contents/private"' EXIT
    
  2. Run inspection and extraction with isolated state and a temporary parent outside the source bundle:

    env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \
      XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \
      XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \
      node scripts/rea.mjs inspect-artifact "$WORK/Demo.app" --json
    env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \
      XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \
      XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \
      node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json
    
  3. Restore access and repeat the extraction command:

    chmod 755 "$WORK/Demo.app/Contents/private"
    env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \
      XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \
      XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \
      node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json
    

I ran inspection once, the denied extraction twice, and the readable control once through a Python subprocess driver with this fixture and environment isolation. Both denied extractions failed identically. The control exited 0, returned four artifacts with complete coverage, and all four output files matched their source bytes. Fixture permissions were restored. The copied executable was never launched.

Expected behavior

Extraction should publish the three readable inventoried files while retaining the unreadable-directory omission and unknown coverage in its result and outer Evidence. It must not claim to have copied the hidden subtree or to have produced a complete source copy.

The documented partial-extraction contract says observed regular files can still be copied when inventory or materialization traversal coverage is unknown, with the limitations retained. Here inspection has already established that every selected regular file is readable; only unobserved descendants are unavailable. The source locators are listed below.

Impact and scope

This input loses the whole extraction result despite retaining a usable partial inventory. The demonstrated scope is a directory-backed macOS app bundle through the CLI; MCP, Linux, Windows ACLs, and real installed applications were not tested. Source files remained intact, and failed attempts left no published extraction tree.

Restoring directory access worked for this owned fixture. That does not establish an available permission-changing workaround for a directory owned by someone else. Other workarounds were not exhaustively tested.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node v24.22.1; macOS 27.0.1, build 26A434; arm64. The non-root fixture owner ran both commands. No external analysis provider was required.

Target and artifact context

Synthetic Demo.app: an XML plist, a copy of the host's /bin/ls, a readable text resource, and one mode-000 child directory containing another text file.

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

The denied extraction command returned these fields twice, each with exit 1. The absolute fixture prefix is generalized as $WORK:

{
  "code": "artifact_operation_failed",
  "category": "execution_failure",
  "message": "Artifact could not be read or written. Check file access and try again.",
  "details": {
    "operation": "extract_artifact",
    "reason": "io",
    "detail": "Could not open directory at $WORK/Demo.app/Contents/private (EACCES): EACCES: permission denied, opendir '$WORK/Demo.app/Contents/private'"
  }
}

Inspection's inventory is at normalized_result.substeps[0].evidence.normalized_result. It contained:

coverage = {"scope":"container-entries","status":"unknown"}
limitation = Artifact traversal omitted directory-descendants at "Contents/private".
verified regular files = Contents/Info.plist, Contents/MacOS/Demo, Contents/Resources/ok.txt

This is inventory coverage; the outer inspection workflow separately reports partial workflow coverage.

Expected-contract locators at the baseline: docs/mcp-contracts.md:88-91 retains unknown coverage for omitted filesystem entries; docs/mcp-contracts.md:1233-1239 documents partial extraction and retained traversal limitations. tests/acceptance/artifactExtractionCoverage.test.ts:16-113 exercises partial directory extraction for opaque filenames on Linux, but does not cover this permission-denied child.

Typed outcome or recovery expected

With the child still unreadable, return successful extraction Evidence for the three inventoried files, coverage.status: "unknown", and both the omission and incomplete-source-copy limitations. Continue to refuse genuinely unavailable selected files or changed inventoried files, and preserve the readable control.

Analysis or suspected cause

Source-derived: inventory uses unreadableDirectories: "omit" in src/artifacts/inventory/reader.ts:39, while extraction uses "fail" in src/artifacts/extraction/ArtifactExtraction.ts:637. src/artifacts/DirectoryArtifactReader.ts:150-166 records access-denied traversal omissions only in omit mode. Extraction's second traversal at src/artifacts/extraction/ArtifactExtraction.ts:430 therefore throws instead of reaching the unknown-coverage result supported at lines 529-543.

Earlier readable files may be copied transiently before that throw; this verification establishes failed publication and rollback, not that no writes ever occurred.

Additional context

Related open #5229 and PR #5230 concern successful partial directory analysis and honest application-graph coverage. Their closure does not establish partial extraction publication. Closed #6852 concerned successful extraction dropping unknown coverage for an opaque filename; this fixture instead aborts, even with all selected files available. This report does not claim a recurrence of that successful-but-falsely-complete outcome or a known last-good release.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A reproducible child-directory permission condition blocks the entire extraction result for this otherwise inspectable bundle. The source remains intact and the failure is limited to affected inputs.
  • Workaround status: available
  • Workaround basis: Restoring access to the owned fixture's child directory made the same extraction succeed with byte-identical output. Availability for foreign-owned directories remains unknown.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7524 ### 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 With a readable app bundle containing one unreadable child directory, `node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json` exits 1 with `artifact_operation_failed`, reason `io`, and an `EACCES` directory-opening diagnostic. It returns no `output_root` or extraction result for the readable files. On the same unchanged bundle, `node scripts/rea.mjs inspect-artifact "$WORK/Demo.app" --json` exits 0. Its inventory retains three regular files with verified hashes, records the unreadable child as a traversal omission, and reports unknown container-entry coverage. The hidden file below that directory is not inventoried or selected for extraction. ### Reproduction 1. Use the built baseline below with Node 24.22.1, on macOS as an ordinary non-root user. Create an inert fixture and isolated state directories: ```sh WORK=$(mktemp -d) mkdir -p "$WORK/Demo.app/Contents/MacOS" \ "$WORK/Demo.app/Contents/Resources" "$WORK/Demo.app/Contents/private" \ "$WORK/home" "$WORK/config" "$WORK/cache" "$WORK/data" "$WORK/state" "$WORK/temp" cp /bin/ls "$WORK/Demo.app/Contents/MacOS/Demo" printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \ '<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Demo</string><key>CFBundleIdentifier</key><string>com.example.demo</string></dict></plist>' \ > "$WORK/Demo.app/Contents/Info.plist" printf readable > "$WORK/Demo.app/Contents/Resources/ok.txt" printf hidden > "$WORK/Demo.app/Contents/private/hidden.txt" chmod 000 "$WORK/Demo.app/Contents/private" trap 'chmod 755 "$WORK/Demo.app/Contents/private"' EXIT ``` 2. Run inspection and extraction with isolated state and a temporary parent outside the source bundle: ```sh env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \ XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \ XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \ node scripts/rea.mjs inspect-artifact "$WORK/Demo.app" --json env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \ XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \ XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \ node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json ``` 3. Restore access and repeat the extraction command: ```sh chmod 755 "$WORK/Demo.app/Contents/private" env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \ XDG_CACHE_HOME="$WORK/cache" XDG_DATA_HOME="$WORK/data" \ XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/temp" \ node scripts/rea.mjs extract-artifact "$WORK/Demo.app" --json ``` I ran inspection once, the denied extraction twice, and the readable control once through a Python subprocess driver with this fixture and environment isolation. Both denied extractions failed identically. The control exited 0, returned four artifacts with complete coverage, and all four output files matched their source bytes. Fixture permissions were restored. The copied executable was never launched. ### Expected behavior Extraction should publish the three readable inventoried files while retaining the unreadable-directory omission and unknown coverage in its result and outer Evidence. It must not claim to have copied the hidden subtree or to have produced a complete source copy. The documented partial-extraction contract says observed regular files can still be copied when inventory or materialization traversal coverage is unknown, with the limitations retained. Here inspection has already established that every selected regular file is readable; only unobserved descendants are unavailable. The source locators are listed below. ### Impact and scope This input loses the whole extraction result despite retaining a usable partial inventory. The demonstrated scope is a directory-backed macOS app bundle through the CLI; MCP, Linux, Windows ACLs, and real installed applications were not tested. Source files remained intact, and failed attempts left no published extraction tree. Restoring directory access worked for this owned fixture. That does not establish an available permission-changing workaround for a directory owned by someone else. Other workarounds were not exhaustively tested. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node `v24.22.1`; macOS `27.0.1`, build `26A434`; arm64. The non-root fixture owner ran both commands. No external analysis provider was required. ### Target and artifact context Synthetic `Demo.app`: an XML plist, a copy of the host's `/bin/ls`, a readable text resource, and one mode-000 child directory containing another text file. ### Interface CLI ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence The denied extraction command returned these fields twice, each with exit 1. The absolute fixture prefix is generalized as `$WORK`: ```json { "code": "artifact_operation_failed", "category": "execution_failure", "message": "Artifact could not be read or written. Check file access and try again.", "details": { "operation": "extract_artifact", "reason": "io", "detail": "Could not open directory at $WORK/Demo.app/Contents/private (EACCES): EACCES: permission denied, opendir '$WORK/Demo.app/Contents/private'" } } ``` Inspection's inventory is at `normalized_result.substeps[0].evidence.normalized_result`. It contained: ```text coverage = {"scope":"container-entries","status":"unknown"} limitation = Artifact traversal omitted directory-descendants at "Contents/private". verified regular files = Contents/Info.plist, Contents/MacOS/Demo, Contents/Resources/ok.txt ``` This is inventory coverage; the outer inspection workflow separately reports partial workflow coverage. Expected-contract locators at the baseline: `docs/mcp-contracts.md:88-91` retains unknown coverage for omitted filesystem entries; `docs/mcp-contracts.md:1233-1239` documents partial extraction and retained traversal limitations. `tests/acceptance/artifactExtractionCoverage.test.ts:16-113` exercises partial directory extraction for opaque filenames on Linux, but does not cover this permission-denied child. ### Typed outcome or recovery expected With the child still unreadable, return successful extraction Evidence for the three inventoried files, `coverage.status: "unknown"`, and both the omission and incomplete-source-copy limitations. Continue to refuse genuinely unavailable selected files or changed inventoried files, and preserve the readable control. ### Analysis or suspected cause Source-derived: inventory uses `unreadableDirectories: "omit"` in `src/artifacts/inventory/reader.ts:39`, while extraction uses `"fail"` in `src/artifacts/extraction/ArtifactExtraction.ts:637`. `src/artifacts/DirectoryArtifactReader.ts:150-166` records access-denied traversal omissions only in omit mode. Extraction's second traversal at `src/artifacts/extraction/ArtifactExtraction.ts:430` therefore throws instead of reaching the unknown-coverage result supported at lines 529-543. Earlier readable files may be copied transiently before that throw; this verification establishes failed publication and rollback, not that no writes ever occurred. ### Additional context Related open [#5229](https://github.com/morluto/rea/issues/5229) and [PR #5230](https://github.com/morluto/rea/pull/5230) concern successful partial directory analysis and honest application-graph coverage. Their closure does not establish partial extraction publication. Closed [#6852](https://github.com/morluto/rea/issues/6852) concerned successful extraction dropping unknown coverage for an opaque filename; this fixture instead aborts, even with all selected files available. This report does not claim a recurrence of that successful-but-falsely-complete outcome or a known last-good release. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A reproducible child-directory permission condition blocks the entire extraction result for this otherwise inspectable bundle. The source remains intact and the failure is limited to affected inputs. - Workaround status: available - Workaround basis: Restoring access to the owned fixture's child directory made the same extraction succeed with byte-identical output. Availability for foreign-owned directories remains unknown.
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#4473
No description provided.