Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/probe/o10_mingw.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# O10: the island conditions on PE. A DLL built by MinGW g++ (libstdc++) is
# used by programs built by llvm-mingw clang++ (libc++). Prints; never asserts.
set -u
W=$(mktemp -d); cd "$W"
GCCBIN=/c/msys64/ucrt64/bin
/c/msys64/usr/bin/bash -lc "pacman -S --noconfirm --needed mingw-w64-ucrt-x86_64-gcc" >/dev/null 2>&1
"$GCCBIN/g++" --version | head -1
url=$(curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN:-}" https://api.github.com/repos/mstorsjo/llvm-mingw/releases/latest \
| grep -o '"browser_download_url": *"[^"]*ucrt-x86_64\.zip"' | head -1 | sed 's/.*"\(http[^"]*\)"/\1/')
echo "llvm-mingw: $url"
curl -fsSL -o lm.zip "$url" && unzip -q lm.zip && LM=$(ls -d "$W"/llvm-mingw-*/ | head -1)
CLANG="$LM/bin/x86_64-w64-mingw32-clang++"
"$CLANG" --version | head -1
OBJDUMP="$LM/bin/llvm-objdump"

cat > b.cpp <<'EOF'
#include <stdexcept>
#include <string>
extern "C" __declspec(dllexport) int b_checked(int v) {
try { if (v < 0) throw std::runtime_error("negative"); return v * 2; }
catch (const std::exception&) { return -1; }
}
__declspec(dllexport) int b_throws(int v) {
if (v < 0) throw std::runtime_error("negative from B (libstdc++)");
return v;
}
EOF
cat > a.cpp <<'EOF'
#include <stdexcept>
__declspec(dllexport) int a_throws(int v) {
if (v < 0) throw std::runtime_error("negative from A (libc++)");
return v;
}
EOF
cat > use_c.cpp <<'EOF'
#include <cstdio>
#include <stdexcept>
extern "C" int b_checked(int);
int main() {
int r = b_checked(-1);
try { throw std::logic_error("local"); } catch (const std::exception& e) { std::printf("local typed catch ok: %s\n", e.what()); }
std::printf("b_checked(-1) = %d (expect -1)\n", r);
return r == -1 ? 0 : 1;
}
EOF
cat > use_cxx.cpp <<'EOF'
#include <cstdio>
#include <stdexcept>
int b_throws(int);
int main() {
try { b_throws(-1); }
catch (const std::exception& e) { std::printf("typed catch across runtimes: %s\n", e.what()); return 0; }
catch (...) { std::printf("only catch(...) caught it\n"); return 2; }
return 1;
}
EOF
cat > use_a.cpp <<'EOF'
#include <cstdio>
#include <stdexcept>
extern "C" int b_checked(int);
int a_throws(int);
int main() {
int r = b_checked(-1);
try { a_throws(-1); }
catch (const std::exception& e) { std::printf("typed catch across OUR two images: %s; b_checked=%d\n", e.what(), r); return 0; }
catch (...) { std::printf("only catch(...)\n"); return 2; }
return 1;
}
EOF
export PATH="$LM/bin:$GCCBIN:$PATH"
"$GCCBIN/g++" -O1 -shared b.cpp -o B.dll -Wl,--out-implib,libB.dll.a; echo "O10 B.dll (g++/libstdc++) rc=$?"
"$OBJDUMP" -p B.dll | grep "DLL Name" | sed 's/^/ B imports: /'

run() { local label=$1; shift; "$@"; echo "O10 $label rc=$?"; }
imports() { "$OBJDUMP" -p "$1" | grep "DLL Name" | tr -d '\t' | sed "s/^/ $1 imports: /"; }

run "V1 build (C interface, shared libc++)" "$CLANG" use_c.cpp -L. -lB -o v1.exe
imports v1.exe; run "V1 run" ./v1.exe
run "V2 build (C++ interface, typed catch across runtimes)" "$CLANG" use_cxx.cpp -L. -lB -o v2.exe
imports v2.exe; run "V2 run" ./v2.exe
run "V3 A.dll (libc++)" "$CLANG" -shared a.cpp -o A.dll -Wl,--out-implib,libA.dll.a
run "V3 build (two of our images + B)" "$CLANG" use_a.cpp -L. -lA -lB -o v3.exe
imports v3.exe; run "V3 run" ./v3.exe
run "V4 build (static libc++ + B)" "$CLANG" -static-libstdc++ -static use_c.cpp -L. -lB -o v4.exe
imports v4.exe; run "V4 run" ./v4.exe
# The #838 shape on PE: libstdc++'s import library named on the link line
# BEFORE the driver appends libc++. Which runtime do __cxa_throw and the
# personality bind to, and does the local typed catch survive?
GL=/c/msys64/ucrt64/lib
run "V5 build (-l:libstdc++.dll.a before libc++)" "$CLANG" use_c.cpp -L. -lB -L$GL -l:libstdc++.dll.a -o v5.exe
imports v5.exe
"$OBJDUMP" -p v5.exe | awk '/DLL Name: libstdc\+\+-6.dll/{f=1;next} /DLL Name/{f=0} f' | head -20 | sed 's/^/ v5 from libstdc++-6.dll: /'
run "V5 run" ./v5.exe
run "V6 build (-l:libstdc++.dll.a AFTER libc++)" "$CLANG" use_c.cpp -L. -lB -lc++ -L$GL -l:libstdc++.dll.a -o v6.exe
imports v6.exe; run "V6 run" ./v6.exe
83 changes: 83 additions & 0 deletions .github/probe/o5_link_race.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# D22 / O5: what a process that starts a program sees while a linker rewrites
# it, per linker and per publication strategy. Prints counts; never asserts.
#
# in-place the linker writes the output name directly (today)
# tmp+rename the linker writes <out>.tmp, then rename over <out> (POSIX D22)
# aside rename <out> to <out>.old-N, then link to <out> (PE D22)
import os, shutil, subprocess, sys, tempfile, threading, time

win = sys.platform == "win32"
d = tempfile.mkdtemp()
os.chdir(d)
n = 6000
with open("big.cpp", "w") as f:
for i in range(n):
f.write(f"int f{i}(int x) {{ return x * {i} + {i % 7}; }}\n")
f.write("int main(int argc, char**) { int s = 0;\n")
for i in range(0, n, 3):
f.write(f" s += f{i}(argc);\n")
f.write(" return s == 12345 ? 1 : 0; }\n")

linkers = []
if win:
subprocess.run(["cl", "/nologo", "/c", "/Od", "big.cpp"], check=True, stdout=subprocess.DEVNULL)
linkers.append(("link.exe", lambda out: ["link", "/nologo", "/DEBUG", f"/OUT:{out}", "big.obj"]))
if shutil.which("lld-link"):
linkers.append(("lld-link", lambda out: ["lld-link", "/nologo", "/DEBUG", f"/OUT:{out}", "big.obj"]))
else:
cxx = os.environ.get("CXX", "c++")
subprocess.run([cxx, "-c", "-O0", "-g", "big.cpp", "-o", "big.o"], check=True)
if sys.platform == "darwin":
linkers.append(("ld64", lambda out: [cxx, "big.o", "-o", out]))
else:
linkers.append(("bfd", lambda out: [cxx, "-fuse-ld=bfd", "big.o", "-o", out]))
for ld in ("lld", "gold", "mold"):
if subprocess.run([cxx, f"-fuse-ld={ld}", "big.o", "-o", "probe-ld"], capture_output=True).returncode == 0:
linkers.append((ld, lambda out, ld=ld: [cxx, f"-fuse-ld={ld}", "big.o", "-o", out]))

exe = "app.exe" if win else "./app"

def publish(strategy, cmd, k):
if strategy == "in-place":
subprocess.run(cmd(exe), capture_output=True)
elif strategy == "tmp+rename":
tmp = exe + ".tmp" + (".exe" if win else "")
subprocess.run(cmd(tmp), capture_output=True)
try:
os.replace(tmp, exe)
except OSError as e:
errors["publish:" + type(e).__name__ + str(getattr(e, "winerror", ""))] = errors.get("publish:" + type(e).__name__ + str(getattr(e, "winerror", "")), 0) + 1
else: # aside
try:
if os.path.exists(exe):
os.replace(exe, f"{exe}.old-{k}")
except OSError as e:
errors["aside:" + type(e).__name__] = errors.get("aside:" + type(e).__name__, 0) + 1
subprocess.run(cmd(exe), capture_output=True)

for name, cmd in linkers:
for strategy in ("in-place", "tmp+rename", "aside"):
for f in os.listdir("."):
if f.startswith("app"):
try: os.remove(f)
except OSError: pass
subprocess.run(cmd(exe), capture_output=True)
errors, runs, stop = {}, [0], [False]
def spawner():
while not stop[0]:
try:
subprocess.run([exe], capture_output=True, timeout=10)
runs[0] += 1
except OSError as e:
key = f"{type(e).__name__}:{getattr(e, 'winerror', None) or e.errno}"
errors[key] = errors.get(key, 0) + 1
ths = [threading.Thread(target=spawner) for _ in range(4)]
for t in ths: t.start()
t0 = time.time()
for k in range(25):
publish(strategy, cmd, k)
stop[0] = True
for t in ths: t.join()
print(f"D22 {sys.platform} linker={name:8} strategy={strategy:10} links=25 ({time.time()-t0:.1f}s) "
f"runs ok={runs[0]} spawn errors={errors or 'none'}")
69 changes: 69 additions & 0 deletions .github/probe/o5_posix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# O5 (POSIX half): flock semantics the build-directory lock relies on, and
# replacing an executable that is running. Prints facts; never asserts.
import fcntl, os, shutil, signal, subprocess, sys, tempfile, time

d = tempfile.mkdtemp()
lock = os.path.join(d, "build.lock")
open(lock, "w").close()

def probe_lock():
code = ("import fcntl,os,sys\n"
f"fd=os.open({lock!r},os.O_RDWR)\n"
"try:\n fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB); print('free')\n"
"except BlockingIOError: print('held')\n")
return subprocess.run([sys.executable, "-c", code], capture_output=True, text=True).stdout.strip()

def holder(cloexec, then):
# A process takes the lock, then either execs (the `mcpp run` shape) or
# spawns a long-lived child and exits.
flags = os.O_RDWR | (os.O_CLOEXEC if cloexec else 0)
code = ("import fcntl,os,subprocess,time\n"
f"fd=os.open({lock!r},{flags})\n"
"fcntl.flock(fd,fcntl.LOCK_EX)\n"
+ ("" if cloexec else "os.set_inheritable(fd,True)\n")
+ ("os.execv('/bin/sleep',['sleep','3'])\n" if then == "exec" else
"subprocess.Popen(['sleep','3'],close_fds=False)\n"))
return subprocess.Popen([sys.executable, "-c", code])

for cloexec in (True, False):
for then in ("exec", "spawn-and-exit"):
p = holder(cloexec, then)
time.sleep(0.8)
print(f"O5.flock cloexec={cloexec} holder={then}: lock is {probe_lock()} while the program runs")
p.wait(); time.sleep(3)

p = subprocess.Popen([sys.executable, "-c",
f"import fcntl,os,time\nfd=os.open({lock!r},os.O_RDWR)\nfcntl.flock(fd,fcntl.LOCK_EX)\ntime.sleep(30)\n"])
time.sleep(0.5)
before = probe_lock()
os.kill(p.pid, signal.SIGKILL); p.wait()
print(f"O5.flock SIGKILL holder: before={before} after={probe_lock()}")

# Replacing a running executable by rename, and exec of the new one.
prog = os.path.join(d, "prog")
shutil.copy("/bin/sleep", prog)
run = subprocess.Popen([prog, "5"])
time.sleep(0.3)
shutil.copy("/bin/echo", prog + ".tmp")
try:
os.replace(prog + ".tmp", prog)
out = subprocess.run([prog, "replaced"], capture_output=True, text=True).stdout.strip()
print(f"O5.replace running exe by rename: ok; new program says {out!r}; old still running={run.poll() is None}")
except OSError as e:
print(f"O5.replace running exe by rename: {e!r}")
run.kill()

# Writing in place over a running executable (what GNU ld's unlink-then-write
# avoids and a plain open(O_TRUNC) does not).
shutil.copy("/bin/sleep", prog)
run = subprocess.Popen([prog, "5"])
time.sleep(0.3)
try:
with open(prog, "r+b") as f:
f.write(b"\0")
print("O5.write in place over running exe: allowed")
except OSError as e:
print(f"O5.write in place over running exe: {e!r}")
run.kill()
print("platform:", sys.platform, os.uname().release)
72 changes: 72 additions & 0 deletions .github/probe/o5_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# O5 (Windows half): LockFileEx waiting semantics, replacing a running
# executable, and what a concurrent CreateProcess sees while link.exe writes.
# Prints facts; never asserts.
import ctypes, ctypes.wintypes as wt, msvcrt, os, shutil, subprocess, sys, tempfile, threading, time

k32 = ctypes.WinDLL("kernel32", use_last_error=True)
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY = 2, 1

class OVERLAPPED(ctypes.Structure):
_fields_ = [("Internal", ctypes.c_void_p), ("InternalHigh", ctypes.c_void_p),
("Offset", wt.DWORD), ("OffsetHigh", wt.DWORD), ("hEvent", wt.HANDLE)]

def lock(fh, flags):
ov = OVERLAPPED()
ok = k32.LockFileEx(wt.HANDLE(msvcrt.get_osfhandle(fh.fileno())), flags, 0, 1, 0, ctypes.byref(ov))
return bool(ok), ctypes.get_last_error()

d = tempfile.mkdtemp()
lk = os.path.join(d, "build.lock")
open(lk, "w").close()
holder = subprocess.Popen([sys.executable, "-c",
"import ctypes,ctypes.wintypes as wt,msvcrt,time\n"
"k=ctypes.WinDLL('kernel32')\n"
"class O(ctypes.Structure): _fields_=[('a',ctypes.c_void_p),('b',ctypes.c_void_p),('c',wt.DWORD),('d',wt.DWORD),('e',wt.HANDLE)]\n"
f"f=open({lk!r},'r+'); o=O(); print(k.LockFileEx(wt.HANDLE(msvcrt.get_osfhandle(f.fileno())),2,0,1,0,ctypes.byref(o)),flush=True); time.sleep(60)\n"],
stdout=subprocess.PIPE, text=True)
print("O5.win holder locked:", holder.stdout.readline().strip())
f = open(lk, "r+")
print("O5.win LockFileEx FAIL_IMMEDIATELY while held -> (ok, err) =", lock(f, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY))
res = {}
def blocking():
t = time.time(); res["r"] = lock(f, LOCKFILE_EXCLUSIVE_LOCK); res["t"] = time.time() - t
th = threading.Thread(target=blocking); th.start()
time.sleep(1.0)
holder.kill(); holder.wait()
th.join(10)
print(f"O5.win blocking LockFileEx after TerminateProcess of holder: {res.get('r')} after {res.get('t', -1):.2f}s (1.0s of it was the hold)")
f.close()

# Handle inheritance: Python's handles are non-inheritable; mcpp's CreateFileW
# passes no SECURITY_ATTRIBUTES, so the same holds there.
fh = open(lk)
print("O5.win os.get_handle_inheritable(lock) =", os.get_handle_inheritable(msvcrt.get_osfhandle(fh.fileno())))
fh.close()

# A running executable: overwrite, rename aside, rename into place, delete.
src = shutil.which("ping") or r"C:\Windows\System32\PING.EXE"
prog = os.path.join(d, "prog.exe")
shutil.copy(src, prog)
run = subprocess.Popen([prog, "-n", "30", "127.0.0.1"], stdout=subprocess.DEVNULL)
time.sleep(0.5)
shutil.copy(src, prog + ".tmp")
def attempt(label, fn):
try:
fn(); print(f"O5.win {label}: ok")
except OSError as e:
print(f"O5.win {label}: {type(e).__name__} winerror={getattr(e, 'winerror', None)}")
attempt("os.replace(tmp -> running exe)", lambda: os.replace(prog + ".tmp", prog))
attempt("open running exe for write", lambda: open(prog, "r+b").close())
attempt("rename running exe aside (.old-1)", lambda: os.rename(prog, prog + ".old-1"))
attempt("rename tmp into the freed name", lambda: os.replace(prog + ".tmp", prog))
attempt("run the new exe", lambda: subprocess.run([prog, "-n", "1", "127.0.0.1"], stdout=subprocess.DEVNULL, check=True))
attempt("delete .old-1 while it runs", lambda: os.remove(prog + ".old-1"))
run.kill(); run.wait(); time.sleep(0.5)
attempt("delete .old-1 after exit", lambda: os.remove(prog + ".old-1"))

# A loaded DLL: rename aside.
dll = os.path.join(d, "probe.dll")
shutil.copy(r"C:\Windows\System32\version.dll", dll)
h = ctypes.WinDLL(dll)
attempt("rename loaded DLL aside", lambda: os.rename(dll, dll + ".old-1"))
attempt("copy a new DLL into the freed name", lambda: shutil.copy(r"C:\Windows\System32\version.dll", dll))
43 changes: 43 additions & 0 deletions .github/probe/o8_libstdcxx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# O8: clang (llvm payload) + libstdc++ (gcc payload) on this host's
# architecture: `import std`, `import std.compat`, a cross-.so typed catch, and
# sanitizers. Prints; never asserts.
set -u
R=${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs
ARCH=$(uname -m)
CXX=$R/xim-x-llvm/22.1.8/bin/clang++
V=16.1.0; G=$R/xim-x-gcc/$V
GID=$(ls -d "$G"/lib/gcc/*-linux-gnu/* | head -1)
TRIPLE=$(basename "$(dirname "$GID")")
GLIBC=$(ls -d "$R"/xim-x-glibc/* | tail -1)
UAPI=$(ls -d "$R"/xim-x-linux-headers/* | tail -1)
LOADER=$(ls "$GLIBC"/lib64/ld-linux-*.so.* "$GLIBC"/lib/ld-linux-*.so.* 2>/dev/null | head -1)
LIBDIR=$(dirname "$LOADER")
echo "arch=$ARCH triple=$TRIPLE gid=$GID glibc=$GLIBC loader=$LOADER"
ls "$G/include/c++/$V/bits/" | grep -E '^std(\.compat)?\.cc$'
W=$(mktemp -d); cd "$W"
INC="-nostdinc++ -isystem $G/include/c++/$V -isystem $G/include/c++/$V/$TRIPLE -isystem $G/include/c++/$V/backward -nostdlibinc -isystem $GLIBC/include -isystem $UAPI/include"
C="--no-default-config -std=c++23 --gcc-install-dir=$GID $INC"
L="--no-default-config -stdlib=libstdc++ --gcc-install-dir=$GID -fuse-ld=lld --rtlib=libgcc --unwindlib=libgcc -L$G/lib64 -L$G/lib -Wl,-rpath,$G/lib64:$G/lib -B$LIBDIR -L$LIBDIR -Wl,-rpath,$LIBDIR -Wl,--dynamic-linker=$LOADER"
step() { local label=$1; shift; "$@" >"$W/log" 2>&1; local rc=$?; echo "O8.$ARCH $label rc=$rc"; [ $rc -ne 0 ] && tail -5 "$W/log"; return $rc; }
step "std.pcm" "$CXX" $C -Wno-reserved-module-identifier --precompile -x c++-module "$G/include/c++/$V/bits/std.cc" -o std.pcm
step "std.compat.pcm" "$CXX" $C -Wno-reserved-module-identifier -fmodule-file=std=std.pcm --precompile -x c++-module "$G/include/c++/$V/bits/std.compat.cc" -o std.compat.pcm
"$CXX" $C -c std.pcm -o std.o; "$CXX" $C -fmodule-file=std=std.pcm -c std.compat.pcm -o std.compat.o
printf 'import std;\nimport std.compat;\nint main(){ std::vector<int> v{3,1,2}; std::ranges::sort(v); std::println("{} {}", v, ::abs(-4));\n try { throw std::runtime_error("boom"); } catch (const std::exception& e) { std::println("caught {}", e.what()); }\n std::jthread t([]{ std::println("thread ok"); }); }\n' > main.cpp
step "import std + std.compat build" "$CXX" $C -fmodule-file=std=std.pcm -fmodule-file=std.compat=std.compat.pcm -c main.cpp -o main.o
step "link" "$CXX" $L main.o std.o std.compat.o -o app && ./app; echo "O8.$ARCH run rc=$?"
printf '#include <stdexcept>\nint checked(int v){ if (v<0) throw std::runtime_error("negative (from .so)"); return v*2; }\n' > lib.cpp
printf 'import std;\nint checked(int);\nint main(){ try { checked(-1); } catch (const std::exception& e) { std::println("caught across .so: {}", e.what()); return 0; } catch (...) { return 1; } }\n' > use.cpp
"$CXX" $C -fPIC -c lib.cpp -o lib.o && "$CXX" $L -shared lib.o -o libthrow.so \
&& "$CXX" $C -fmodule-file=std=std.pcm -c use.cpp -o use.o && "$CXX" $L use.o std.o -L. -lthrow -Wl,-rpath,"$PWD" -o use && ./use
echo "O8.$ARCH cross-.so typed catch rc=$?"
for S in address undefined thread; do
"$CXX" $C -fsanitize=$S -c std.pcm -o std-$S.o 2>/dev/null
printf 'import std;\nint main(){ std::vector<int> v{1,2,3}; try { throw std::runtime_error("x"); } catch (const std::exception& e) { std::println("{} {}", e.what(), v.size()); } }\n' > s.cpp
"$CXX" $C -fsanitize=$S -fmodule-file=std=std.pcm -c s.cpp -o s.o && "$CXX" $L -fsanitize=$S s.o std-$S.o -o s-$S >"$W/log" 2>&1 && ./s-$S
rc=$?; echo "O8.$ARCH sanitizer=$S rc=$rc"; [ $rc -ne 0 ] && tail -3 "$W/log"
done
printf 'import std;\nint main(){ int* p = new int[2]; p[2]=1; std::println("{}", p[0]); delete[] p; }\n' > bad.cpp
"$CXX" $C -fsanitize=address -fmodule-file=std=std.pcm -c bad.cpp -o bad.o && "$CXX" $L -fsanitize=address bad.o std-address.o -o bad && ./bad 2>&1 | grep -m1 "ERROR: AddressSanitizer"
echo "O8.$ARCH asan detects overflow (grep rc=$?)"
readelf -d app | grep NEEDED
Loading
Loading