Skip to content

Unpacked files lose +x on extract; nested paths under directory symlinks fail; missing-path lookups mutate the header #499

Open

Description

@trackoor

Three small problems in the read/extract side of the API, all on 4.3.2 (current main).

1. Unpacked executables lose their executable bit in extractAll

Filesystem.insertFile returns early for unpacked files without recording executable, so the header entry has no executable: true and extractAll writes the file as 0644. The copy in app.asar.unpacked is still 0755, so it is only the extract path that loses it. Packed (non-unpacked) executables keep their mode.

// npm i @electron/asar@4.3.2, save as repro.mjs; in an empty dir first run: mkdir app && printf '#!/bin/sh\n' > app/run.sh && chmod 755 app/run.sh
import * as asar from '@electron/asar';
import fs from 'node:fs';
await asar.createPackageWithOptions('app', 'app.asar', { unpack: '*.sh' });
console.log(asar.statFile('app.asar', 'run.sh').executable);
console.log((fs.statSync('app.asar.unpacked/run.sh').mode & 0o777).toString(8));
asar.extractAll('app.asar', 'out');
console.log((fs.statSync('out/run.sh').mode & 0o777).toString(8));

Actual: undefined, 755, 644.
Expected: true, 755, 755, the same as for a packed executable (asar extract should give back the tree that was packed).

2. Paths more than one level below a directory symlink cannot be read

#318 made getNode follow a link when it is the immediate parent, but searchNodeFromDirectory still walks every earlier component as a plain directory and throws when one of them is a link.

// mkdir -p src/real/sub && echo hi > src/real/sub/f.txt && echo top > src/real/g.txt && ln -s real src/linkdir
import * as asar from '@electron/asar';
await asar.createPackage('src', 'a.asar');
console.log(asar.extractFile('a.asar', 'linkdir/g.txt').toString());     // works
console.log(asar.extractFile('a.asar', 'linkdir/sub/f.txt').toString()); // throws

Actual: Error: Unexpected directory state while traversing: linkdir/sub.
Expected: hi\n, the same as real/sub/f.txt (and what fs.readFileSync('src/linkdir/sub/f.txt') returns).

3. Looking up a missing path inserts it into the cached header

searchNodeFromDirectory creates any missing intermediate directory while it searches. The archive's Filesystem is cached, so a failed statFile/extractFile leaves an empty directory behind that later shows up in listPackage and is created by extractAll.

// same a.asar as above
import * as asar from '@electron/asar';
import fs from 'node:fs';
try { asar.statFile('a.asar', 'missing/x.txt'); } catch (e) { console.log(e.message); }
console.log(asar.listPackage('a.asar', {}).includes('/missing'));
asar.extractAll('a.asar', 'out2');
console.log(fs.existsSync('out2/missing'));

Actual: "missing/x.txt" was not found in this archive, then true, true.
Expected: the error, then false, false; a read should not change the archive's listing.

Related: since the header read from disk is a plain JSON.parse object, asar.statFile('a.asar', 'toString') returns Object.prototype.toString instead of throwing "not found".

Version: @electron/asar@4.3.2 (main e621701), Node.js v26.13.0.

Found with a differential-testing tool; the report was drafted with AI assistance and reviewed and edited by me.

Credit: https://ucsbplse.github.io/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions