Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
55a8e06
fix(tls): preserve transport and completion ownership
vincentkoc Oct 26, 2026
61bfe79
merge: integrate current Bun TLS ownership boundaries
vincentkoc Oct 26, 2026
1dff3f9
style(socket): format native TLS ownership paths
vincentkoc Oct 26, 2026
eb84ae2
test(ci): qualify TLS with native release artifacts
vincentkoc Oct 26, 2026
715325a
Merge CI qualification safeguards into TLS transport repair
vincentkoc Oct 26, 2026
9f54e79
style: organize TLS qualification imports
vincentkoc Oct 26, 2026
c55262d
fix(tls): preserve allocation roots in native dispatch
vincentkoc Oct 26, 2026
8f17469
fix(tls): narrow wrapped socket types
vincentkoc Oct 26, 2026
20b21a6
test(tls): deliver peer FIN and expose failed close traces
vincentkoc Oct 27, 2026
62a0f88
chore(tls): document unsafe lifetimes and format peer FIN test
vincentkoc Oct 27, 2026
86589d6
fix(tls): wait for generic ciphertext before transport shutdown
vincentkoc Oct 27, 2026
4116c9f
Merge frozen Bun main 191755ec into TLS transport repair
vincentkoc Oct 27, 2026
c03aa0c
fix(tls): preserve verifier callbacks when checking resumed sessions
vincentkoc Oct 27, 2026
6206814
test(tls): qualify destruction scenarios independently
vincentkoc Oct 27, 2026
454a78a
test(tls): collect completion owners before module re-entry
vincentkoc Oct 28, 2026
12e500e
ci(tls): inspect symbols from failing qualification artifacts
vincentkoc Oct 31, 2026
9d7208d
fix(ci): complete Darwin inspection when symbol lookups are empty
vincentkoc Oct 31, 2026
a2e411a
test(tls): compare dropped cycles against published baseline
vincentkoc Oct 31, 2026
dc683a4
ci(tls): reproduce ARM64 completion retention with full symbols
vincentkoc Oct 31, 2026
f1040ae
ci(tls): reuse retained ARM64 symbols for native diagnosis
vincentkoc Oct 31, 2026
2e13ab5
ci(tls): qualify original shipping symbols with LLVM 23
vincentkoc Oct 31, 2026
13430a5
test(tls): retain shipping optimizations in native debug builds
vincentkoc Oct 31, 2026
1791c83
fix(ci): capture shipping header drift before core compilation
vincentkoc Oct 31, 2026
0a1544a
fix(ci): compare shipping headers without known provenance stamps
vincentkoc Oct 31, 2026
bd7da8a
test(tls): capture the destroyed completion owner after marking
vincentkoc Oct 31, 2026
c2f61af
test(tls): record native capture breakpoint observations
vincentkoc Oct 31, 2026
378e5ee
test(tls): bind WeakRef observation to its physical constructor
vincentkoc Oct 31, 2026
1f159fd
test(tls): read verifier marker stacks without inferior calls
vincentkoc Oct 31, 2026
a4e984c
test(tls): record marker layout before stack admission
vincentkoc Oct 31, 2026
d694f60
test(tls): capture the first precise mark through frame pointers
vincentkoc Oct 31, 2026
2021c19
fix(ci): observe the TLS owner real-mark writer
vincentkoc Oct 31, 2026
5ffae65
test(tls): trace accepted conservative root spans
vincentkoc Oct 31, 2026
0acdc3e
test(tls): observe the surviving machine-stack root boundary
vincentkoc Oct 31, 2026
1240e1a
test(tls): inventory inline conservative-root scanner sites
vincentkoc Oct 31, 2026
a814c05
test(tls): observe physical conservative-root span scanners
vincentkoc Oct 31, 2026
07538ea
test(tls): trace the exact accepted conservative scanner word
vincentkoc Oct 31, 2026
8664f99
test(tls): bind the scanner word to conducting stack provenance
vincentkoc Oct 31, 2026
8167dca
test(tls): inventory the conducting interpreter call frame
vincentkoc Oct 31, 2026
555dd50
test(tls): watch the accepted stack word before frame initialization
vincentkoc Oct 31, 2026
c375b7d
test(tls): observe retained stack storage from the first timer
vincentkoc Oct 31, 2026
96aa4e7
test(tls): admit the profiled timer tail call
vincentkoc Oct 31, 2026
aa9d955
test(tls): admit observed byte stores into retained stack storage
vincentkoc Oct 31, 2026
3d7b13f
test(tls): identify the absent config stack copy
vincentkoc Oct 31, 2026
8b02717
test(tls): qualify the empty config cleanup repair
vincentkoc Oct 31, 2026
e0972ab
test(tls): inspect the retained shipping deinit instructions
vincentkoc Oct 31, 2026
ddac89a
ci(tls): inspect retained Darwin debugger capabilities
vincentkoc Oct 31, 2026
313fefe
ci(tls): reconstruct Darwin type layouts with frozen compiler
vincentkoc Oct 31, 2026
38478f2
fix(ci): match Darwin type probe to shipping build host
vincentkoc Oct 31, 2026
c7afff8
fix(ci): retain native macro arguments in Darwin type probe
vincentkoc Oct 31, 2026
ccfd29c
ci(tls): inspect physical Darwin guards and retain verifier layout
vincentkoc Oct 31, 2026
b13a1fd
fix(ci): admit Mach-O x64 library capability bit
vincentkoc Oct 31, 2026
e6afcbd
ci(tls): compare collection checkpoints on shipping Darwin
vincentkoc Oct 31, 2026
ef6edc1
fix(ci): match TLS replay to dry-run signing contract
vincentkoc Oct 31, 2026
a9c7eec
ci(tls): preserve active frozen layout reconstruction
vincentkoc Oct 31, 2026
84ebe5c
ci(tls): qualify fixture correction on retained native artifacts
vincentkoc Oct 31, 2026
10f0792
ci(tls): capture verifier layout from compiler AST
vincentkoc Oct 31, 2026
922d5a8
ci(tls): capture production cycle heap roots
vincentkoc Oct 31, 2026
7b50b0a
ci(tls): inventory async frame root boundaries
vincentkoc Oct 31, 2026
4cbd185
ci(tls): trace cycle frame roots at the original checkpoint
vincentkoc Oct 31, 2026
e4a502d
ci(tls): preserve debugger stop and exception details
vincentkoc Oct 31, 2026
c370071
ci(tls): synchronize debugger stops before counting GC
vincentkoc Oct 31, 2026
d8abd7a
ci(tls): handle empty conservative root spans
vincentkoc Oct 31, 2026
9feaffc
test(tls): map module-entry stack instructions
vincentkoc Oct 31, 2026
5d7e4ec
test(tls): trace writes to the retained stack word
vincentkoc Oct 31, 2026
0bffad6
test(tls): bound stack-word capture overhead
vincentkoc Oct 31, 2026
7e718d3
ci(tls): capture all watched writes and diagnose unexpected stops
vincentkoc Oct 31, 2026
18f68a3
ci(tls): reject unexpected stops on every debugger thread
vincentkoc Oct 31, 2026
9883745
ci(tls): retain bounded debugger step and remote-stop diagnostics
vincentkoc Oct 31, 2026
1c8fc4b
ci(tls): bound physical breakpoint sites to the captured collection
vincentkoc Nov 1, 2026
36c0f83
ci(tls): bound root history to value-changing stores
vincentkoc Nov 1, 2026
3e0bc52
ci(tls): separate raw root history from writer symbol inspection
vincentkoc Nov 1, 2026
4a864cc
test(tls): bound asynchronous physical root capture
vincentkoc Nov 1, 2026
056f35a
test(tls): serialize bounded physical root capture
vincentkoc Nov 1, 2026
16faa52
test(tls): join late word writes to accepted scan order
vincentkoc Nov 1, 2026
97b715d
test(tls): capture bounded context for the first word change
vincentkoc Nov 1, 2026
8ebb643
test(tls): collect all suites before reporting failure
vincentkoc Nov 1, 2026
d8cb272
fix(ci): refresh checkout metadata before TLS admission
vincentkoc Nov 1, 2026
75c46a1
chore: reconcile TLS diagnostics after transport repair lands
vincentkoc Nov 1, 2026
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
131 changes: 131 additions & 0 deletions .github/openclaw/tls-config-disassembly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Inspect the exact retained shipping profile without executing it."""
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import struct
import subprocess
import zipfile

RUN = 41238111110
COMMIT = '510fef558b464c58de1209298f638fa476d03eef'
ARTIFACT = 13056617461
SIZE = 222623871
DIGEST = 'c54dbe7ea29a336c76db1dd5ba491bbb117a6c211d938892c62f2f5e74251fcd'
INPUT = Path(os.environ['RUNNER_TEMP']) / 'tls-disassembly-input'
OUT = Path('tls-disassembly-proof')
OUT.mkdir(exist_ok=True)


def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()


def selected(archive, wanted, maximum):
entries = archive.infolist()
assert len(entries) <= 32
names = set()
for entry in entries:
path = PurePosixPath(entry.filename)
assert entry.filename and not path.is_absolute() and '..' not in path.parts
assert '\\' not in entry.filename and '\x00' not in entry.filename
assert str(path) == entry.filename.rstrip('/')
assert str(path) not in names
names.add(str(path))
mode = entry.external_attr >> 16
assert stat.S_IFMT(mode) in (0, stat.S_IFREG, stat.S_IFDIR)
assert not entry.flag_bits & 1
matches = [entry for entry in entries if entry.filename == wanted]
assert len(matches) == 1
entry = matches[0]
assert not entry.is_dir() and 0 < entry.file_size <= maximum
return entry


def extract(archive_path, member, destination, maximum):
with zipfile.ZipFile(archive_path) as archive:
entry = selected(archive, member, maximum)
with archive.open(entry) as source, destination.open('xb') as target:
count = 0
while chunk := source.read(1024**2):
count += len(chunk)
assert count <= maximum
target.write(chunk)
assert count == entry.file_size
destination.chmod(0o600)


def run(name, arguments):
result = subprocess.run(arguments, stdin=subprocess.DEVNULL,
capture_output=True, timeout=120, check=False)
assert len(result.stdout) <= 32 * 1024**2 and len(result.stderr) <= 1024**2
(OUT / (name + '.stderr')).write_bytes(result.stderr)
assert result.returncode == 0 and not result.stderr, name
return result.stdout.decode('utf-8')


archive = INPUT / 'artifact.zip'
assert archive.stat().st_size == SIZE and sha(archive) == DIGEST
metadata = json.loads((INPUT / 'metadata.json').read_text())
assert metadata['id'] == ARTIFACT and metadata['name'] == 'build-linux-arm64'
assert metadata['size_in_bytes'] == SIZE and metadata['digest'] == 'sha256:' + DIGEST
assert not metadata['expired']
assert metadata['workflow_run']['id'] == RUN and metadata['workflow_run']['head_sha'] == COMMIT
profile_zip = INPUT / 'profile.zip'
profile = INPUT / 'bun-profile.elf'
extract(archive, 'bun-linux-aarch64-profile.zip', profile_zip, 1024**3)
extract(profile_zip, 'bun-linux-aarch64-profile/bun-profile', profile, 2 * 1024**3)
with profile.open('rb') as stream:
header = stream.read(64)
assert header[:7] == b'\x7fELF\x02\x01\x01'
assert struct.unpack_from('<H', header, 18)[0] == 183
version = run('llvm-version', ['llvm-objdump-23', '--version'])
assert re.search(r'LLVM version 23\.', version)
symbols = run('nm', ['llvm-nm-23', '--defined-only', '--format=posix',
'--print-size', '--radix=x', '--no-demangle', str(profile)])
rows = []
for line in symbols.splitlines():
fields = line.split()
if fields and re.fullmatch(
r'_RNv\w*11bun_runtime6socket11socket_body\w*20DuplexUpgradeContext6deinit', fields[0]):
assert len(fields) == 4 and fields[1] in ('T', 't')
rows.append(fields)
assert len(rows) == 1, 'deinit missing or ambiguous'
name, kind, address, size = rows[0]
start, length = int(address, 16), int(size, 16)
assert start > 0 and 0 < length <= 65536
assembly = run('objdump', ['llvm-objdump-23', '--disassemble', '--start-address=' + hex(start),
'--stop-address=' + hex(start + length), str(profile)])
assert re.search(r'^0*' + format(start, 'x') + r' <' + re.escape(name) + r'>:$', assembly, re.M)
instructions = []
for line in assembly.splitlines():
match = re.match(r'^\s*([0-9a-f]+):\s+([0-9a-f]{8})\s+(\S+)', line)
if match:
address = int(match[1], 16)
assert start <= address < start + length
assert match[3] != '<unknown>'
instructions.append(address)
assert instructions and instructions[0] == start
assert instructions == sorted(set(instructions))
notes = run('build-id', ['llvm-readelf-23', '--notes', str(profile)])
build_ids = re.findall(r'Build ID: ([0-9a-f]+)', notes)
assert len(build_ids) == 1
assert sha(archive) == DIGEST
receipt = dict(captured=True, control_flow_review_pending=True, producer_run=RUN,
commit=COMMIT, artifact_id=ARTIFACT, artifact_sha256=DIGEST,
profile_zip_sha256=sha(profile_zip), profile_sha256=sha(profile),
elf_machine='AArch64', build_id=build_ids[0], symbol=rows[0],
instruction_count=len(instructions), llvm_version=version)
(OUT / 'deinit.asm').write_text(assembly.replace(str(profile), 'bun-profile.elf'))
(OUT / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n')
(OUT / 'symbol.txt').write_text(' '.join(rows[0]) + '\n')
(OUT / 'build-id.txt').write_text(notes)
files = sorted(OUT.iterdir())
assert len(files) <= 12 and sum(path.stat().st_size for path in files) <= 2 * 1024**2
(OUT / 'manifest.json').write_text(json.dumps({
path.name: dict(bytes=path.stat().st_size, sha256=sha(path)) for path in files
}, indent=2) + '\n')
print(json.dumps(receipt))
27 changes: 27 additions & 0 deletions .github/openclaw/tls-config-repair.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs
index 2752cbc894159817f1557df3e7d0f518895a3a18..5fa5d59042a200a50106415325b8f6d1b4255f83 100644
--- a/src/runtime/socket/socket_body.rs
+++ b/src/runtime/socket/socket_body.rs
@@ -5118,7 +5118,9 @@ impl DuplexUpgradeContext {
Self::release_connection(this);
return;
}
- this.ssl_config.set(None); // Drop frees.
+ if this.ssl_config.get().is_some() {
+ this.ssl_config.set(None); // Drop frees.
+ }
// Bytes the peer sent while this task was still queued were
// staged by `UpgradedDuplex::on_internal_receive_data` (the
// engine did not exist yet to receive them). Feed them now
@@ -5189,7 +5191,10 @@ impl DuplexUpgradeContext {
debug_assert!(!ctx.native_shutdown_callback.get());
ctx.tls.set(None);
// Close raced ahead of StartTLS — drop the unconsumed config / ctx.
- ctx.ssl_config.set(None);
+ // An absent config has an inactive payload that must not spill onto the JS stack.
+ if ctx.ssl_config.get().is_some() {
+ ctx.ssl_config.set(None);
+ }
ctx.owned_ctx.set(None);
}
// `UpgradedDuplex` cleanup
136 changes: 136 additions & 0 deletions .github/openclaw/tls-config-repair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Qualify the reviewed empty-config repair without publishing the runtime branch."""
import hashlib
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import time
from xml.etree import ElementTree

SOURCE = Path('/work/source')
OUT = Path('/work/repair-proof')
INPUTS = Path(__file__).resolve().parent
BUN = '454a78a4d3b0202ecdbedf7dd051fc84ffc59e9e'
WEBKIT = 'b82bc917e99110b61c3a21202d6def7457aa194c'
OWNER = 'src/runtime/socket/socket_body.rs'
BEFORE = '66121e98f4e8537520f976ff572ef538cc7baedf3e59c8ab2fdfce5fe4bc2afc'
AFTER = '2a40a7df44cfbc9344eb92ced128da282ade137fe2a08f5319d85b23c297dfb0'
PATCH = 'a0fbcc59fae14006b39c60d5cd71d0b71f80f151c6b0eac7d689db5fcbcfc25b'
FIXTURE = 'test/js/bun/net/socket-retention.test.ts'
FIXTURE_SHA = 'd4c04a72b89bc7d33c292a16a57c38f5da540815dc2e141adfd58c1bd9973af2'
BUILD = SOURCE / 'build/tls-empty-config'
ENV = {k: v for k, v in os.environ.items() if not any(x in k.upper() for x in ('TOKEN', 'SECRET', 'PASSWORD'))}
ENV.update(CI='true', CARGO_BUILD_JOBS='2')
DEADLINE = time.monotonic() + 240 * 60
STEPS = []


def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()


def git(*args):
return subprocess.check_output(['git', '-C', str(SOURCE), *args], timeout=120, text=True).strip()


def save(**facts):
(OUT / 'proof.json').write_text(json.dumps(dict(passed=False, bun=BUN, webkit=WEBKIT,
patch_sha256=PATCH, source_before=BEFORE, source_after=AFTER, steps=STEPS, **facts), indent=2) + '\n')


def run(name, argv, seconds):
log = OUT / (name + '.log')
step = dict(name=name, argv=argv, exit_code=None, timed_out=False)
STEPS.append(step)
save()
with log.open('xb') as stream:
process = subprocess.Popen(argv, cwd=SOURCE, env=ENV, stdin=subprocess.DEVNULL,
stdout=stream, stderr=subprocess.STDOUT, start_new_session=True)
started = time.monotonic()
try:
while process.poll() is None:
if time.monotonic() >= min(DEADLINE, started + seconds):
step['timed_out'] = True
raise TimeoutError(name)
assert sum(p.stat().st_size for p in OUT.iterdir() if p.is_file()) <= 64 * 1024**2, 'proof exceeds 64 MiB'
time.sleep(1)
finally:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
process.wait()
stream.flush()
step['exit_code'] = process.returncode
save()
assert process.returncode == 0 and not step['timed_out'], name
return log.read_text(errors='replace')


def source_state(adapted):
assert git('rev-parse', 'HEAD') == BUN
assert git('write-tree') == original_tree
assert git('diff', '--no-renames', '--name-only', 'HEAD').splitlines() == ([OWNER] if adapted else [])
assert not git('ls-files', '--others', '--exclude-standard')
assert sha(SOURCE / OWNER) == (AFTER if adapted else BEFORE)
assert sha(SOURCE / FIXTURE) == FIXTURE_SHA


OUT.mkdir(exist_ok=True)
original_tree = git('rev-parse', 'HEAD^{tree}')
source_state(False)
assert os.uname().machine == 'aarch64'
assert sha(INPUTS / 'tls-config-repair.patch') == PATCH
save()
run('apply-check', ['git', 'apply', '--check', str(INPUTS / 'tls-config-repair.patch')], 60)
run('apply', ['git', 'apply', str(INPUTS / 'tls-config-repair.patch')], 60)
source_state(True)
run('rustfmt', ['rustup', 'run', 'nightly-2026-09-15', 'rustfmt', '--edition', '2024',
'--config-path', 'rustfmt.toml', '--config', 'skip_children=true', '--check', OWNER], 120)
source_state(True)
run('build', ['bun', 'scripts/build.ts', '--profile=release', '--ci=on', '--os=linux',
'--arch=aarch64', '--abi=gnu', '--build-dir=build/tls-empty-config', '-j2'], 13800)
source_state(True)
binary, profile = BUILD / 'bun', BUILD / 'bun-profile'
assert binary.is_file() and profile.is_file()
identity = json.loads(run('identity', [str(binary), '-e',
'console.log(JSON.stringify({commit:Bun.revision,webkit:process.versions.webkit,platform:process.platform,arch:process.arch}))'], 60))
assert identity == dict(commit=BUN, webkit=WEBKIT, platform='linux', arch='arm64'), identity
node = json.loads(run('node', ['node', '-e',
'console.log(JSON.stringify({node:process.versions.node,bun:process.versions.bun??null,platform:process.platform,arch:process.arch}))'], 60))
assert node == dict(node='26.12.1', bun=None, platform='linux', arch='arm64'), node
symbols = run('symbols', ['llvm-nm', '--defined-only', '--format=posix', str(profile)], 120)
names = [line.split()[0] for line in symbols.splitlines() if re.fullmatch(
r'_RNv\w*11bun_runtime6socket11socket_body\w*20DuplexUpgradeContext6deinit', line.split()[0])]
assert len(names) == 1, 'repair deinit symbol missing or ambiguous'
run('deinit-disassembly', ['llvm-objdump', '--disassemble-symbols=' + names[0], '--demangle', str(profile)], 120)
files = json.loads(run('test-files', ['bun', '-e',
'import {tlsQualificationFiles} from "./scripts/openclaw-release/release.ts";console.log(JSON.stringify(tlsQualificationFiles))'], 60))
assert len(files) == 6 and len(set(files)) == 6 and FIXTURE in files
fixture_hashes = {name: sha(SOURCE / name) for name in files}
cases = []
for index, name in enumerate(files):
xml = OUT / ('tls-' + str(index) + '.xml')
run('tls-' + str(index), [str(binary), 'test', name, '--reporter=junit', '--reporter-outfile=' + str(xml)], 600)
selected = list(ElementTree.parse(xml).iter('testcase'))
assert selected and all(c.find('failure') is None and c.find('error') is None for c in selected), name
assert any(c.find('skipped') is None for c in selected), name
cases += selected
source_state(True)
for name in ['a TLS Duplex completion retains its owner and is inert after destroy',
'TLS Duplex cycles collect after startup while a retained origin keeps its wrapper alive']:
selected = [case for case in cases if case.get('name') == name]
assert len(selected) == 1 and selected[0].find('skipped') is None, name
assert len(cases) == 291 and sum(c.find('skipped') is not None for c in cases) == 8
assert fixture_hashes == {name: sha(SOURCE / name) for name in files}
identities = dict(binary=sha(binary), profile=sha(profile), build_ninja=sha(BUILD / 'build.ninja'))
run('restore', ['git', 'restore', '--source=' + BUN, '--worktree', '--', OWNER], 60)
source_state(False)
save(completed=True, identity=identity, binary_sha256=identities, fixture_sha256=fixture_hashes,
passed_cases=283, skipped_cases=8, source_restored=True)
result = json.loads((OUT / 'proof.json').read_text())
result['passed'] = True
(OUT / 'proof.json').write_text(json.dumps(result, indent=2) + '\n')
Loading
Loading