Extracting embedded files from a compiled Deno binary

Deno version

The self-extraction mechanism was introduced in Deno 2.7.0 (February 25, 2026) with the addition of the --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.

Prior to 2.7.0

Earlier versions lack the --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 compile

Deno is a JavaScript and TypeScript runtime created by Ryan Dahl, the original author of Node.js. The deno compile command bundles a project into a single self-contained executable:

deno compile --allow-net --allow-read src/main.ts

The output is a single .exe (Windows), ELF (Linux), or Mach-O (macOS) binary containing:

  • Deno runtime — the V8 engine and Rust wrapper
  • Embedded project files — a virtual file system (VFS)
  • Bundled dependencies — npm packages, JSR modules, HTTP imports; some module data lives in separate stores
  • Metadata — entrypoint, permissions, configuration

The user can run the binary without installing Deno. Here we examine how it stores embedded files and how to extract the VFS contents.

TL;DR

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.


Structure of a standalone binary

The d3n0l4nd section

Deno embeds its data into a PE resource named D3N0L4ND (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 magic

Two runtime modes

When a compiled binary starts, Deno locates the 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
        ...
    }
}

The executable is mapped into the process address space. In the VFS read path shown below, 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 array

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.

Self-extracting mode — unpacking to disk

If the metadata contains a "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))
};

The function recursively walks the VFS and writes its files to disk (unless the .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(())
}

The extraction directory depends on the runtime version, executable, platform, environment, and writable locations. 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.

INFO

This mode was originally designed for native modules (FFI, .node addons) that cannot be loaded directly from memory.


Patching: enabling self-extraction in an existing binary

Running the binary executes the application

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 "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

Inserting a field is not enough

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:

  1. Metadata is length-prefixed. A u64 with the exact byte size sits before the JSON. Adding a field increases the JSON by ~37 bytes → the u64 no longer matches.
  2. Sections are packed tightly. No padding — the npm snapshot starts on the very next byte after the metadata JSON.
  3. The PE resource has its own Size. The PE header records the size of the D3N0L4ND resource. Inserting bytes breaks it too.
  4. The file can be large. Deno standalone binaries can exceed 100 MB. We use memory mapping to avoid creating a separate in-memory copy of the whole file.

Step-by-step algorithm

Step 1. Locate the D3N0L4ND resource in the PE

We use pefile 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(),
                )

We get three values:

  • file_offset — where the d3n0l4nd section starts in the file
  • size — the current section size
  • struct_file_offset — the file offset of the IMAGE_RESOURCE_DATA_ENTRY structure; its Size field is updated in step 6

Step 2. Read the metadata JSON

Knowing the section offset, we read just the first few kilobytes — magic + length + 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 bytes

Step 4. Shift the file tail

If the JSON length changes, we shift the bytes that follow it. We use 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()
Why mmap?

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

We update the recorded 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

Patcher code: GitHub Gist

Installing dependencies

pip install pefile
NOTE

The only external dependency. Everything else is Python’s standard library.


Summary

We patch the metadata to enable Deno’s built-in VFS extraction routine:

  1. Metadata is stored as length-prefixed JSON at the beginning of the d3n0l4nd section
  2. We add a "self_extracting": "<hash>" field to this JSON
  3. We shift all bytes after the metadata via mmap.move()
  4. We update two numbers: the u64 metadata length and the u32 PE resource size
  5. On startup, Deno sees the flag → calls extract_vfs_to_disk() → embedded VFS files end up on disk, unless extraction is skipped because .done already exists

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.