Skip to content

Instantly share code, notes, and snippets.

@Hona
Created April 30, 2026 02:40
Show Gist options
  • Select an option

  • Save Hona/b2c876afa74d71c662acc3e5279a2203 to your computer and use it in GitHub Desktop.

Select an option

Save Hona/b2c876afa74d71c662acc3e5279a2203 to your computer and use it in GitHub Desktop.
WSL1 Bun build --compile ELF ENOEXEC repro

WSL1 Bun build --compile ELF repro

Repro bundle for oven-sh/bun#29963.

Run inside WSL1:

sudo apt-get update
sudo apt-get install -y curl unzip file binutils strace
bash repro-bun-compile-wsl1.sh

Files:

  • repro-bun-compile-wsl1.sh: minimal Bun-only repro using official Bun 1.3.11 and 1.3.13 releases.
  • repro-opencode-release-wsl1.sh: real-world confirmation with public opencode releases.
  • experiment-move-bun-into-rw-load.sh: validates the alternate ELF shape where .bun is mapped through the existing RW PT_LOAD and runs on WSL1.
  • experiment-extend-rw-segment.sh: failed intermediate experiment showing why .bun must be moved past old BSS before extending RW PT_LOAD.

Expected primary repro result on WSL1:

Bun 1.3.11 compiled output: runs
Bun 1.3.13 compiled output: execve(...) = -1 ENOEXEC
/lib64/ld-linux-x86-64.so.2 ./hello-1.3.13: runs
#!/usr/bin/env bash
set -euo pipefail
cd "${1:-/tmp/bun-wsl1-elf-repro-verified}"
cp hello-1.3.13 hello-1.3.13-extend-rw-current
new_filesz=$((0x603a000 + 0x1000 - 0x5f96050))
new_vaddr=$((0x62ae050 + 0x603a000 - 0x5f96050))
printf "new_filesz=0x%x new_vaddr=0x%x\n" "$new_filesz" "$new_vaddr"
perl -e '
open my $f, "+<", $ARGV[0] or die $!;
binmode $f;
sub w64 { my ($off, $v) = @_; seek $f, $off, 0; print $f pack("Q<", $v); }
sub w32 { my ($off, $v) = @_; seek $f, $off, 0; print $f pack("L<", $v); }
w64(320, $ARGV[1]); # RW PT_LOAD p_filesz
w64(328, $ARGV[1]); # RW PT_LOAD p_memsz
w32(456, 0x6474e551); # appended .bun PT_LOAD -> PT_GNU_STACK
w64(0x6020000, $ARGV[2]); # BUN_COMPILED.size original slot -> mapped vaddr
close $f;
' hello-1.3.13-extend-rw-current "$new_filesz" "$new_vaddr"
readelf -lW hello-1.3.13-extend-rw-current | awk '/Program Headers:/,/Section to Segment mapping:/'
./hello-1.3.13-extend-rw-current
#!/usr/bin/env bash
set -euo pipefail
cd "${1:-/tmp/bun-wsl1-elf-repro-verified}"
cp hello-1.3.13 hello-1.3.13-rw-load-fixed-shape
perl -e '
use strict;
use warnings;
use Fcntl qw(SEEK_SET);
my $path = shift;
open my $f, "+<", $path or die $!;
binmode $f;
local $/;
my $data = <$f>;
sub u64 { unpack("Q<", substr($data, $_[0], 8)) }
sub w64 { substr($data, $_[0], 8) = pack("Q<", $_[1]) }
sub w32 { substr($data, $_[0], 4) = pack("L<", $_[1]) }
my $rw_ph = 64 + 4 * 56;
my $extra_ph = 64 + 7 * 56;
my $shnum = unpack("S<", substr($data, 60, 2));
my $shentsize = unpack("S<", substr($data, 58, 2));
my $old_shoff = u64(40);
my $old_shdr_size = $shnum * $shentsize;
my $rw_off = u64($rw_ph + 8);
my $rw_vaddr = u64($rw_ph + 16);
my $rw_filesz = u64($rw_ph + 32);
my $rw_memsz = u64($rw_ph + 40);
my $rw_align = u64($rw_ph + 48);
my $old_file_backed_end = $rw_off + $rw_filesz;
my $old_mem_end_vaddr = $rw_vaddr + $rw_memsz;
my $new_vaddr = ($old_mem_end_vaddr + $rw_align - 1) & ~($rw_align - 1);
my $new_file_offset = $rw_off + ($new_vaddr - $rw_vaddr);
my $aligned_payload_size = 0x1000;
my $new_shoff = $new_file_offset + $aligned_payload_size;
my $new_file_size = $new_shoff + $old_shdr_size;
my $new_filesz = $new_file_offset + $aligned_payload_size - $rw_off;
my $payload = substr($data, 0x603a000, $aligned_payload_size);
my $shdrs = substr($data, $old_shoff, $old_shdr_size);
if (length($data) < $new_file_size) {
$data .= "\0" x ($new_file_size - length($data));
}
# Any bytes between the old file-backed data and the moved payload become
# file-backed BSS after extending the RW segment. They must be zero.
substr($data, $old_file_backed_end, $new_file_offset - $old_file_backed_end) = "\0" x ($new_file_offset - $old_file_backed_end);
substr($data, $new_file_offset, $aligned_payload_size) = $payload;
substr($data, $new_shoff, $old_shdr_size) = $shdrs;
w64(40, $new_shoff);
w64($rw_ph + 32, $new_filesz);
w64($rw_ph + 40, $new_filesz);
w32($extra_ph, 0x6474e551);
w64(0x6020000, $new_vaddr);
# Section header #25 is .bun.
my $bun_sh = $new_shoff + 25 * $shentsize;
w64($bun_sh + 16, $new_vaddr);
w64($bun_sh + 24, $new_file_offset);
truncate $f, 0;
seek $f, 0, SEEK_SET;
print $f $data;
close $f;
printf "rw_off=0x%x rw_vaddr=0x%x old_file_backed_end=0x%x old_mem_end_vaddr=0x%x\n", $rw_off, $rw_vaddr, $old_file_backed_end, $old_mem_end_vaddr;
printf "new_file_offset=0x%x new_vaddr=0x%x new_filesz=0x%x new_shoff=0x%x\n", $new_file_offset, $new_vaddr, $new_filesz, $new_shoff;
' hello-1.3.13-rw-load-fixed-shape
readelf -lW hello-1.3.13-rw-load-fixed-shape | awk '/Program Headers:/,/Section to Segment mapping:/'
readelf -SW hello-1.3.13-rw-load-fixed-shape | grep -E '(^ \[Nr\]|\.bun)'
./hello-1.3.13-rw-load-fixed-shape
#!/usr/bin/env bash
set -euo pipefail
# Minimal WSL1 repro for Bun compiled executables failing with ENOEXEC.
# Run inside WSL1. Dependencies: bash, curl, unzip, file, binutils/readelf, strace.
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
need() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "missing dependency: $1" >&2
echo "install on Debian/Ubuntu with: sudo apt-get update && sudo apt-get install -y curl unzip file binutils strace" >&2
exit 1
fi
}
need curl
need unzip
need file
need readelf
need strace
need dd
workdir="${1:-/tmp/bun-wsl1-elf-repro}"
rm -rf "$workdir"
mkdir -p "$workdir"
cd "$workdir"
echo "== environment =="
uname -a
case "$(uname -r)" in
*Microsoft*) echo "WSL kernel string detected" ;;
*) echo "warning: this does not look like WSL1" ;;
esac
cat > hello.js <<'JS'
console.log("hello from compiled bun")
JS
download_bun() {
local version="$1"
local zip="bun-linux-x64-baseline.zip"
local url="https://github.com/oven-sh/bun/releases/download/bun-v${version}/${zip}"
echo "== downloading Bun ${version} =="
mkdir -p "bun-${version}"
curl -fsSLo "bun-${version}.zip" "$url"
unzip -q "bun-${version}.zip" -d "bun-${version}"
mv "bun-${version}/bun-linux-x64-baseline/bun" "bun-${version}/bun"
chmod +x "bun-${version}/bun"
"./bun-${version}/bun" --version
}
compile_with() {
local version="$1"
echo "== compiling hello.js with Bun ${version} =="
"./bun-${version}/bun" build --compile ./hello.js --outfile "./hello-${version}"
file "./hello-${version}"
}
run_direct() {
local version="$1"
echo "== direct exec: hello-${version} =="
set +e
"./hello-${version}" >"run-${version}.stdout" 2>"run-${version}.stderr"
local status=$?
set -e
echo "exit=${status}"
sed -n '1,20p' "run-${version}.stdout"
sed -n '1,20p' "run-${version}.stderr"
}
show_elf() {
local version="$1"
echo "== ELF program headers: hello-${version} =="
readelf -lW "./hello-${version}" | awk '/Program Headers:/,/Section to Segment mapping:/'
echo "== .bun section: hello-${version} =="
readelf -SW "./hello-${version}" | grep -E '(^ \[Nr\]|\.bun)' || true
}
trace_exec() {
local version="$1"
echo "== strace execve: hello-${version} =="
set +e
strace -f -o "strace-${version}.log" "./hello-${version}" >/dev/null 2>&1
local status=$?
set -e
echo "exit=${status}"
sed -n '1,12p' "strace-${version}.log"
}
loader_bypass() {
local version="$1"
echo "== dynamic loader invocation: hello-${version} =="
set +e
/lib64/ld-linux-x86-64.so.2 "./hello-${version}" >"loader-${version}.stdout" 2>"loader-${version}.stderr"
local status=$?
set -e
echo "exit=${status}"
sed -n '1,20p' "loader-${version}.stdout"
sed -n '1,20p' "loader-${version}.stderr"
}
patch_extra_load_to_gnu_stack() {
# Bun 1.3.13 emits 9 program headers. Header #7 (zero-based) is the appended
# .bun PT_LOAD. Change only p_type from PT_LOAD (1) to PT_GNU_STACK
# (0x6474e551). This should make WSL1 accept execve, proving the extra PT_LOAD
# is the loader rejection trigger. The program then crashes because .bun is no
# longer mapped; that crash is expected.
echo "== control patch: PT_LOAD -> PT_GNU_STACK for appended .bun segment =="
cp ./hello-1.3.13 ./hello-1.3.13-no-extra-load
printf '\x51\xe5\x74\x64' | dd of=./hello-1.3.13-no-extra-load bs=1 seek=456 conv=notrunc status=none
readelf -lW ./hello-1.3.13-no-extra-load | awk '/Program Headers:/,/Section to Segment mapping:/'
set +e
./hello-1.3.13-no-extra-load >patched.stdout 2>patched.stderr
local status=$?
set -e
echo "exit=${status}"
sed -n '1,20p' patched.stdout
sed -n '1,30p' patched.stderr
}
download_bun 1.3.11
download_bun 1.3.13
compile_with 1.3.11
compile_with 1.3.13
run_direct 1.3.11
run_direct 1.3.13
show_elf 1.3.11
show_elf 1.3.13
trace_exec 1.3.13
loader_bypass 1.3.13
patch_extra_load_to_gnu_stack
echo "== output directory =="
pwd
#!/usr/bin/env bash
set -euo pipefail
# Real-world confirmation using public opencode release binaries.
# This is not the primary minimal repro; use repro-bun-compile-wsl1.sh first.
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
need() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "missing dependency: $1" >&2
echo "install on Debian/Ubuntu with: sudo apt-get update && sudo apt-get install -y curl file binutils strace" >&2
exit 1
fi
}
need curl
need tar
need file
need readelf
need strace
workdir="${1:-/tmp/opencode-wsl1-elf-repro}"
rm -rf "$workdir"
mkdir -p "$workdir"
cd "$workdir"
echo "== environment =="
uname -a
download_opencode() {
local version="$1"
local url="https://github.com/anomalyco/opencode/releases/download/v${version}/opencode-linux-x64.tar.gz"
echo "== downloading opencode ${version} =="
mkdir -p "$version"
curl -fsSL "$url" | tar -xz -C "$version"
file "$version/opencode"
}
run_direct() {
local version="$1"
echo "== direct exec: opencode ${version} =="
set +e
"$version/opencode" --version >"run-${version}.stdout" 2>"run-${version}.stderr"
local status=$?
set -e
echo "exit=${status}"
sed -n '1,20p' "run-${version}.stdout"
sed -n '1,20p' "run-${version}.stderr"
}
show_elf() {
local version="$1"
echo "== ELF program headers: opencode ${version} =="
readelf -lW "$version/opencode" | awk '/Program Headers:/,/Section to Segment mapping:/'
echo "== .bun section: opencode ${version} =="
readelf -SW "$version/opencode" | grep -E '(^ \[Nr\]|\.bun)' || true
}
trace_exec() {
local version="$1"
echo "== strace execve: opencode ${version} =="
set +e
strace -f -o "strace-${version}.log" "$version/opencode" --version >/dev/null 2>&1
local status=$?
set -e
echo "exit=${status}"
sed -n '1,12p' "strace-${version}.log"
}
loader_bypass() {
local version="$1"
echo "== dynamic loader invocation: opencode ${version} =="
set +e
/lib64/ld-linux-x86-64.so.2 "$version/opencode" --version >"loader-${version}.stdout" 2>"loader-${version}.stderr"
local status=$?
set -e
echo "exit=${status}"
sed -n '1,20p' "loader-${version}.stdout"
sed -n '1,20p' "loader-${version}.stderr"
}
download_opencode 1.14.20
download_opencode 1.14.21
download_opencode 1.14.30
run_direct 1.14.20
run_direct 1.14.21
run_direct 1.14.30
show_elf 1.14.20
show_elf 1.14.21
show_elf 1.14.30
trace_exec 1.14.30
loader_bypass 1.14.30
echo "== output directory =="
pwd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment