Extracting embedded files from a compiled Deno binary
Table of Contents
How to patch a Deno 2.7.0 Windows executable to extract its embedded VFS files, and what this method does not recover. The self-extraction mechanism was introduced in Deno 2.7.0 (February 25, 2026) with the addition of the Earlier versions lack the Deno is a JavaScript and TypeScript runtime created by Ryan Dahl, the original author of Node.js. The The output is a single The user can run the binary without installing Deno. Here we examine how it stores embedded files and how to extract the VFS contents. We add a JSON field to the binary’s metadata and use Deno’s built-in self-extraction mechanism to write the embedded VFS files to disk. This does not reconstruct the entire original repository or extract every separate module store. Deno embeds its data into a PE resource named When a compiled binary starts, Deno locates the The executable is mapped into the process address space. In the VFS read path shown below, This VFS read path does not write the embedded files to disk. That says nothing about other runtime paths or the application itself; either can perform disk IO, and accessing mapped pages can also require disk reads. If the metadata contains a The function recursively walks the VFS and writes its files to disk (unless the The extraction directory depends on the runtime version, executable, platform, environment, and writable locations. This mode was originally designed for native modules (FFI, Self-extraction is not a passive unpacking tool: starting the patched binary also executes the application. For untrusted binaries, use a disposable isolated environment with no secrets and no network access. Do not run them on your normal workstation. We add Inserting bytes into the metadata without adjusting lengths and offsets can break the binary. The patch needs to account for data layout and file size: We use We get three values: Knowing the section offset, we read just the first few kilobytes — magic + length + JSON: If the JSON length changes, we shift the bytes that follow it. We use We update the recorded resource size. Patcher code: GitHub Gist The only external dependency. Everything else is Python’s standard library. We patch the metadata to enable Deno’s built-in VFS extraction routine: This extracts embedded VFS files, not the complete original repository or every module store. The method is limited here to Deno 2.7.0 Windows executables; running the patched binary also runs the application.--self-extracting flag for deno compile. The analysis below targets Deno v2.7.0 and Windows binaries (PE). The Python script uses pefile; it is not an ELF/Mach-O extractor, and compatibility with later Deno releases has not been verified.--self-extracting flag used here, so this workflow does not apply to them. Extracting their embedded data requires a separate approach, such as parsing the relevant binary format. This is not a claim that those runtimes never write files to disk.What is
deno compiledeno compile command bundles a project into a single self-contained executable:deno compile --allow-net --allow-read src/main.ts.exe (Windows), ELF (Linux), or Mach-O (macOS) binary containing:Structure of a standalone binary
The
d3n0l4nd sectionD3N0L4ND (Windows) or an ELF/Mach-O section named d3n0l4nd. The embedded data is laid out sequentially. The diagram marks the u64 length fields:d3n0l4nd ← 8 bytes, opening magic
┌──────────────────────────────────┐
│ u64: metadata_len │
│ metadata JSON │ ← entrypoint, permissions, settings
├──────────────────────────────────┤
│ u64: npm_snapshot_len │
│ npm snapshot blob │ ← npm dependency graph
├──────────────────────────────────┤
│ specifiers store │ ← module URLs → IDs
├──────────────────────────────────┤
│ redirects store │ ← module redirects
├──────────────────────────────────┤
│ remote modules store │ ← HTTP import sources
├──────────────────────────────────┤
│ u64: vfs_headers_len │
│ VFS headers JSON │ ← directory tree with offset/size
├──────────────────────────────────┤
│ u64: vfs_file_data_len │
│ VFS file data blob │ ← raw bytes of all embedded files
└──────────────────────────────────┘
d3n0l4nd ← 8 bytes, closing magicTwo runtime modes
d3n0l4nd section and deserializes its contents. From there, two paths are possible.Default mode — reading the embedded VFS
fn find_section() -> Result<&'static [u8], AnyError> {
match libsui::find_section("d3n0l4nd") {
Ok(Some(data)) => Ok(data), // direct pointer into process memory
...
}
}FileBackedVfs computes the offset and returns a slice of the embedded bytes rather than opening a separate file:let start = self.start_file_offset + file.offset;
let end = start + file.len;
Ok(Cow::Borrowed(&self.data[start..end]))
// No disk IO — just indexing into a byte arraySelf-extracting mode — unpacking to disk
"self_extracting" field, the runtime calls extract_vfs_to_disk() on startup:let root_path = if let Some(hash) = &metadata.self_extracting {
let dir = choose_and_create_extraction_dir(hash)?;
dir
} else {
std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name))
};.done marker causes it to skip extraction). Separate module stores are outside this traversal:pub fn extract_vfs_to_disk(vfs: &FileBackedVfs, extraction_dir: &Path) -> Result<()> {
// If the .done marker exists — extraction already happened, skip
let done_marker = extraction_dir.join(".done");
if done_marker.exists() {
return Ok(());
}
// Recursively write ALL files from VFS to disk
extract_vfs_dir(vfs, vfs.root_dir(), &mut extraction_dir.to_path_buf())?;
// Marker: extraction won't repeat on the next run
std::fs::File::create(&done_marker)?;
Ok(())
}taskflow is an example application name, not a universal Deno directory. To determine an exact path, check choose_and_create_extraction_dir and its callers in the source for the matching runtime version, then verify the location in your isolated environment..node addons) that cannot be loaded directly from memory.Patching: enabling self-extraction in an existing binary
"self_extracting": "<hash>" to the metadata JSON inside the binary. On the next run, Deno uses its built-in extraction routine to write the VFS files to disk, unless a .done marker is already present.What the patch needs to account for
u64 with the exact byte size sits before the JSON. Adding a field increases the JSON by ~37 bytes → the u64 no longer matches.D3N0L4ND resource. Inserting bytes breaks it too.Step-by-step algorithm
Step 1. Locate the
D3N0L4ND resource in the PEpefile in fast_load mode, then explicitly parse the resource directory:def locate_resource(exe_path: str) -> ResourceLocation:
pe = pefile.PE(exe_path, fast_load=True)
pe.parse_data_directories(
directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]
)
for rsrc_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
for rsrc_id in rsrc_type.directory.entries:
name = rsrc_id.name.string.upper() if rsrc_id.name else b""
if name == b"D3N0L4ND":
lang = rsrc_id.directory.entries[0]
rva = lang.data.struct.OffsetToData
return ResourceLocation(
file_offset=pe.get_offset_from_rva(rva),
size=lang.data.struct.Size,
struct_file_offset=lang.data.struct.get_file_offset(),
)file_offset — where the d3n0l4nd section starts in the filesize — the current section sizestruct_file_offset — the file offset of the IMAGE_RESOURCE_DATA_ENTRY structure; its Size field is updated in step 6Step 2. Read the metadata JSON
def read_metadata(f, section_offset: int) -> MetadataInfo:
f.seek(section_offset)
if f.read(8) != MAGIC:
raise RuntimeError("Bad opening magic")
length = struct.unpack("<Q", f.read(8))[0]
json_offset = section_offset + 16 # 8 (magic) + 8 (u64)
raw = f.read(length)
return MetadataInfo(
json_file_offset=json_offset,
original_bytes=raw,
parsed=json.loads(raw),
)Step 3. Modify the JSON
meta.parsed["self_extracting"] = hash_value
new_bytes = json.dumps(meta.parsed, separators=(",", ":")).encode()
diff = len(new_bytes) - len(meta.original_bytes)
# diff ≈ +37 bytesStep 4. Shift the file tail
mmap rather than creating a separate copy of the entire file:def shift_bytes(f, *, start: int, amount: int) -> None:
if amount == 0:
return
f.seek(0, 2)
old_size = f.tell()
tail = old_size - start
if amount > 0:
# Expand the file, then shift the block
f.truncate(old_size + amount)
f.flush()
with mmap.mmap(f.fileno(), 0) as mm:
mm.move(start + amount, start, tail)
else:
# Shrink: shift first, then truncate
with mmap.mmap(f.fileno(), 0) as mm:
mm.move(start + amount, start, tail)
f.truncate(old_size + amount)
f.flush()mmap.move() is a wrapper over memmove. It correctly handles overlapping memory regions. Memory mapping avoids a separate Python buffer for the whole file, but moving bytes still accesses memory pages and may cause disk IO.Step 5. Write the new metadata
f.seek(meta.json_file_offset - 8)
f.write(struct.pack("<Q", len(new_bytes)))
f.write(new_bytes)Step 6. Update the PE resource size
IMAGE_RESOURCE_DATA_ENTRY.Size is a u32 at offset +4 from the start of the structure:def patch_resource_size(path: str, struct_offset: int, new_size: int) -> None:
with open(path, "r+b") as f:
f.seek(struct_offset + 4)
f.write(struct.pack("<I", new_size))Full code
Installing dependencies
pip install pefileSummary
d3n0l4nd section"self_extracting": "<hash>" field to this JSONmmap.move()u64 metadata length and the u32 PE resource sizeextract_vfs_to_disk() → embedded VFS files end up on disk, unless extraction is skipped because .done already exists