@@ -86,6 +86,15 @@ hex_ge() { awk -v a="$1" -v b="$2" 'BEGIN { exit !(a >= b) }'; }
8686# hex_in_range ADDR LO HI → return 0 (true) if LO <= ADDR <= HI
8787hex_in_range () { hex_ge " $1 " " $2 " && hex_le " $1 " " $3 " ; }
8888
89+ # covered_by_merged RLO RHI (merged disjoint runs "lo hi\n..." on stdin)
90+ # → return 0 (true) if some single run contains [RLO, RHI]. Inputs are decimal
91+ # integers; addresses fit in awk's exact-integer range (< 2^53). Because the
92+ # runs are merged-maximal and disjoint, [RLO,RHI] lies in the union iff one run
93+ # contains it — a span crossing two runs has a real gap between them.
94+ covered_by_merged () {
95+ awk -v rlo=" $1 " -v rhi=" $2 " ' $1 <= rlo && $2 >= rhi { f = 1 } END { exit !f }'
96+ }
97+
8998# Extract the value of a `key=value` token from a tagged line.
9099# Usage: get_field LINE KEY → echoes VALUE (without 0x), empty if absent.
91100get_field () {
@@ -235,6 +244,40 @@ if [ -n "$iomem_ram" ]; then
235244 truth_ram_hi=$( hex16 " $( printf ' %s' " $last_ram " | sed ' s/^ *//' | cut -d' -' -f2 | cut -d' ' -f1 | tr -d ' ' ) " )
236245fi
237246
247+ # Hotplug memory-block size. sysfs_memory_blocks reports RAM at this
248+ # granularity, so its highest online block legitimately rounds the RAM top up to
249+ # the next block boundary — which sits above the fine-grained /proc/iomem top.
250+ # block_ram_ceiling is that boundary; a P ram address between truth_ram_hi and
251+ # it is block-rounded (sound), anything beyond is a genuine out-of-RAM leak.
252+ block_ram_ceiling=" "
253+ if [ -n " $truth_ram_hi " ]; then
254+ _bs=$( tr -d ' \n' < /sys/devices/system/memory/block_size_bytes 2> /dev/null || true)
255+ if [ -n " $_bs " ] && [ " 0x$_bs " != " 0x0" ]; then
256+ _bc=$(( ( (0 x$truth_ram_hi / 0 x$_bs ) + 1 ) * 0 x$_bs - 1 ))
257+ block_ram_ceiling=$( hex16 " $( printf ' %x' " $_bc " ) " )
258+ fi
259+ fi
260+
261+ # -------------------------------------------------------------------------
262+ # Scalar-fact ground truth (for the S <fact> validators below). Each source is
263+ # root-cheap and arch-portable: absent fields leave the truth empty, and the
264+ # validator SKIPs rather than fails. These facts now drive the inference
265+ # (phys_addr_bits -> phys_bits_ceiling, phys_memtotal -> ceiling, virt_addr_bits
266+ # / page_size -> paging level), so a wrong leak silently mis-bounds the window.
267+ # -------------------------------------------------------------------------
268+ # CPU address sizes (x86: "address sizes : N bits physical, M bits virtual").
269+ gt_phys_bits=" "
270+ gt_virt_bits=" "
271+ _as=$( grep -m1 ' address sizes' /proc/cpuinfo 2> /dev/null || true)
272+ if [ -n " $_as " ]; then
273+ gt_phys_bits=$( printf ' %s' " $_as " | sed -n ' s/.*: *\([0-9]\{1,\}\) bits physical.*/\1/p' )
274+ gt_virt_bits=$( printf ' %s' " $_as " | sed -n ' s/.*physical, *\([0-9]\{1,\}\) bits virtual.*/\1/p' )
275+ fi
276+ # Page size (universal).
277+ gt_page_size=$( getconf PAGESIZE 2> /dev/null || getconf PAGE_SIZE 2> /dev/null || true)
278+ # EFI presence.
279+ if [ -d /sys/firmware/efi ]; then gt_efi=1; else gt_efi=0; fi
280+
238281# -------------------------------------------------------------------------
239282# Print ground truth
240283# -------------------------------------------------------------------------
@@ -291,16 +334,113 @@ printf '%s\n' "$tagged_lines" | while IFS= read -r line; do
291334 # ceiling is a reliable plausibility bound for image_size.
292335 if [ " $type " = " S" ]; then
293336 value=$( get_field " $line " value)
337+ if [ -z " $value " ]; then
338+ skip " S $region_name — no value field"
339+ continue
340+ fi
341+ # Guard the arithmetic below: a malformed value (e.g. "0xZZ") would make
342+ # $(( value )) error and, under `set -e`, abort the whole validation. The
343+ # wire is hex per api.h; reject anything else rather than die mid-run.
344+ if ! printf ' %s' " $value " | grep -qiE ' ^0x[0-9a-f]+$|^[0-9]+$' ; then
345+ skip " S $region_name =$value — malformed value (not hex)"
346+ continue
347+ fi
294348 case " $region_name " in
295- image_size)
296- if [ -z " $value " ] || [ " $value " = " 0x0" ] || [ " $value " = " 0" ]; then
297- skip " S image_size — zero/absent"
349+ image_size|init_size)
350+ # A kernel image (or its init region) cannot exceed the RAM it lives in.
351+ if [ " $value " = " 0x0" ] || [ " $value " = " 0" ]; then
352+ skip " S $region_name — zero"
298353 elif [ -z " $truth_ram_hi " ]; then
299- skip " S image_size =$value — no /proc/iomem System RAM ground truth"
354+ skip " S $region_name =$value — no /proc/iomem System RAM ground truth"
300355 elif hex_le " $( hex16 " $value " ) " " $truth_ram_hi " ; then
301- pass " S image_size=$value — within RAM (top 0x$truth_ram_hi )"
356+ pass " S $region_name =$value — within RAM (top 0x$truth_ram_hi )"
357+ else
358+ fail " S $region_name =$value — exceeds all RAM (top 0x$truth_ram_hi ); bogus"
359+ fi
360+ ;;
361+ phys_addr_bits)
362+ if [ -z " $gt_phys_bits " ]; then
363+ skip " S phys_addr_bits=$value — no cpuinfo 'address sizes' ground truth"
364+ elif [ " $(( value )) " = " $gt_phys_bits " ]; then
365+ pass " S phys_addr_bits=$value — matches cpuinfo ($gt_phys_bits bits physical)"
366+ else
367+ fail " S phys_addr_bits=$value ($(( value )) ) — cpuinfo says $gt_phys_bits bits physical"
368+ fi
369+ ;;
370+ virt_addr_bits)
371+ if [ -z " $gt_virt_bits " ]; then
372+ skip " S virt_addr_bits=$value — no cpuinfo 'address sizes' ground truth"
373+ elif [ " $(( value )) " = " $gt_virt_bits " ]; then
374+ pass " S virt_addr_bits=$value — matches cpuinfo ($gt_virt_bits bits virtual)"
375+ else
376+ fail " S virt_addr_bits=$value ($(( value )) ) — cpuinfo says $gt_virt_bits bits virtual"
377+ fi
378+ ;;
379+ page_size)
380+ if [ -z " $gt_page_size " ]; then
381+ skip " S page_size=$value — getconf unavailable"
382+ elif [ " $(( value )) " = " $gt_page_size " ]; then
383+ pass " S page_size=$value — matches getconf PAGESIZE ($gt_page_size )"
384+ else
385+ fail " S page_size=$value ($(( value )) ) — getconf PAGESIZE is $gt_page_size "
386+ fi
387+ ;;
388+ efi_present)
389+ if [ " $(( value )) " = " $gt_efi " ]; then
390+ pass " S efi_present=$value — matches /sys/firmware/efi presence ($gt_efi )"
391+ else
392+ fail " S efi_present=$value — /sys/firmware/efi presence is $gt_efi "
393+ fi
394+ ;;
395+ phys_max_pfn)
396+ if [ -z " $truth_ram_hi " ]; then
397+ skip " S phys_max_pfn=$value — no /proc/iomem System RAM"
398+ else
399+ _pfn_ram=$(( (0 x$truth_ram_hi + 1 ) >> 12 ))
400+ if [ " $(( value )) " = " $_pfn_ram " ]; then
401+ pass " S phys_max_pfn=$value — matches RAM-top PFN"
402+ elif [ " $(( value )) " -ge " $_pfn_ram " ]; then
403+ pass " S phys_max_pfn=$value — at/above RAM-top PFN (reserved above RAM)"
404+ else
405+ fail " S phys_max_pfn=$value — below RAM-top PFN (0x$( printf ' %x' " $_pfn_ram " ) ); under-counts RAM"
406+ fi
407+ fi
408+ ;;
409+ phys_memtotal)
410+ if [ -z " $truth_ram_lo " ]; then
411+ skip " S phys_memtotal=$value — no /proc/iomem System RAM"
412+ else
413+ # Usable RAM cannot exceed the physical RAM span (a too-LOW value only
414+ # under-bounds the ceiling, which is conservative — so bound above only).
415+ _ramspan=$(( 0 x$truth_ram_hi - 0 x$truth_ram_lo + 1 ))
416+ if [ " $(( value )) " -le " $_ramspan " ]; then
417+ pass " S phys_memtotal=$value — within physical RAM span"
418+ else
419+ fail " S phys_memtotal=$value — exceeds physical RAM span (0x$( printf ' %x' " $_ramspan " ) )"
420+ fi
421+ fi
422+ ;;
423+ physical_start)
424+ # The real phys base sits at physical_start + KASLR offset, so the
425+ # configured minimum start can never exceed the real base.
426+ if [ -z " $truth_ptext_lo " ]; then
427+ skip " S physical_start=$value — no /proc/iomem Kernel code base"
428+ elif hex_le " $( hex16 " $value " ) " " $truth_ptext_lo " ; then
429+ pass " S physical_start=$value — at/below real phys base (0x$truth_ptext_lo )"
302430 else
303- fail " S image_size=$value — exceeds all RAM (top 0x$truth_ram_hi ); bogus"
431+ fail " S physical_start=$value — above real phys base (0x$truth_ptext_lo )"
432+ fi
433+ ;;
434+ phys_kernel_align)
435+ # The real phys base must be a multiple of the KASLR physical alignment.
436+ if [ -z " $truth_ptext_lo " ]; then
437+ skip " S phys_kernel_align=$value — no /proc/iomem Kernel code base"
438+ elif [ " $(( value )) " -eq 0 ]; then
439+ skip " S phys_kernel_align=$value — zero"
440+ elif [ " $(( 0 x$truth_ptext_lo % value )) " -eq 0 ]; then
441+ pass " S phys_kernel_align=$value — real phys base is $value -aligned"
442+ else
443+ fail " S phys_kernel_align=$value — real phys base 0x$truth_ptext_lo not $value -aligned"
304444 fi
305445 ;;
306446 * )
@@ -487,6 +627,14 @@ printf '%s\n' "$tagged_lines" | while IFS= read -r line; do
487627 fi
488628 if hex_in_range " $addr " " $truth_ram_lo " " $truth_ram_hi " ; then
489629 pass " $desc — within System RAM"
630+ elif [ " $region " = " ram" ] && [ -n " $block_ram_ceiling " ] && \
631+ hex_ge " $addr " " $truth_ram_hi " && \
632+ hex_le " $addr " " $block_ram_ceiling " ; then
633+ # The hotplug-block map (sysfs_memory_blocks) is block-coarse: its top
634+ # online block rounds the RAM top up to the next block boundary, above
635+ # the fine /proc/iomem top. Sound — the extra slice carries no real RAM
636+ # and is never excluded — so accept up to the block boundary.
637+ pass " $desc — block-rounded RAM top (hotplug-block granularity)"
490638 else
491639 # Some regions (efi_memmap, acpi_*) may sit in reserved holes
492640 # outside the System RAM extents. Don't fail those — skip with a
@@ -540,6 +688,59 @@ phys_win=$(printf '%s\n' "$raw_input" | grep 'Inferred phys text range:' | head
540688validate_inferred " Inferred virt text window" " $truth_stext " " $virt_win "
541689validate_inferred " Inferred phys text window" " $truth_ptext_lo " " $phys_win "
542690
691+ # -------------------------------------------------------------------------
692+ # RAM-map coverage. A `pos=extent` line is one member of a COMPLETE single-source
693+ # RAM map (a covering); the gaps between a source's extents are what
694+ # ram_map_phys_exclude carves as non-RAM. So every /proc/iomem System RAM extent
695+ # MUST lie within the union of the coverings — a hole here means a covering map
696+ # is incomplete, which would carve a false non-RAM gap (the failure this whole
697+ # mechanism guards against).
698+ #
699+ # Coverage is one-directional (RAM ⊆ union): coverings may extend ABOVE real RAM
700+ # (the block-coarse hotplug map rounds up) — that only adds slack.
701+ #
702+ # The wire carries no origin, so a full `kasld -v` validates the AGGREGATE map.
703+ # To validate ONE source's completeness, pipe that component alone, e.g.:
704+ # ./build/*/components/firmware_memmap | sudo ./extra/check-results -
705+ # then every pos=extent line belongs to that single origin.
706+ # -------------------------------------------------------------------------
707+ coverings=$( printf ' %s\n' " $tagged_lines " | awk ' $1 == "P" && $2 == "ram"' \
708+ | grep ' pos=extent' || true)
709+ if [ -z " $coverings " ]; then
710+ : # no covering sources in this input — nothing to check
711+ elif [ -z " $truth_ram_lo " ]; then
712+ skip " RAM coverage — no /proc/iomem System RAM ground truth"
713+ else
714+ # Covering extents as decimal "lo hi", sorted by lo.
715+ cov_intervals=$( printf ' %s\n' " $coverings " | while IFS= read -r cl; do
716+ clo=$( get_field " $cl " lo) ; chi=$( get_field " $cl " hi) ; csz=$( get_field " $cl " sz)
717+ # get_field yields the 0x-prefixed value, so use it directly in $(( )).
718+ [ -n " $clo " ] || continue
719+ if [ -z " $chi " ] && [ -n " $csz " ]; then chi=$( printf ' 0x%x' $(( clo + csz - 1 )) ) ; fi
720+ [ -n " $chi " ] || continue
721+ printf ' %d %d\n' " $(( clo )) " " $(( chi )) "
722+ done | sort -n)
723+ # Merge into disjoint maximal runs (touching/overlapping coalesced).
724+ merged=$( printf ' %s\n' " $cov_intervals " | awk '
725+ NR == 1 { lo = $1; hi = $2; next }
726+ { if ($1 <= hi + 1) { if ($2 > hi) hi = $2 }
727+ else { print lo, hi; lo = $1; hi = $2 } }
728+ END { if (NR > 0) print lo, hi }' )
729+ # Each System RAM extent must sit inside one merged covering run.
730+ printf ' %s\n' " $iomem_ram " | while IFS= read -r row; do
731+ [ -n " $row " ] || continue
732+ range=$( printf ' %s' " $row " | sed ' s/^ *//' | awk ' { print $1 }' )
733+ rlo_h=$( printf ' %s' " $range " | cut -d- -f1)
734+ rhi_h=$( printf ' %s' " $range " | cut -d- -f2)
735+ if [ -z " $rlo_h " ] || [ -z " $rhi_h " ]; then continue ; fi
736+ if printf ' %s\n' " $merged " | covered_by_merged " $(( 0 x$rlo_h )) " " $(( 0 x$rhi_h )) " ; then
737+ pass " RAM coverage — System RAM [0x$rlo_h -0x$rhi_h ] within covering map"
738+ else
739+ fail " RAM coverage — System RAM [0x$rlo_h -0x$rhi_h ] NOT covered (covering map incomplete)"
740+ fi
741+ done
742+ fi
743+
543744# -------------------------------------------------------------------------
544745# Summary
545746# -------------------------------------------------------------------------
0 commit comments