1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
# rsdos Debug Notes — heap/cells corruption family (v0.5.7)
Handoff state: **HEAD = `ab633ce`**; the mystery below is SOLVED and the
verify rework is committed on top (see git log).
## ✅ ROOT CAUSE + FIX (this session) — ExitBootServices retry wipes the free list
**Root cause (confirmed by QEMU simulation + OptiPlex screen markers):**
1. `capture_memory_map` (main.rs, pre-EBS) → `init_heap` builds the free
list from the raw map; `map_key` stashed.
2. `gop::init` → `SetMode` — on real hardware SetMode may allocate/reallocate
memory, making `map_key` stale — then allocates the live display shadow
(first heap alloc, `mem::forget`'d) and `vt::init` builds the cells grids.
3. `ExitBootServices(image_handle, map_key)` **fails** (stale key) → retry
`refresh_and_exit` → `capture_memory_map` → **`init_heap` REBUILT the
free list from the raw map, wiping every pre-EBS live allocation's
state** — the shadow's 1.8 MB span and the vt grids became FREE while
still live.
4. Post-EBS allocations carved **inside the live shadow** (the Opt's vt
marker String at exactly `heap+0x443e0`, the family-A crash ptr); the
display renders into the shadow → clobbers those objects → **zeroed
back-pointers** (family A `ptr=0x404443e0 block=0x0`) and **gray text
pixels** (family B `0xaaaaaa00aaaaaa` = 2× `0x00AAAAAA` = PALETTE[7]
fg-7, NOT a firmware scrub).
**Fix (committed):** `refresh_and_exit` now re-captures the map and returns
a FRESH EBS key WITHOUT rebuilding the free list (`refresh_map_key`, the
free list is the authoritative live-allocation state). Verified in QEMU
with a forced EBS failure: `verify: ok`, `probe free=false`, `flhead
in_shadow=false` — identical to the no-failure boot. Test hook:
`memory::SIMULATE_EBS_FAILURE` (one-shot, defaults off).
**OptiPlex markers that exposed it:** `probe@+0x443e0 free=YES`,
`w0=203a7476 w1=78302063` (= "vt: c 0x" — the freed marker String),
`blk=0x404443c0 sz=0x770`, `flhead=0x404443c0 in-shadow=YES`, `shadow
=0x40400000 freed=no`, `fb=0xe0000000 in-heap=no`.
## TL;DR
- Real-hardware (OptiPlex, no serial) boots crash in the shell with heap
corruption. Two **distinct** bug families:
- **(A) scroll-crash family**: in-heap `ptr=0x404443e0` with zeroed header.
- **(B) 0xAA family**: `0xaaaaaa00aaaaaa` = two `0x00AAAAAA` u32s.
- **CORRECTION (this session): `0x00AAAAAA` is NOT a UEFI firmware scrub.
It is the VGA GRAY text colour: `rgb(170,170,170)` packs to
`(170<<16)|(170<<8)|170 = 0x00AAAAAA`, which is `PALETTE[7]` = fg 7 — the
vt's default foreground.** Every rendered glyph pixel is a `0x00AAAAAA`
word in the shadow; the shadow's background is `0x00000000` (bg 0).
- Family B `0xaaaaaa00aaaaaa` = **two adjacent gray TEXT pixels** — the
memory read as heap held rendered text ⇒ it was **inside the live
shadow** (see root cause above).
- Family A zeros = the shadow's **black background** at the same span.
- ⇒ BOTH families = the allocator handed out memory inside the live
shadow after the EBS-retry free-list rebuild.
- The pre-verify `0x70020` "mystery" was a debug artifact, not corruption.
`BLANK = {ch:32, fg:7, bg:0}` = raw `0x70020`, and `vec![BLANK; rows*cols]`
fills the whole grid with it at init. QEMU proves both grids are 100%
BLANK at shell entry. (Note: 0x70020 ≠ 0x00AAAAAA — the two "fills" are
cleanly distinguishable; the family-A/B dump content is TEXT/bg, not
BLANK cells.)
## The two confirmed bugs
1. **(A) scroll-crash family** — heap allocator returns an in-heap block whose
header is zeroed (`ptr=0x404443e0`). Dropping it panics in the guard.
2. **(B) 0xAA family** — freed garbage pointer `0xaaaaaa00aaaaaa`; the two
`0x00AAAAAA` words are **gray text pixels** (PALETTE[7]) — the memory at
that address was rendered shadow content. Something handed the allocator a
pointer that was never a live allocation (or a live-shadow block that
leaked into the free list).
## What we've fixed / proven already
- **IST=1 stack collision → ROOT CAUSE of ACTIVE_VOL corruption**: vec 14 ran on
`PF_STACK`, a 4 KiB `.bss` static overlapping FAT `ACTIVE_VOL`. Fixed in
`gdt.rs`: heap-allocated 32 KiB IST stacks. (Done, earlier.)
- **AHCI/DMA EXONERATED**: user removed all disks; crashes persisted.
- **All QEMU repro attempts were clean** (mem/klog/tail-crash) until the content
probe upgrade — which tripped the *content check only* on a healthy boot for
the reason above (expectation bug + break bug). Fixed: the check now expects
`blank_raw` (0x70020), records all four probe values, and adds a full-grid
nz/blank summary line. QEMU now reports `verify: ok` on every boot.
- **Clean heap geometry (OptiPlex)**: heap = `0x40400000..0xc7e34000` (~2.2 GB),
boot stack ~1 MiB above heap end.
- **Shadow geometry (QEMU)**: shadow = first alloc at `heap+0x20` (`0x1980020`),
480000 words → span `[heap+0x20, heap+0x1D4C20)`; cells data at
`heap+0x1D4C40`; VecDatas at ~`heap+0x1D8630`.
- **Shadow-rejection hypothesis (strengthened by QEMU forced-reject test)**:
on the OptiPlex the shadow alloc may have been rejected (or its block
freed), leaving that span free + text/zero-filled. `0x40444970 =
heap+0x44970` is *inside* the would-be shadow span. Forcing rejection in
QEMU reproduces family A exactly: `probe heap+0x443e0 IN free blk` with
all-zero content.
- **Original #GP resolved**: crashed image was RELEASE; drop of `Parsed` struct;
cause = real memory corruption (laytest excluded compiler bug).
- **Stale PDB trap**: fresh PDB re-resolved shell-entry #GP →
`rsnix::vt::put_byte+0x16a` (`mov %ax,(%rdx,%rcx,4)`) — u16 char at 4-byte
stride into cells buffer via garbage ptr from `r9=0x405d8630`. bt chain:
`shell::run → GopWriter::write_str → fmt::Formatter::pad_integral →
klog::LineBuf::write_str → core::fmt::write → Display(u32)`.
- **Crash decoded**: the dropped object in the last boot crash was a 126-byte
String (`cap 0x7e`) = the vt verify klog line (19-char prefix + 106-char text +
`\n`); `ptr=0x40444970` is STALE (inside the would-be shadow span, back-pointer
never written).
## Allocator layout (verified, `memory.rs`)
- `HEADER_SIZE=16`, `MINI_SIZE=8`, `MIN_BLOCK=32`, `MIN_ALIGN=16`
- payload = `align_up(block+24, align)`
- split: rewrites rem header + `free_list_insert` + back-pointer at `payload-8`
- `back_pointer_ok(ptr)` validates `[ptr-8]`; guard v0.5.7 (`0187b80`) reports +
halts on failure; `corrupt` QEMU hook verified.
## vt grid facts
- `put_cell(r,c,ch)` sets `ch` + fg/bg from statics `SGR_FG=7`/`SGR_BG=0`.
- Cell raw encoding: `ch | fg<<16 | bg<<24`; BLANK = 0; cursor-inverse blank =
`0x07000000` (bg and fg swapped). **`0x70020` is NOT a cursor-inverse cell** —
it's a literal space with fg=7 (ch=0x20, fg=7, bg=0).
- `flush()` diffs cells vs rendered; cursor pass writes inverse colors into both
FB and `rendered`.
- `term_println(s)` = `gop_print!` + blank line + explicit `serial::write_line`.
Display mirror to serial is NOT automatic for `gop_println!`.
- Boot order after vt init: shadow → boot markers → memory self-test → page
allocator + self-tests → paging adopt + self-test → sched init →
rtc/pci/ahci/fat/fs → **cursor spawn** → **`debug_verify()`** → `shell::run()`
(first banner `rs-dos v0.5.7 …`, then `=====`, then crash-log read).
## Current committed work (post-`ab633ce`)
- `gop::boot_marker_at(msg, row)` + `gop::text_rows()` + `gop::bottom_row(off)`
— shadow marker now on the LAST text row, vt verify marker one above it
(both survive the banner, prompt, and crash.log replay).
- `debug_verify()` fixed: compares probes against `blank_raw` (0x70020), not
zero; records all four probe values (no early break); probes BOTH cells and
rendered; adds a full-grid `grids c nz=… blank=… r nz=… blank=…` summary
(serial). Marker format:
`vt: c {ptr:#x} cap={cap:#x} ok|GARBAGE | r {ptr:#x} ok|GARBAGE` at
`bottom_row(1)`.
- QEMU verified: `verify: ok` every boot; screendump shows the banner (gray),
prompt, and both bottom markers. test-msg + test-fault PASS; debug and
release builds 0 warnings.
## QEMU reproduction of the content-check failure (RESOLVED)
- **0x00AAAAAA scan verdict (this session)**: `shadow scan: 11333w/480000`
hits are all at **glyph positions of on-screen text** (fg=7 gray pixels).
The "scrub" reading at the crash offset (`probe mem: 00000000 00aaaaaa
00aaaaaa 00000000 …`) is the heapdbg output line **rendered at that exact
shadow location** — the probe reads its own text. The shadow far rows
(r300, r500) read 0 ⇒ init zeroing is correct and nothing over-writes
untouched shadow rows in QEMU. FB pixel at (171,1) = (170,170,170) GRAY
= 0x00AAAAAA — shadow and FB agree; there is NO shadow/FB desync.
- The shadow scan is still a useful health signal: untouched far rows must
stay 0; text-row hits are expected (that's what the shadow holds).
- Serial showed `probes c=[0x70020,0x0,0x0,0x0] r=[0x70020,0x0,0x0,0x0]` — this
was **misread** as "a single stray cell". The full-grid dump (3700 reads)
showed `nz=3700 blank=3700` in both grids: **every cell is BLANK (0x70020),
the grids are healthy**. The probe array's 1–3 slots were zero-initialized
leftovers from the early-`break` bug.
- Memory dump proves grids are fully healthy on QEMU: cells block base
`0x1b54c20` `{size=0x39f0, free=0}`, back-ptr written; rendered block base
`0x1b58650`; both grids contain the shell banner post-draw. **No heap
corruption on QEMU.**
- QEMU verify geometry: cells ptr `0x1b54c40` cap `0xe74` len `0xe74`; rendered
ptr `0x1b58670`.
- ⇒ `CONTENT-GARBAGE` was never evidence of OptiPlex corruption — the check was
comparing against the wrong expectation. Now: probes compare against
`blank_raw` (0x70020); every probe value is logged; a `grids c nz=… blank=…`
summary is the definitive health signal.
## The pre-verify `0x70020` at grid(0,0) — MYSTERY SOLVED
**There was no writer.** `0x70020` is the grid's own init fill:
`BLANK = Cell{ch:32, fg:7, bg:0}` encodes as `0x70020`, and `vt::init` builds
both grids with `vec![BLANK; rows*cols]`. The old content check expected
all-zero cells (wrong), and the probe loop's early `break` made probes 1–3
look like zeros. Nothing writes a pre-banner space; the shell banner (gray
VGA-7 text, rows 0–1) and the `>` prompt (row 2) are the only vt writes
before the markers.
Also learned while verifying with a screendump:
- The shell banner occupies grid rows **0 and 1** (motd + `=====` line), and
the prompt sits at row 2 — so a marker at FB row 1 (the old shadow marker)
is **clobbered** by the banner, and row 2's first glyphs get eaten by the
prompt + cursor. Markers whose values matter now live at the **bottom**
(`gop::bottom_row(0)` = last row, `bottom_row(1)` = second-to-last), which
also survive a stale crash.log replay (~20 lines at grid rows 2+).
- Banner text renders VGA gray `0xAAAAAA` (fg 7), boot markers white `0xFFFFFF`
— a white-only pixel matcher misses the banner. QEMU screendump decode of
the bottom rows: `vt: c 0x1b54c40 cap=0xe74 ok | r 0x1b58670 ok` and
`gop: shadow 0x1980020 1.83 MiB`.
**Excluded candidates (all still true, now moot):** cursor task (FB/shadow
only), flush cursor-inverse pass (0x07000000 ≠ 0x70020), boot markers (direct
FB), memory self-test, shell banner (runs after verify), backspace (BLANK).
## OptiPlex flash — what to look for (the linchpin)
**FIRST OPTIPLEX REPORT (this session, previous image):**
```
heapdbg: probe@+0x443e0 free=YES w0=203a7476 w1=78302063
heapdbg: shadow=0x40400000 freed=no
```
- `w0=0x203a7476 w1=0x78302063` = LE ASCII **"vt: c 0x"** — the leftover
bytes of the *vt verify marker String* (`debug_verify`'s
`alloc::format!("vt: c {:#x} …")`), which was **allocated at exactly
`heap+0x443e0`** (the historical family-A crash ptr) and freed before
the marker ran. ⇒ on the Opt the heap's first-fit hands out
**`heap+0x443e0` — INSIDE the live shadow span
[`heap+0x20`, `heap+0x1D4C20`)** — while `shadow=0x40400000 freed=no`
(the shadow Vec's block header claims allocated, forget'd).
- QEMU control: same marker prints `probe free=false … flhead=0x1b706d0
in_shadow=false` — the probe address is NOT in the free list.
- ⇒ The free list contains the live shadow span on the Opt. The two new
boot lines (`fb … in-heap=YES/no`, `flhead … in-shadow=YES/no`) plus
the probe's containing `blk=…` tell us whether the overlap is the whole
heap block (shadow carve never happened / init_heap after alloc), the
fb inside the heap (display clobbers), or a mid-span block.
New image puts on screen (markers are **bottom-anchored** now — they survive
the banner rows 0–1, the prompt row 2, AND a stale crash.log replay at rows
2+; use `gop::bottom_row(off)` to place any future ones):
1. **Last row (bottom_row(0))**: `heapdbg: probe@+0x443e0 free=YES|no w0=… w1=… blk={base} sz={size}`
— crash offset free? content (zeros=shadow bg, `00aaaaaa`=gray text,
ASCII=leftover string bytes)? and the **base/size of the free block
containing it** (0x40400000 = whole-heap block → the shadow carve
never happened; inside the shadow span → overlap; a normal small
block → ordinary freed allocation).
2. **Second-to-last row (bottom_row(1))**: `heapdbg: shadow={ptr| -} freed=YES|no`
— the shadow block base + header free flag.
3. **Third-to-last (bottom_row(2))**: `heapdbg: fb={base} size={size} in-heap=YES|no`
— is the framebuffer inside the heap range (display clobbers heap)?
4. **Fourth-to-last (bottom_row(3))**: `heapdbg: flhead={base} size={size} in-shadow=YES|no`
— does the free-list head overlap the shadow span?
5. Also: `vt: c {ptr} cap=… ok|GARBAGE | r {ptr} ok` marker (was
bottom_row(1), now overwritten by the shadow line) — the cells ptr
value is the linchpin for a stale-VecData theory: equals `0x40444970`?
6. If it crashes: the full guard band (`hdr:` + all four `mem:` lines, 16
stack words) plus the bottom markers if still visible.
Serial-side (QEMU only): the boot heapdbg values are mirrored to serial
(`heapdbg: probe free=… shadow=… freed=… fb_in_heap=… flhead=… in_shadow=…`)
and captured in the klog ring — a healthy OptiPlex boot can also dump them
with the `klog` shell command.
## Build/flash/test loop (traps)
- Build 0 warnings, edition 2024, `no_std`, `x86_64-unknown-uefi`, workspace
v0.5.7. Zero new deps.
- Real-hardware boot chain: rsboot at `EFI/BOOT/BOOTX64.EFI` → kernel at
`bzImage`. QEMU dev loop boots kernel as BOOTX64.EFI.
- CRASHLOG stick must be FAT32; `mdir` grep gotcha: match `crash`
case-insensitively.
- **Stale qemu holds flock** → `pkill -9 -f 'qemu[-]system'` in a SEPARATE
command; `rm -f esp/NvVars` between boots.
- Use repo `scripts/` infra: test-msg/test-fault/test-mouse; monitor socket; HMP
`mouse_move`.
- Fault-context serial rule: never `serial_println!` in ISR/fault code; use
`serial::write_raw`; guard report must not allocate (stack `FmtBuf`).
- Marker on row 0 gets overwritten by the shell MOTD — use `boot_marker_at(msg, row>0)`.
- Stale PDBs misname symbols — re-resolve with a fresh PDB per build (PDB math
is decimal); `r9` = struct base with Vec layout `{ptr@0, cap@8, len@16}`.
## Next steps (priority order)
1. **Flash the fixed image to the OptiPlex** (BOOTX64.EFI in `esp/`).
Expected bottom rows now: `probe@+0x443e0 free=no` (was YES),
`flhead=… in-shadow=no` (was YES), `vt: … ok`. Run the old repro
(`klog -t 2`, `ls`, `help`) — should no longer crash.
2. If the Opt still misbehaves: relay the four bottom lines again — the
fix only holds if the free list no longer overlaps the shadow.
3. Reattach the window-manager milestone once confirmed.
(QEMU control arm is green with and without a forced EBS failure:
`verify: ok`, probe NOT free, shadow base NOT in free list, full shell
sequence clean. `0x00AAAAAA` = gray text colour; shadow far rows stay 0.)
|