Files
electron/script
Samuel Attard 7377022f03 feat: serve asar file descriptors, streams and copies from the archive (#52833)
* feat: serve asar file descriptors, streams and copies from the archive

fs.open/openSync/promises.open, fs.createReadStream, fs.copyFile/cp (and
their sync/promise variants) previously extracted a packed asar entry into a
temporary file first (Archive::CopyFileOut): a synchronous, main-thread copy of
the whole file on first access, a temp copy left behind for the life of the
process, and a cached copy that later opens with write flags could corrupt.

The wrapper now hands out real file descriptors duplicated from the archive's
retained handle and hooks the fs binding so every fd-based API (read/readv/
fstat/close/readFile(fd), ReadStream, FileHandle) reads straight out of the
archive. With ASAR integrity enabled, bytes are only ever served from a
block whose hash was just verified, and a violation terminates the process
synchronously instead of after the tampered data was already handed back.
Copies stream from the archive to the destination; opening a packed file for
writing fails with EACCES; fchmod/fchown/futimes on such descriptors are
refused. Only process.dlopen and child_process.execFile still need a real
path and keep using the temporary copy (now made read-only).

Also adds fs.opendir/readlink support for archives, makes fs.stat follow
archive symlinks (fs.cp dereference now works), returns BigIntStats when
asked, and gives original-fs its own pristine copy of the fs binding so it is
unaffected by these overrides.

* docs: note the asar file descriptor behavior change in breaking-changes

* fix: address review of the asar fd reader

- readv on a packed entry: a zero-length buffer no longer ends the clamped
  read early; later buffers are still filled, like preadv.
- copyFile with COPYFILE_FICLONE_FORCE on an unpacked entry is passed to
  native fs (which may reflink); only packed entries reject it with ENOTSUP.
- FileHandle#close() forgets the descriptor entry immediately instead of
  waiting for the C++ handle's fd to be reset after the threadpool close, so
  a real file that reuses the number in that window is never routed through
  the archive reader.
- open() of a symbolic link to a directory reports EISDIR, not ENOENT.
- readlink and opendir honour name encodings other than 'buffer'.
- recursive readdir withFileTypes skips an unreadable archive entry instead
  of dropping the rest of the listing.

* fix: keep the asar fs binding hooks working when Array's iterator is deleted

- Forward pass-through calls with Function#apply and index access instead of
  spread / array destructuring, which need Array.prototype[Symbol.iterator]
  (user code may delete it; parallel/test-require-delete-array-iterator).
- Use literal permission bits for the fake asar stats: the S_IRGRP/S_IROTH/
  S_IX* constants do not exist on Windows, so the modes came out as 0600.
- Allow blocking around the chmod of a copied-out temp file; CopyFileOut is
  also reached from the UI thread via WebContents.downloadURL().

* fix: report the executable bit from asar stat and drop stale fd entries

- Archive::Stat now exposes `executable`, so fs.stat/lstat of a packed
  executable report 0755 like fs.fstat does; fs.cp no longer strips the bit
  that copyFile preserved when it chmods the destination to the source mode.
- A descriptor for a packed entry can be closed without going through
  fs.close (a native FileHandle built on it, e.g. http2's respondWithFile,
  a FileHandle moved to a worker, or one closed on GC). Whenever fs hands out
  a descriptor number (native pass-through open or a new archive-backed one),
  any stale entry for that number is dropped first, so it can never capture
  the new owner's reads, and its retained block is returned to the budget.
- Document http2stream.respondWithFile()/respondWithFD() among the APIs
  that read the raw descriptor.

* fix: hand out a sentinel descriptor for packed asar entries, not the archive

The descriptor returned by open() for a packed entry used to be a dup of the
archive's own handle. Code that read it directly instead of through fs (a
native addon, a child's stdio, net.Socket({ fd }), http2's respondWithFile)
silently got bytes of the archive at the wrong offset, and a raw handle to
the archive escaped to user code.

Hand out a per-open dup of a write-only null device instead. It identifies
the entry to fs, which serves every read from the archive's retained handle
positionally as before, and it is useless to anything else: direct reads
fail with EBADF, the archive itself is never exposed, and a descriptor
closed behind our back can at worst misroute through fs, never leak archive
bytes. Writes, truncation and metadata changes through such a descriptor are
refused with EACCES rather than vanishing into the null device. copyFile and
the writeFile fast path read the entry without allocating a descriptor at
all.

* chore: clang-format electron_api_asar.cc

* fix: use libuv error numbers for asar fs errors

libuv's error numbers differ from POSIX errno on Windows (UV_EACCES is
-4092 there, not -13) and Node decodes `errno` through libuv's table, so the
hand-written errno values decoded as UNKNOWN on Windows. Take them from the
uv binding instead.

Also refuse writeFileSync's utf8 fast path when it is given a packed entry's
descriptor rather than a path.
2026-08-21 23:03:41 -07:00
..
2025-05-30 10:59:11 +02:00
2026-03-18 19:14:49 -04:00
2026-02-18 13:24:29 -05:00
2024-03-21 09:48:23 -04:00
2025-12-10 14:04:14 -05:00