nmarshall commited on
Commit
710340d
·
1 Parent(s): 1c27bee

Recover native BSP plane node and leaf payload loading

Browse files
docs/ASSET_PIPELINE_TODO.md CHANGED
@@ -202,3 +202,5 @@ Names and output formats below are proposals to refine after the relevant resear
202
  - [ ] AP-133 Produce an exhaustive conversion report and resolve all required-content blockers before release.
203
 
204
  Native collision follow-up: [BSP planes](research/BSP_PLANES.md) records type/sign fields and 16-byte native node addressing. Connect these to file-lump conversion only after loading/type/sign producers and valid counts are recovered (RE-320); native in-memory offsets are not a disk-format specification.
 
 
 
202
  - [ ] AP-133 Produce an exhaustive conversion report and resolve all required-content blockers before release.
203
 
204
  Native collision follow-up: [BSP planes](research/BSP_PLANES.md) records type/sign fields and 16-byte native node addressing. Connect these to file-lump conversion only after loading/type/sign producers and valid counts are recovered (RE-320); native in-memory offsets are not a disk-format specification.
205
+
206
+ Native loading follow-up: [BSP payload loading](research/BSP_LOADING.md) connects plane/node/leaf descriptors 2/3/4 to native conversion. Preserve original disk fields and every map variant; native collision projection omits fields that rendering or other consumers may need. Complete range/reference validation and actual-map conversion remain RE-324 work.
docs/FINDINGS.md CHANGED
@@ -336,3 +336,9 @@ Bounded qagame paths recover bomb-plant/inactivity correction requests, direct f
336
  **F-098 — Observed general plane boundaries:** eight verified sign dispatches classify corner projections with front >= distance and back < distance. Maximum-face equality can differ from axial classification. Native arithmetic order is retained; arbitrary rounding, exceptional inputs and platform behavior remain open. [PLANE-002](research/BSP_PLANES.md).
337
 
338
  **F-099 — Observed native BSP entry/order:** 16-byte native node records hold plane pointer +0 and children +8/+12; negative children encode complemented leaf IDs. Bounded one-node compositions visit child 0 before child 1 when crossing. Full unrolled/recursive traversal, callback effects, allocation validity and runtime behavior remain open. [PLANE-003](research/BSP_PLANES.md).
 
 
 
 
 
 
 
336
  **F-098 — Observed general plane boundaries:** eight verified sign dispatches classify corner projections with front >= distance and back < distance. Maximum-face equality can differ from axial classification. Native arithmetic order is retained; arbitrary rounding, exceptional inputs and platform behavior remain open. [PLANE-002](research/BSP_PLANES.md).
337
 
338
  **F-099 — Observed native BSP entry/order:** 16-byte native node records hold plane pointer +0 and children +8/+12; negative children encode complemented leaf IDs. Bounded one-node compositions visit child 0 before child 1 when crossing. Full unrolled/recursive traversal, callback effects, allocation validity and runtime behavior remain open. [PLANE-003](research/BSP_PLANES.md).
339
+
340
+ **F-100 — Observed native plane production:** 16-byte disk payloads become 20-byte records; type chooses the first component exactly +1, otherwise 3, and strict negative comparisons produce signbits. Negative zero is copied without setting a bit. Tested allocation reserves twelve spare records; their initialization/use and runtime behavior remain open. [LOAD-001](research/BSP_LOADING.md).
341
+
342
+ **F-101 — Observed node reconstruction:** 36-byte disk records become 16-byte native nodes containing plane_base + 20 * signed index and unchanged child words. Sampled conversion performs no local plane-index guard and omits the six trailing disk words. Complete file validation and native ownership/runtime behavior remain open. [LOAD-002](research/BSP_LOADING.md).
343
+
344
+ **F-102 — Observed leaf projection/state:** 48-byte disk records become 24-byte native leaves in word order 0,1,10,11,8,9. Zero-seeded running cluster/area words track tested maximum nonnegative IDs plus one; leaf storage reserves two spare records and later requests area-related storage. Reference semantics, initialization, count overflow and runtime conformance remain open. [LOAD-003](research/BSP_LOADING.md).
docs/PROVISIONAL_TODO.md CHANGED
@@ -514,3 +514,5 @@ This revision contains **384 provisional tasks**. Select a few bounded items at
514
  Before marking a broad subsystem complete, reconcile it with the canonical checklist, the per-file coverage registry, variant conversion reports, runtime fixtures and the deviation register. Keep unfinished and blocked work visible with the specific missing evidence or dependency.
515
 
516
  Research progress: [BSP plane boundaries and bounded node traversal](research/BSP_PLANES.md) completes bounded RE-317–319. Broad geometry/runtime/port milestones remain open; see RE-320 for full traversal and loading work.
 
 
 
514
  Before marking a broad subsystem complete, reconcile it with the canonical checklist, the per-file coverage registry, variant conversion reports, runtime fixtures and the deviation register. Keep unfinished and blocked work visible with the specific missing evidence or dependency.
515
 
516
  Research progress: [BSP plane boundaries and bounded node traversal](research/BSP_PLANES.md) completes bounded RE-317–319. Broad geometry/runtime/port milestones remain open; see RE-320 for full traversal and loading work.
517
+
518
+ Research progress: [native BSP payload loading](research/BSP_LOADING.md) completes bounded RE-321–323. Complete asset conversion and geometry milestones remain open; RE-324 tracks actual variant audits, validation, reference ownership and runtime fixtures.
docs/RESEARCH_LEDGER.md CHANGED
@@ -181,3 +181,7 @@ Completed bounded/static RE-313–315 with `analyze_leaf_metadata.py`: ten verif
181
  ## 2026-10-11 — Native BSP plane boundaries and root traversal
182
 
183
  Completed bounded RE-317–319 with `analyze_bsp_planes.py`: four hash-verified native graphs, two verified eight-entry jump tables, 124 plane classifications, ten node-entry and eight single-node traversal slices. PLANE-001–003/F-097–099 distinguish axial/general maximum-face equality, finite corner classification from arbitrary rounding, and local node order from complete recursion. Eight independent tests check geometry/table/order/paired outcomes and evaluator carry/byte/address semantics. The full suite passes 246 tests; new and preceding reports regenerate byte-identically, storage and documentation checks pass. RE-316 remains open; RE-320 tracks loading/ownership, complete unrolled/recursive/callback composition, actual maps, float environment and runtime/platform conformance. No original process or native callee executes.
 
 
 
 
 
181
  ## 2026-10-11 — Native BSP plane boundaries and root traversal
182
 
183
  Completed bounded RE-317–319 with `analyze_bsp_planes.py`: four hash-verified native graphs, two verified eight-entry jump tables, 124 plane classifications, ten node-entry and eight single-node traversal slices. PLANE-001–003/F-097–099 distinguish axial/general maximum-face equality, finite corner classification from arbitrary rounding, and local node order from complete recursion. Eight independent tests check geometry/table/order/paired outcomes and evaluator carry/byte/address semantics. The full suite passes 246 tests; new and preceding reports regenerate byte-identically, storage and documentation checks pass. RE-316 remains open; RE-320 tracks loading/ownership, complete unrolled/recursive/callback composition, actual maps, float environment and runtime/platform conformance. No original process or native callee executes.
184
+
185
+ ## 2026-10-11 — Native BSP payload loading
186
+
187
+ Completed bounded RE-321–323 with `analyze_bsp_loading.py`: eight verified graphs, six header descriptor-routing slices, 72 supplied-allocation plane/node/leaf payload conversions and corresponding allocation-size slices. LOAD-001–003/F-100–102 distinguish disk/native records, +1 type priority from unit-normal validation, signed plane-pointer construction from valid indices, and projected leaf words/maxima from complete ownership. Eight independent tests cover field copies/type/sign/padding, signed pointers/children, leaf projection/maxima, allocation/routing arithmetic, exact byte writes, paired outputs and restricted byte/conditional/signed-branch evaluator semantics. The full suite passes 254 tests; current and preceding reports regenerate byte-identically, storage and documentation checks pass. RE-320 remains open; RE-324 tracks complete validation/reset/allocator/spare-record/reference/portal contracts, actual BSP variants, full traversal and runtime/platform behavior. No original process, allocator or native callee executes.
docs/REVERSE_ENGINEERING_TODO.md CHANGED
@@ -382,3 +382,8 @@ Purpose: turn the supplied installation into an evidence-backed specification of
382
  - [x] RE-318 Verify the eight-entry general-plane jump table and bounded sign/corner/projection/equality classifications ([PLANE-002](research/BSP_PLANES.md)).
383
  - [x] RE-319 Recover bounded node-entry addressing and one-node negative-child visit order ([PLANE-003](research/BSP_PLANES.md)).
384
  - [ ] RE-320 Recover BSP node/plane/leaf loading/count/lifetime and sign/type producers, all unrolled/recursive query paths with callback capacity/overflow effects, real-map fixtures, exceptional float environment and original-runtime/all-platform conformance.
 
 
 
 
 
 
382
  - [x] RE-318 Verify the eight-entry general-plane jump table and bounded sign/corner/projection/equality classifications ([PLANE-002](research/BSP_PLANES.md)).
383
  - [x] RE-319 Recover bounded node-entry addressing and one-node negative-child visit order ([PLANE-003](research/BSP_PLANES.md)).
384
  - [ ] RE-320 Recover BSP node/plane/leaf loading/count/lifetime and sign/type producers, all unrolled/recursive query paths with callback capacity/overflow effects, real-map fixtures, exceptional float environment and original-runtime/all-platform conformance.
385
+
386
+ - [x] RE-321 Recover paired bounded plane payload conversion, +1 component type priority, signbits/padding and allocation/header routing ([LOAD-001](research/BSP_LOADING.md)).
387
+ - [x] RE-322 Recover disk-to-native node reconstruction, signed stride-20 plane pointers, unchanged children and allocation sizes ([LOAD-002](research/BSP_LOADING.md)).
388
+ - [x] RE-323 Recover leaf word projection, running cluster/area maxima and bounded leaf/area/portal allocation requests ([LOAD-003](research/BSP_LOADING.md)).
389
+ - [ ] RE-324 Recover complete header/lump/error/endian/count validation, allocator/reset/spare-record initialization, brush/surface references and area/portal ownership; audit actual BSP variants and compose full loading/traversal/callback/runtime/all-platform fixtures.
docs/research/BSP_LOADING.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Native BSP plane, node and leaf loading
2
+
3
+ Reproduce: `python3 scripts/analyze_bsp_loading.py`. Tests: `python3 scripts/test_bsp_loading.py`. Evidence: [bsp-loading.json](bsp-loading.json), containing hash-verified graphs for CM_LoadMap and three loaders in both Linux ELF64 engines. This follows [plane classification](BSP_PLANES.md) and [leaf metadata](LEAF_METADATA.md).
4
+
5
+ These are bounded synthetic instruction compositions with supplied allocation returns, not original executable/callee execution. Every payload probe starts after the first allocator call and stops before an epilogue or later allocator. First allocation sizes and descriptor routing are separate instruction slices; they do not execute loader entry validation. The leaf auxiliary-allocation continuation also supplies an allocator return. No real map is loaded in this batch.
6
+
7
+ ## LOAD-001 — Plane conversion and derived fields (RE-321)
8
+
9
+ CM_LoadMap copies 0x90 bytes into a local header and compares version against 46 in the retained graph. The bounded routing slice passes header +24 to CMod_LoadPlanes, corresponding to descriptor 2 under the eight-byte prefix/eight-byte descriptor layout. Header +32 and +40 route nodes and leaves to descriptors 3 and 4. Native header read safety, magic checks and the complete file-bound validation contract remain open.
10
+
11
+ | Plane representation | Fields |
12
+ | --- | --- |
13
+ | Disk record, 16 bytes | normal float bits +0/+4/+8; distance bits +12 |
14
+ | Native record, 20 bytes | same four words; type byte +16; signbits byte +17; padding +18/+19 |
15
+
16
+ The tested allocation request is `20 * (count + 12)` bytes. The twelve spare plane records are not initialized by the sampled payload loops. Their later use remains open. Payload writes preserve all four original words, including negative zero. Padding remains the supplied 0xa5 sentinel; this does not establish real allocator initialization.
17
+
18
+ For finite normals, type is the first component index whose value equals **+1.0**, otherwise 3. There is no local normalization check in the sampled conversion. Thus `(1,-2,3)` gets type 0 and `(-1,0,0)` gets type 3. This producer rule explains why negative axis normals reach the general classifier. Signbits are the three component `< 0.0` predicates; negative zero does not set a bit. The comparison constant comes from the verified file-backed ELF section.
19
+
20
+ Twenty-four paired plane payload probes cover counts 1–9, 16, 17 and 31, including odd/even peel and pair-loop paths. The 31-record case samples every normal in `{-1,0,1}^3`, negative zero, fractional components and two nonunit normals with +1 components. Exceptional values and MXCSR-dependent behavior remain open.
21
+
22
+ ## LOAD-002 — Native node reconstruction (RE-322)
23
+
24
+ | Node representation | Fields used by this loader |
25
+ | --- | --- |
26
+ | Disk record, 36 bytes | signed plane index +0; child words +4/+8 |
27
+ | Native record, 16 bytes | plane pointer +0; unchanged child words +8/+12 |
28
+
29
+ The allocation request is `16 * count`. The loader sign-extends the disk plane index and computes `plane_base + 20 * index`; it does not locally check that index before writing the pointer in sampled paths. A deliberately invalid -1 index therefore produces `plane_base - 20` without dereferencing that plane. This observation is not a valid-map or memory-safety guarantee. Child bits are copied unchanged; their node/negative-leaf meaning is corroborated by the earlier traversal evidence.
30
+
31
+ Disk offsets +12 through +32 are not copied into native collision nodes. This is a collision-loader projection, not permission for the asset converter to discard source bounds or other consumers' requirements. Twenty-four paired payload probes exercise counts across remainder and four-record unrolling, preserve all child words and write exactly the native records.
32
+
33
+ ## LOAD-003 — Leaf projection and allocation state (RE-323)
34
+
35
+ The allocation request is `24 * (count + 2)` bytes. The two spare leaf records remain untouched in these payload probes.
36
+
37
+ | Disk offset (48-byte record) | Native offset (24-byte record) |
38
+ | --- | --- |
39
+ | +0 cluster word | +0 |
40
+ | +4 area word | +4 |
41
+ | +40 | +8 |
42
+ | +44 | +12 |
43
+ | +32 | +16 |
44
+ | +36 | +20 |
45
+
46
+ Disk offsets +8 through +28 are not copied. Reference-word semantics and complete brush/surface array ownership must still be connected to their loaders and consumers; the table records exact word movement without assuming native/disk layout identity.
47
+
48
+ With cluster and area running words explicitly seeded to zero, tested signed values update each running word to the maximum nonnegative ID plus one. Cluster -1 leaves the seeded cluster count unchanged. Repeated IDs do not grow counts. The maximum-word storage addresses match across the observed unrolled lanes within each engine. The collector records their roles separately.
49
+
50
+ After the payload, the loader requests `8 * area_count` bytes for one auxiliary allocation. With an explicitly supplied return for that call, the next instruction slice requests `4 * area_count * area_count` bytes using a 32-bit square before sign extension. The report samples small valid counts only; overflow, allocator zeroing, portal/area structures and reset/lifetime remain open.
51
+
52
+ Twenty-four paired leaf payload probes cover odd/even peel and pair-loop paths. The report also retains the static entry checks: plane length alignment, node/leaf reciprocal-division size gates and nonpositive-count error requests. Recorded error texts are `MOD_LoadBmodel: funny lump size`, `Map with no planes`, `Map has no nodes` and `Map with no leafs`. These guards are not exercised; complete malformed-lump behavior is not claimed.
53
+
54
+ ## Validation and follow-up
55
+
56
+ Eight independent tests cover copy/type/sign/padding behavior, signed node pointer arithmetic and child bits, leaf word reordering/maxima, allocation arithmetic, header descriptor routing, exact per-record byte writes, paired outcomes and restricted evaluator byte/conditional/signed-branch semantics. No probe writes into a spare record. Larger instruction traces retain counts/hashes; small traces retain addresses.
57
+
58
+ RE-324: validate complete header/lump bounds, alignment/division/error paths, endian handling and count overflow; recover allocator/reset/zeroing and spare-record initialization; recover leaf brush/surface references, area/portal structures and lifetime; audit every actual BSP variant and compose loading with full recursive query/callback paths; obtain original-runtime and other-platform differential fixtures. Rendering geometry and Rust/Bevy/Solari conversion remain separate open work.
docs/research/BSP_PLANES.md CHANGED
@@ -40,3 +40,5 @@ The retained full traversal graph contains nine plane call sites and unrolled ne
40
  The report contains 124 plane classifications, ten entry slices and eight root compositions. Eight independent tests compare axial rules, all-corner projection extrema, sign dispatch, invalid-sign behavior, equality differences, node addressing/order and paired outcomes. Evaluator tests cover byte unsigned flags, carry subtraction, low-byte register preservation and indexed addresses. Unknown flags, unsupported operations and unknown memory remain errors.
41
 
42
  RE-320: recover node/plane/leaf lump loading, allocation/count/lifetime and sign/type producers; compose all unrolled and recursive query paths with callback effects and capacity boundaries; sample real maps; recover float exceptional/MXCSR behavior and differential original-runtime/platform fixtures. No complete collision geometry, original-runtime conformance, or Rust/Bevy/Solari implementation is claimed.
 
 
 
40
  The report contains 124 plane classifications, ten entry slices and eight root compositions. Eight independent tests compare axial rules, all-corner projection extrema, sign dispatch, invalid-sign behavior, equality differences, node addressing/order and paired outcomes. Evaluator tests cover byte unsigned flags, carry subtraction, low-byte register preservation and indexed addresses. Unknown flags, unsupported operations and unknown memory remain errors.
41
 
42
  RE-320: recover node/plane/leaf lump loading, allocation/count/lifetime and sign/type producers; compose all unrolled and recursive query paths with callback effects and capacity boundaries; sample real maps; recover float exceptional/MXCSR behavior and differential original-runtime/platform fixtures. No complete collision geometry, original-runtime conformance, or Rust/Bevy/Solari implementation is claimed.
43
+
44
+ Follow-up: [BSP payload loading](BSP_LOADING.md) establishes bounded +1 type/sign producers, native plane stride, node reconstruction and leaf projection. RE-320 remains open for complete loading/validation/recursive-query/runtime contracts.
docs/research/README.md CHANGED
@@ -115,3 +115,5 @@ The extension collector adds five focused tests for native tail calls, command-r
115
  [Leaf areas, clusters and query outputs](LEAF_METADATA.md) adds RE-313–315: all-lane area/cluster compositions, exact overflow fallback and guarded leaf/store callback contracts. Reproduce with `python3 scripts/analyze_leaf_metadata.py`; check with `python3 scripts/test_leaf_metadata.py`. Full BSP traversal/layout/output ownership, consumers and original-runtime/platform conformance remain open.
116
 
117
  [Native BSP planes and bounded node traversal](BSP_PLANES.md) adds RE-317–319: axial/general equality rules, verified eight-case sign dispatch and one-node child order/addressing. Reproduce with `python3 scripts/analyze_bsp_planes.py`; check with `python3 scripts/test_bsp_planes.py`. Full traversal/loading/callback/float/runtime/platform contracts remain open.
 
 
 
115
  [Leaf areas, clusters and query outputs](LEAF_METADATA.md) adds RE-313–315: all-lane area/cluster compositions, exact overflow fallback and guarded leaf/store callback contracts. Reproduce with `python3 scripts/analyze_leaf_metadata.py`; check with `python3 scripts/test_leaf_metadata.py`. Full BSP traversal/layout/output ownership, consumers and original-runtime/platform conformance remain open.
116
 
117
  [Native BSP planes and bounded node traversal](BSP_PLANES.md) adds RE-317–319: axial/general equality rules, verified eight-case sign dispatch and one-node child order/addressing. Reproduce with `python3 scripts/analyze_bsp_planes.py`; check with `python3 scripts/test_bsp_planes.py`. Full traversal/loading/callback/float/runtime/platform contracts remain open.
118
+
119
+ [Native BSP plane, node and leaf loading](BSP_LOADING.md) adds RE-321–323: descriptor routing, bounded payload conversion, derived plane fields, native node pointers, leaf projection/maxima and allocation requests. Reproduce with `python3 scripts/analyze_bsp_loading.py`; check with `python3 scripts/test_bsp_loading.py`. Complete validation/loading/ownership/recursive-query/runtime/platform contracts remain open.
docs/research/bsp-loading.json ADDED
The diff for this file is too large to render. See raw diff
 
scripts/analyze_bsp_loading.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Bounded native plane/node/leaf payload conversion after supplied allocation."""
3
+ import gzip
4
+ import hashlib
5
+ import itertools
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ from analyze_chat_crosshair import bits
10
+ from analyze_chat_paths import native_string
11
+ from analyze_collision_composition import evaluate,get_word,put_word
12
+ from analyze_engine_control import control_flow
13
+ from analyze_frame_timing import read_address
14
+ from analyze_native import elf
15
+ from analyze_qvm import write_json
16
+ from analyze_vm_extensions import native_functions,verified
17
+
18
+ NORMALS=list(itertools.product([-1.,0.,1.],repeat=3))+[(-0.,0.,0.),(.5,-.25,.75),(1.,-2.,3.),(0.,1.,-2.)]
19
+ COUNTS=list(range(1,10))+[16,17,31]
20
+
21
+
22
+ def digest(trace):return hashlib.sha256(json.dumps(trace,separators=(',',':')).encode()).hexdigest()
23
+
24
+
25
+ def main():
26
+ out={'native':{},'limitations':'Synthetic native payload conversion after an explicitly supplied '
27
+ 'first allocation return; leaf auxiliary-allocation continuation also supplies a return. No allocation, error, original process or native callee executes; '
28
+ 'execution stops before epilogues/later allocations. Entry length/division gates '
29
+ 'is retained as static evidence, not executed. No actual BSP file is loaded by these probes. '
30
+ 'Complete validation/ownership/reset/endian/exceptional float and runtime/platform contracts remain open.'}
31
+ source,output,plane_base=0x10000,0x30000,0x50000
32
+ for label in ['linux--Quake3-UrT-Ded.x86_64','linux--Quake3-UrT.x86_64']:
33
+ r=json.loads(Path(f'docs/research/native/{label}.json').read_text());data=verified((Path('.cache/UrbanTerror43')/r['source']['path']).read_bytes(),r['source']['sha256'])
34
+ a=r['artifacts']['disassembly.txt'];fs=native_functions(verified(gzip.open(a['path'],'rb').read(),a['uncompressed_sha256']).decode());metadata,_=elf(data)
35
+ graphs={n:control_flow(fs[n]) for n in ['CMod_LoadPlanes','CMod_LoadNodes','CMod_LoadLeafs','CM_LoadMap']};results={};routing={}
36
+ caller=graphs['CM_LoadMap']['instructions']
37
+ for name in ['CMod_LoadPlanes','CMod_LoadNodes','CMod_LoadLeafs']:
38
+ i=next(i for i,x in enumerate(caller) if '<'+name+'>' in x['operands'])
39
+ if caller[i-2]['opcode']!='leaq' or caller[i-2]['operands']!='0x10(%rsp), %rdi' or caller[i-1]['opcode']!='addq':raise ValueError('header routing changed')
40
+ offset=int(caller[i-1]['operands'].split(',')[0][1:],0)
41
+ if offset<8 or (offset-8)%8:raise ValueError('unexpected lump descriptor offset')
42
+ t=evaluate(graphs['CM_LoadMap'],caller[i-2]['address'],[caller[i]['address']],{}, {'%rsp':0x90000},capture_registers=True)
43
+ routing[name]={'header_relative_offset':offset,'lump_index':(offset-8)//8,'descriptor_pointer':t['registers']['rdi'],'trace':t['trace'],'call':caller[i]['address']}
44
+
45
+ for name in ['CMod_LoadPlanes','CMod_LoadNodes','CMod_LoadLeafs']:
46
+ g=graphs[name]
47
+ rows=g['instructions'];alloc=[i for i,x in enumerate(rows) if '<Hunk_Alloc>' in x['operands']];first=alloc[0];start=rows[first+1]['address']
48
+ end=rows[alloc[1]]['address'] if name=='CMod_LoadLeafs' else next(x['address'] for x in rows[first+1:] if x['opcode']=='popq')
49
+ static_globals=[];seed={}
50
+ for x in rows:
51
+ if '(%rip)' not in x['operands']:continue
52
+ address=int(re.search(r'#\s*0x([0-9a-f]+)',x['operands'])[1],16)
53
+ static_globals.append({'address':address,'instruction':x['address'],'opcode':x['opcode'],'operands':x['operands']})
54
+ if x['opcode']=='movss':
55
+ raw,_=read_address(data,metadata['sections'],address,4)
56
+ for i,v in enumerate(raw):seed[address+i]=v
57
+ if name=='CMod_LoadNodes':
58
+ slots={x['address'] for x in static_globals if x['opcode']=='addq'}
59
+ if len(slots)!=1:raise ValueError('node plane-base slot changed')
60
+ for slot in slots:put_word(seed,slot,plane_base,8)
61
+ elif name=='CMod_LoadLeafs':
62
+ slots={x['address'] for x in static_globals if x['opcode']=='cmpl'}
63
+ if len(slots)!=2:raise ValueError('leaf running maxima changed')
64
+ for slot in slots:put_word(seed,slot,0)
65
+ if name=='CMod_LoadPlanes':allocation_start=next(x['address'] for x in rows[:first] if x['opcode']=='leal' and x['operands']=='0xc(%rbp), %ecx')
66
+ elif name=='CMod_LoadLeafs':allocation_start=next(x['address'] for x in rows[:first] if x['opcode']=='leal' and x['operands']=='0x2(%rbp), %r9d')
67
+ else:allocation_start=next(x['address'] for x in rows[:first] if x['opcode']=='movslq' and x['operands']=='%r14d, %rdi')
68
+ probes=[]
69
+ for count in COUNTS:
70
+ m=dict(seed);disk=[];disk_stride={'CMod_LoadPlanes':16,'CMod_LoadNodes':36,'CMod_LoadLeafs':48}[name]
71
+ native_stride={'CMod_LoadPlanes':20,'CMod_LoadNodes':16,'CMod_LoadLeafs':24}[name]
72
+ for i in range(count):
73
+ if name=='CMod_LoadPlanes':words=[bits(v) for v in NORMALS[i%len(NORMALS)]]+[bits(i-.5)]
74
+ elif name=='CMod_LoadNodes':words=[[-1,0,1,2,7][i%5],-i-1,i+1]+[0x1100+i*16+j for j in range(6)]
75
+ else:words=[[-1,0,2,2,7][i%5],[0,3,1,0][i%4]]+[0x2200+i*16+j for j in range(6)]+[100+i,200+i,300+i,400+i]
76
+ disk.append([v&0xffffffff for v in words])
77
+ for j,v in enumerate(words):put_word(m,source+disk_stride*i+4*j,v)
78
+ # Fill destination with sentinels, including native padding and spare records.
79
+ for i in range(native_stride*(count+12)+2):m[output-1+i]=0xa5
80
+ allocation=evaluate(g,allocation_start,[rows[first]['address']],m,{'%rbp':count,'%r14':0,'%r13':source},capture_registers=True)
81
+ regs={'%rax':output,'%rbx':source,'%rbp':count,'%r12':count}
82
+ t=evaluate(g,start,[end],m,regs,limit=4096,capture_registers=True)
83
+ records=[]
84
+ for i in range(count):
85
+ p=output+native_stride*i
86
+ if name=='CMod_LoadPlanes':records.append({'words':[get_word(t['memory'],p+4*j) for j in range(4)],'type':get_word(t['memory'],p+16,1),'signbits':get_word(t['memory'],p+17,1),'padding':[t['memory'][p+18],t['memory'][p+19]]})
87
+ elif name=='CMod_LoadNodes':records.append({'plane_pointer':get_word(t['memory'],p,8),'children':[get_word(t['memory'],p+8),get_word(t['memory'],p+12)]})
88
+ else:records.append([get_word(t['memory'],p+4*j) for j in range(6)])
89
+ portal_allocation=None
90
+ if name=='CMod_LoadLeafs':
91
+ continuation=dict(t['registers']);continuation['rax']=0x70000
92
+ portal_allocation=evaluate(g,rows[alloc[1]+1]['address'],[rows[alloc[2]]['address']],t['memory'],continuation,capture_registers=True)
93
+ destination_writes=[w for w in t['writes'] if output<=w['address']<output+native_stride*(count+12)]
94
+ outside=[w for w in t['writes'] if output-1<=w['address']<output or output+native_stride*count<=w['address']<output+native_stride*(count+12)]
95
+ probes.append({'count':count,'allocation_bytes':allocation['registers']['rdi'],'allocation_trace':allocation['trace'],'input_words':disk,'records':records,'destination_writes':destination_writes,'writes_outside_records':outside,
96
+ 'globals':{str(x['address']):get_word(t['memory'],x['address'],8 if x['opcode']=='movq' else 4) for x in static_globals if x['operands'].split('#')[0].strip().endswith('(%rip)') and x['opcode'] in ['movq','movl'] and all(x['address']+j in t['memory'] for j in range(8 if x['opcode']=='movq' else 4))},
97
+ 'portal_allocation_bytes':portal_allocation['registers']['rdi'] if portal_allocation else None,'portal_allocation_trace':portal_allocation['trace'] if portal_allocation else None,'next_allocation_bytes':t['registers']['rdi'] if name=='CMod_LoadLeafs' else None,'trace':t['trace'] if count<=4 else None,'trace_length':len(t['trace']),'trace_sha256':digest(t['trace'])})
98
+ errors=[]
99
+ for i,x in enumerate(rows):
100
+ if '<Com_Error>' not in x['operands']:continue
101
+ fmt=next(y for y in reversed(rows[max(0,i-4):i]) if y['opcode']=='movl' and y['operands'].split('#')[0].strip().endswith(', %esi'))
102
+ address=int(fmt['operands'].split(',')[0][1:],0)
103
+ errors.append({'call':x['address'],'text':native_string(data,metadata['sections'],address)})
104
+ maxima={}
105
+ if name=='CMod_LoadLeafs':
106
+ for role,reg in [('cluster','%edx'),('area','%eax')]:
107
+ maxima[role]=next(x['address'] for x in static_globals if x['opcode']=='cmpl' and x['operands'].split('#')[0].strip().endswith(', '+reg))
108
+ results[name]={'maxima_slots':maxima,'start_after_supplied_allocation':start,'stop_before_epilogue_or_allocation':end,'global_references':static_globals,'error_requests':errors,'probes':probes}
109
+ out['native'][label]={'source':r['source'],'disassembly_sha256':a['uncompressed_sha256'],'graphs':graphs,'lump_routing':routing,'loaders':results}
110
+ write_json(Path('docs/research/bsp-loading.json'),out)
111
+ print('Recovered 72 supplied-allocation plane/node/leaf conversion probes')
112
+
113
+
114
+ if __name__=='__main__':main()
scripts/analyze_collision_composition.py CHANGED
@@ -14,10 +14,11 @@ from analyze_vm_extensions import native_functions,verified
14
 
15
  def register(r):
16
  aliases={'eax':('rax',32),'edi':('rdi',32),'esi':('rsi',32),'edx':('rdx',32),
17
- 'ecx':('rcx',32),'ebp':('rbp',32),'ebx':('rbx',32),'al':('rax',8),'sil':('rsi',8)}
18
  r=r.lstrip('%')
19
  if r in aliases:return aliases[r]
20
  if re.fullmatch(r'r\d+d',r):return r[:-1],32
 
21
  if re.fullmatch(r'r(?:ax|di|si|dx|cx|bp|bx|sp|ip|\d+)',r):return r,64
22
  if re.fullmatch(r'xmm\d+',r):return r,32
23
  raise ValueError('unsupported register '+r)
@@ -37,7 +38,7 @@ def evaluate(graph,start,stops,memory,registers,limit=128,capture_registers=Fals
37
  regs={register(k)[0]:v for k,v in registers.items()};m=dict(memory);equal=None;ge=greater=below=None;signed_le=signed_lt=negative=None;pc=start;trace=[];writes=[]
38
  def read_reg(name):
39
  key,size=register(name)
40
- if key not in regs:raise ValueError('unknown register')
41
  return regs[key]&((1<<size)-1)
42
  def address(arg):
43
  match=re.fullmatch(r'(-?0x[0-9a-f]+)?\((%\w+)?(?:,(%\w+)(?:,([1248]))?)?\)',arg)
@@ -61,18 +62,18 @@ def evaluate(graph,start,stops,memory,registers,limit=128,capture_registers=Fals
61
  i=indices[pc];pc=rows[i+1]['address'] if i+1<len(rows) else None
62
  regs['rip']=pc
63
  if op in ['nop','nopl','nopw']:pass
64
- elif op in ['movl','movq','movss','movabsq']:
65
- a,b=args.split(', ');width=8 if op in ['movq','movabsq'] else 4;write(b,read(a,width),width)
66
  elif op=='movzbl':
67
  a,b=args.split(', ');write(b,read(a,1),4)
68
  elif op=='sbbl':
69
  if below is None:raise ValueError('unmodeled carry flag')
70
  a,b=args.split(', ');write(b,read(b,4)-read(a,4)-int(below),4)
71
  equal=None;signed_le=signed_lt=negative=None;ge=greater=below=None
72
- elif op=='cmovael':
73
  if ge is None:raise ValueError('unmodeled unsigned flags')
74
  a,b=args.split(', ')
75
- if ge:write(b,read(a,4),4)
76
  elif op=='movaps':
77
  a,b=args.split(', ')
78
  if not a.startswith('%xmm') or not b.startswith('%xmm'):raise ValueError('unsupported vector move')
@@ -112,7 +113,8 @@ def evaluate(graph,start,stops,memory,registers,limit=128,capture_registers=Fals
112
  write(b,v>>(read(a,8)&63),8);equal=None;signed_le=signed_lt=negative=None;ge=greater=below=None
113
  elif op in ['xorl','orl','andl','subl','addl','addq','subq']:
114
  width=8 if op in ['addq','subq'] else 4
115
- a,b=args.split(', ');v,w=read(a,width),read(b,width)
 
116
  result=v^w if op=='xorl' else v|w if op=='orl' else v&w if op=='andl' else w-v if op in ['subl','subq'] else v+w
117
  write(b,result,width);equal=(result&((1<<(width*8))-1))==0
118
  signed_le=signed_lt=negative=None;ge=greater=below=None
@@ -132,15 +134,15 @@ def evaluate(graph,start,stops,memory,registers,limit=128,capture_registers=Fals
132
  write(b,bits(w+v if op=='addss' else w-v if op=='subss' else w*v),4)
133
  elif op=='comiss':
134
  a,b=args.split(', ');v,w=number(read(a,4)),number(read(b,4));equal=v==w;ge=w>=v;greater=w>v;below=w<v;signed_le=signed_lt=negative=None
135
- elif op in ['sete','setbe','setae']:
136
- if op=='sete' and equal is None:raise ValueError('unmodeled zero flag')
137
- if op in ['setbe','setae'] and below is None:raise ValueError('unmodeled unsigned flags')
138
- write(args,int(equal if op=='sete' else ge if op=='setae' else below or equal),1)
139
- elif op in ['je','jne','jae','ja','jb','jbe','jle','jl','js','jns','jmp']:
140
  if op in ['je','jne'] and equal is None:raise ValueError('unmodeled zero flag')
141
- if op in ['jle','jl','js','jns'] and signed_le is None:raise ValueError('unmodeled signed flags')
142
  if op in ['jae','ja','jb','jbe'] and ge is None:raise ValueError('unmodeled unsigned flags')
143
- taken=op=='jmp' or (equal if op=='je' else not equal if op=='jne' else signed_le if op=='jle' else signed_lt if op=='jl' else negative if op=='js' else not negative if op=='jns' else greater if op=='ja' else below if op=='jb' else below or equal if op=='jbe' else ge)
144
  if taken:pc=int(args.split()[0],16)
145
  else:raise ValueError('unsupported composition opcode '+op)
146
  result={'trace':trace,'stop':pc,'memory':m,'writes':writes}
 
14
 
15
  def register(r):
16
  aliases={'eax':('rax',32),'edi':('rdi',32),'esi':('rsi',32),'edx':('rdx',32),
17
+ 'ecx':('rcx',32),'ebp':('rbp',32),'ebx':('rbx',32),'al':('rax',8),'sil':('rsi',8),'dl':('rdx',8)}
18
  r=r.lstrip('%')
19
  if r in aliases:return aliases[r]
20
  if re.fullmatch(r'r\d+d',r):return r[:-1],32
21
+ if re.fullmatch(r'r\d+b',r):return r[:-1],8
22
  if re.fullmatch(r'r(?:ax|di|si|dx|cx|bp|bx|sp|ip|\d+)',r):return r,64
23
  if re.fullmatch(r'xmm\d+',r):return r,32
24
  raise ValueError('unsupported register '+r)
 
38
  regs={register(k)[0]:v for k,v in registers.items()};m=dict(memory);equal=None;ge=greater=below=None;signed_le=signed_lt=negative=None;pc=start;trace=[];writes=[]
39
  def read_reg(name):
40
  key,size=register(name)
41
+ if key not in regs:raise ValueError('unknown register '+name)
42
  return regs[key]&((1<<size)-1)
43
  def address(arg):
44
  match=re.fullmatch(r'(-?0x[0-9a-f]+)?\((%\w+)?(?:,(%\w+)(?:,([1248]))?)?\)',arg)
 
62
  i=indices[pc];pc=rows[i+1]['address'] if i+1<len(rows) else None
63
  regs['rip']=pc
64
  if op in ['nop','nopl','nopw']:pass
65
+ elif op in ['movb','movl','movq','movss','movabsq']:
66
+ a,b=args.split(', ');width=8 if op in ['movq','movabsq'] else 1 if op=='movb' else 4;write(b,read(a,width),width)
67
  elif op=='movzbl':
68
  a,b=args.split(', ');write(b,read(a,1),4)
69
  elif op=='sbbl':
70
  if below is None:raise ValueError('unmodeled carry flag')
71
  a,b=args.split(', ');write(b,read(b,4)-read(a,4)-int(below),4)
72
  equal=None;signed_le=signed_lt=negative=None;ge=greater=below=None
73
+ elif op in ['cmovael','cmovbl']:
74
  if ge is None:raise ValueError('unmodeled unsigned flags')
75
  a,b=args.split(', ')
76
+ if (ge if op=='cmovael' else below):write(b,read(a,4),4)
77
  elif op=='movaps':
78
  a,b=args.split(', ')
79
  if not a.startswith('%xmm') or not b.startswith('%xmm'):raise ValueError('unsupported vector move')
 
113
  write(b,v>>(read(a,8)&63),8);equal=None;signed_le=signed_lt=negative=None;ge=greater=below=None
114
  elif op in ['xorl','orl','andl','subl','addl','addq','subq']:
115
  width=8 if op in ['addq','subq'] else 4
116
+ a,b=args.split(', ')
117
+ v,w=(0,0) if op=='xorl' and a==b else (read(a,width),read(b,width))
118
  result=v^w if op=='xorl' else v|w if op=='orl' else v&w if op=='andl' else w-v if op in ['subl','subq'] else v+w
119
  write(b,result,width);equal=(result&((1<<(width*8))-1))==0
120
  signed_le=signed_lt=negative=None;ge=greater=below=None
 
134
  write(b,bits(w+v if op=='addss' else w-v if op=='subss' else w*v),4)
135
  elif op=='comiss':
136
  a,b=args.split(', ');v,w=number(read(a,4)),number(read(b,4));equal=v==w;ge=w>=v;greater=w>v;below=w<v;signed_le=signed_lt=negative=None
137
+ elif op in ['sete','setne','setb','setbe','setae']:
138
+ if op in ['sete','setne'] and equal is None:raise ValueError('unmodeled zero flag')
139
+ if op in ['setb','setbe','setae'] and below is None:raise ValueError('unmodeled unsigned flags')
140
+ write(args,int(equal if op=='sete' else not equal if op=='setne' else below if op=='setb' else ge if op=='setae' else below or equal),1)
141
+ elif op in ['je','jne','jae','ja','jb','jbe','jle','jl','jge','js','jns','jmp']:
142
  if op in ['je','jne'] and equal is None:raise ValueError('unmodeled zero flag')
143
+ if op in ['jle','jl','jge','js','jns'] and signed_le is None:raise ValueError('unmodeled signed flags')
144
  if op in ['jae','ja','jb','jbe'] and ge is None:raise ValueError('unmodeled unsigned flags')
145
+ taken=op=='jmp' or (equal if op=='je' else not equal if op=='jne' else signed_le if op=='jle' else signed_lt if op=='jl' else not signed_lt if op=='jge' else negative if op=='js' else not negative if op=='jns' else greater if op=='ja' else below if op=='jb' else below or equal if op=='jbe' else ge)
146
  if taken:pc=int(args.split()[0],16)
147
  else:raise ValueError('unsupported composition opcode '+op)
148
  result={'trace':trace,'stop':pc,'memory':m,'writes':writes}
scripts/test_bsp_loading.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Independent disk/native conversion, allocation and instruction oracles."""
3
+ import json
4
+ from pathlib import Path
5
+ import unittest
6
+ from analyze_chat_crosshair import number
7
+ from analyze_collision_composition import evaluate
8
+ from test_bsp_planes import graph
9
+
10
+
11
+ def signed(v):return v if v<2**31 else v-2**32
12
+
13
+
14
+ class BspLoadingTests(unittest.TestCase):
15
+ @classmethod
16
+ def setUpClass(cls):cls.r=json.loads(Path('docs/research/bsp-loading.json').read_text())
17
+
18
+ def test_plane_copy_type_priority_signs_and_padding(self):
19
+ for n in self.r['native'].values():
20
+ for p in n['loaders']['CMod_LoadPlanes']['probes']:
21
+ for original,record in zip(p['input_words'],p['records']):
22
+ normal=[number(v) for v in original[:3]]
23
+ expected_type=next((i for i,v in enumerate(normal) if v==1),3)
24
+ self.assertEqual(record['words'],original);self.assertEqual(record['type'],expected_type)
25
+ self.assertEqual(record['signbits'],sum(1<<i for i,v in enumerate(normal) if v<0))
26
+ self.assertEqual(record['padding'],[0xa5,0xa5])
27
+ final=n['loaders']['CMod_LoadPlanes']['probes'][-1]
28
+ self.assertEqual(final['records'][27]['words'][0],0x80000000)
29
+ self.assertEqual(final['records'][27]['signbits'],0)
30
+ self.assertEqual(final['records'][29]['type'],0) # +1 priority even for a nonunit normal.
31
+
32
+ def test_node_signed_plane_pointer_and_exact_children(self):
33
+ for n in self.r['native'].values():
34
+ for p in n['loaders']['CMod_LoadNodes']['probes']:
35
+ for original,record in zip(p['input_words'],p['records']):
36
+ self.assertEqual(record['plane_pointer'],(0x50000+20*signed(original[0]))&((1<<64)-1))
37
+ self.assertEqual(record['children'],original[1:3])
38
+ self.assertEqual(p['records'][0]['plane_pointer'],0x50000-20)
39
+
40
+ def test_leaf_mapping_and_running_maxima(self):
41
+ for n in self.r['native'].values():
42
+ info=n['loaders']['CMod_LoadLeafs']
43
+ maxima_slots=info['maxima_slots']
44
+ for p in info['probes']:
45
+ expected=[[w[0],w[1],w[10],w[11],w[8],w[9]] for w in p['input_words']]
46
+ self.assertEqual(p['records'],expected)
47
+ for role,index in [('cluster',0),('area',1)]:
48
+ self.assertEqual(p['globals'][str(maxima_slots[role])],max(0,max(signed(w[index]) for w in p['input_words'])+1))
49
+ areas=max(0,max(signed(w[1]) for w in p['input_words'])+1)
50
+ self.assertEqual(p['next_allocation_bytes'],8*areas);self.assertEqual(p['portal_allocation_bytes'],4*areas*areas)
51
+
52
+ def test_allocation_sizes_and_error_texts(self):
53
+ for n in self.r['native'].values():
54
+ for name,info in n['loaders'].items():
55
+ self.assertEqual(len(info['probes']),12)
56
+ for p in info['probes']:
57
+ c=p['count'];expected=20*(c+12) if name.endswith('Planes') else 16*c if name.endswith('Nodes') else 24*(c+2)
58
+ self.assertEqual(p['allocation_bytes'],expected)
59
+ texts={e['text'] for e in info['error_requests']}
60
+ self.assertIn('MOD_LoadBmodel: funny lump size',texts)
61
+ routing=n['lump_routing'][name];index={'CMod_LoadPlanes':2,'CMod_LoadNodes':3,'CMod_LoadLeafs':4}[name]
62
+ self.assertEqual(routing['lump_index'],index);self.assertEqual(routing['descriptor_pointer'],0x90010+8+8*index)
63
+ self.assertIn({'CMod_LoadPlanes':'Map with no planes','CMod_LoadNodes':'Map has no nodes','CMod_LoadLeafs':'Map with no leafs'}[name],texts)
64
+
65
+ def test_exact_write_coverage_and_spare_records(self):
66
+ for n in self.r['native'].values():
67
+ for name,info in n['loaders'].items():
68
+ stride=20 if name.endswith('Planes') else 16 if name.endswith('Nodes') else 24
69
+ used=18 if name.endswith('Planes') else stride
70
+ for p in info['probes']:
71
+ covered=[a for w in p['destination_writes'] for a in range(w['address'],w['address']+w['bytes'])]
72
+ expected=[0x30000+stride*i+j for i in range(p['count']) for j in range(used)]
73
+ self.assertEqual(sorted(covered),expected);self.assertEqual(p['writes_outside_records'],[])
74
+ if p['trace'] is not None:self.assertEqual(len(p['trace']),p['trace_length'])
75
+
76
+ def test_paired_conversion_and_allocation_outcomes(self):
77
+ a,b=self.r['native'].values()
78
+ for name in a['loaders']:
79
+ def clean(n):return [{k:v for k,v in p.items() if k not in ['globals','trace','trace_sha256','allocation_trace','portal_allocation_trace']} for p in n['loaders'][name]['probes']]
80
+ self.assertEqual(clean(a),clean(b))
81
+
82
+ def test_byte_aliases_set_and_conditional_moves(self):
83
+ for v in [0,1,0xffffffff]:
84
+ g=graph([('cmpl','$0x1, %eax'),('setb','%r11b'),('setne','%dl'),('cmovbl','%edi, %esi'),('nop','')])
85
+ t=evaluate(g,0,[4],{}, {'%rax':v,'%r11':0x1200,'%rdx':0x3400,'%rdi':777,'%rsi':888},capture_registers=True)
86
+ self.assertEqual(t['registers']['r11'],0x1200+int(v<1));self.assertEqual(t['registers']['rdx'],0x3400+int(v!=1));self.assertEqual(t['registers']['rsi'],777 if v<1 else 888)
87
+ with self.assertRaises(ValueError):evaluate(graph([('cmovbl','%edi, %esi')]),0,[1],{}, {'%rdi':1,'%rsi':2})
88
+
89
+ def test_self_xor_and_signed_greater_equal(self):
90
+ g=graph([('xorl','%r8d, %r8d'),('movb','$0x7, %r8b'),('testl','%esi, %esi'),('jge','0x5'),('movl','$0x9, %r8d'),('nop','')])
91
+ for v in [0,1,0x80000000,0xffffffff]:
92
+ t=evaluate(g,0,[5],{}, {'%rsi':v},capture_registers=True)
93
+ self.assertEqual(t['registers']['r8'],7 if v<2**31 else 9)
94
+ with self.assertRaises(ValueError):evaluate(graph([('xorl','%eax, %edi')]),0,[1],{}, {})
95
+
96
+
97
+ if __name__=='__main__':unittest.main()