|
| 1 | +#!/bin/sh |
| 2 | +# This file is part of KASLD - https://github.com/bcoles/kasld |
| 3 | +# |
| 4 | +# check-truncation — guard against silent 64-bit -> word narrowing on 32-bit. |
| 5 | +# |
| 6 | +# kasld's value domain is the target kernel word (kasld_addr_t == unsigned |
| 7 | +# long; see the CONTRACT in include/kasld/api.h). A value that is WIDER than |
| 8 | +# the word — an `unsigned long long`: an arch-independent bound such as 1<<50, |
| 9 | +# or a uint64_t — assigned to or passed as that word-sized domain is silently |
| 10 | +# truncated on 32-bit builds. That is the class that produced the |
| 11 | +# MAX_PLAUSIBLE_KERNEL_PHYS (1<<50) bug: correct on 64-bit, zero on 32-bit. |
| 12 | +# |
| 13 | +# The compiler flags this under -Wconversion/-Woverflow, but those are far too |
| 14 | +# noisy to enable globally. So this guard compiles each translation unit for a |
| 15 | +# 32-bit target and fails on the NARROWING SUBSET only: `long long` -> `long`. |
| 16 | +# Keep wide values in the unsigned-long-long bound layer (kasld_addr_in_*), |
| 17 | +# which never truncates. |
| 18 | +# |
| 19 | +# A file that #errors on the 32-bit target (arch-incompatible, e.g. an |
| 20 | +# x86_64-only exploit) is skipped: it never builds for 32-bit, so a |
| 21 | +# recovery-mode warning is moot. A file that DOES build 32-bit and carries an |
| 22 | +# intentional narrowing (a deliberate 64 -> 2x32 pointer split) is listed in |
| 23 | +# ALLOW. Skips cleanly when no 32-bit cross toolchain is present. |
| 24 | +# |
| 25 | +# Usage: tests/check-truncation (also run by `make test`) |
| 26 | +# Env: JOBS (default: nproc) |
| 27 | +# --- |
| 28 | +# <bcoles@gmail.com> |
| 29 | + |
| 30 | +set -u |
| 31 | +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) |
| 32 | + |
| 33 | +# Force ASCII diagnostics. GCC quotes type names with Unicode ‘ ’ under a |
| 34 | +# UTF-8 locale (e.g. CI's glibc toolchain) and ASCII ' ' under C — and the |
| 35 | +# grep below keys on the quoted type names, so without this the guard silently |
| 36 | +# passes wherever the locale is UTF-8. C locale makes the output deterministic. |
| 37 | +export LC_ALL=C |
| 38 | + |
| 39 | +# Reviewed, intentional narrowings in genuinely-32-bit translation units. |
| 40 | +# Keep alphabetical. |
| 41 | +ALLOW="proc_pid_syscall" # deliberate 64-bit register split into two 32-bit halves |
| 42 | + |
| 43 | +# Pick ONE 32-bit (ILP32, sizeof(long)==4) GCC. The narrowing this guard |
| 44 | +# catches is sizeof(long)-dependent and therefore identical across every 32-bit |
| 45 | +# arch, so a single representative compiler is sufficient — no need to sweep all |
| 46 | +# arches (the value-dependent overflow/shift classes are swept on every arch by |
| 47 | +# `make cross` / `tests/test-cross` instead). The list MUST stay 32-bit-only: a |
| 48 | +# 64-bit compiler would never see the narrowing, silently making the guard a |
| 49 | +# no-op. i686 is canonical (CI installs gcc-i686-linux-gnu); the rest are |
| 50 | +# fallbacks so any of our cross toolchains satisfies it. (clang is not listed: |
| 51 | +# its -Wconversion diagnostic spelling differs; the guard is GCC-based and skips |
| 52 | +# where no 32-bit GCC is found.) |
| 53 | +CC= |
| 54 | +for c in i686-unknown-linux-musl-gcc i686-linux-gnu-gcc i686-linux-musl-gcc \ |
| 55 | + arm-unknown-linux-musleabi-gcc armv7-unknown-linux-musleabi-gcc \ |
| 56 | + armeb-linux-musleabi-gcc arm-linux-gnueabihf-gcc arm-linux-gnueabi-gcc \ |
| 57 | + mipsel-unknown-linux-musl-gcc mips-unknown-linux-musl-gcc \ |
| 58 | + powerpc-linux-musl-gcc riscv32-linux-musl-gcc; do |
| 59 | + command -v "$c" >/dev/null 2>&1 && { |
| 60 | + CC=$c |
| 61 | + break |
| 62 | + } |
| 63 | +done |
| 64 | + |
| 65 | +if [ -t 2 ]; then |
| 66 | + RED=$(printf '\033[31m') |
| 67 | + GREEN=$(printf '\033[32m') |
| 68 | + RESET=$(printf '\033[0m') |
| 69 | +else |
| 70 | + RED= |
| 71 | + GREEN= |
| 72 | + RESET= |
| 73 | +fi |
| 74 | + |
| 75 | +if [ -z "$CC" ]; then |
| 76 | + printf '%struncation guard: SKIP%s (no 32-bit cross toolchain)\n' "$GREEN" "$RESET" |
| 77 | + exit 0 |
| 78 | +fi |
| 79 | + |
| 80 | +CFLAGS="-std=c99 -O2 -Wall -Wextra -Wconversion -I$ROOT/src -fsyntax-only" |
| 81 | +JOBS=${JOBS:-$(nproc 2>/dev/null || echo 1)} |
| 82 | + |
| 83 | +# The detection, in ONE place (used by both the scan and the self-check below): |
| 84 | +# a 64-bit UNSIGNED value narrowed to the unsigned word, in the compiler's |
| 85 | +# C-locale (ASCII-quoted) diagnostics. Reads compiler output on stdin, emits the |
| 86 | +# matching lines. The value domain is unsigned, so signed narrowings |
| 87 | +# (time_t/off_t differences — small relative values) are deliberately excluded. |
| 88 | +narrowings() { |
| 89 | + grep -E '\[-W(conversion|overflow)\]' | |
| 90 | + grep 'long long unsigned int' | grep "to 'long unsigned int'" |
| 91 | +} |
| 92 | + |
| 93 | +# Self-check: this guard greps compiler diagnostics, which are sensitive to |
| 94 | +# locale (quote style), compiler, and version — a mismatch makes it pass |
| 95 | +# SILENTLY (as it did when GCC's UTF-8 Unicode quotes slipped the grep). So |
| 96 | +# before vouching, prove detection still fires on a KNOWN narrowing. This also |
| 97 | +# rejects an accidentally-64-bit compiler, on which 1<<50 fits the word and |
| 98 | +# would not narrow at all. |
| 99 | +if ! printf 'unsigned long f(void){unsigned long n=1ull<<50;return n;}\n' | |
| 100 | + $CC $CFLAGS -x c - 2>&1 | narrowings >/dev/null; then |
| 101 | + printf '%struncation guard: SELF-CHECK FAILED%s — %s did not flag a known\n' \ |
| 102 | + "$RED" "$RESET" "$CC" >&2 |
| 103 | + printf '64-bit -> word narrowing, so detection is broken (a diagnostic-format\n' >&2 |
| 104 | + printf 'or locale change, or a non-32-bit compiler). Refusing to report OK\n' >&2 |
| 105 | + printf 'rather than pass silently. See narrowings() / LC_ALL in this script.\n' >&2 |
| 106 | + exit 1 |
| 107 | +fi |
| 108 | + |
| 109 | +RESDIR=$(mktemp -d) |
| 110 | + |
| 111 | +# Compile one TU; emit "<basename>\n<narrowing lines>" to its .res iff it |
| 112 | +# (a) builds for 32-bit (no #error) and (b) has a long-long -> word narrowing |
| 113 | +# that is not allowlisted. |
| 114 | +check_one() { |
| 115 | + f=$1 |
| 116 | + res=$2 |
| 117 | + out=$($CC $CFLAGS "$f" 2>&1) |
| 118 | + printf '%s\n' "$out" | grep -q "error:" && return # arch-incompatible on 32-bit |
| 119 | + hits=$(printf '%s\n' "$out" | narrowings) |
| 120 | + [ -n "$hits" ] || return |
| 121 | + b=$(basename "$f" .c) |
| 122 | + for a in $ALLOW; do [ "$a" = "$b" ] && return; done |
| 123 | + { |
| 124 | + printf ' %s:\n' "$b" |
| 125 | + printf '%s\n' "$hits" | sed 's/^/ /' |
| 126 | + } >"$res" |
| 127 | +} |
| 128 | + |
| 129 | +n=0 |
| 130 | +running=0 |
| 131 | +for f in "$ROOT"/src/*.c "$ROOT"/src/rules/*.c "$ROOT"/src/components/*.c \ |
| 132 | + "$ROOT"/src/render/*.c; do |
| 133 | + [ -f "$f" ] || continue |
| 134 | + n=$((n + 1)) |
| 135 | + check_one "$f" "$RESDIR/$n.res" & |
| 136 | + running=$((running + 1)) |
| 137 | + if [ "$running" -ge "$JOBS" ]; then |
| 138 | + wait |
| 139 | + running=0 |
| 140 | + fi |
| 141 | +done |
| 142 | +wait |
| 143 | + |
| 144 | +fail=0 |
| 145 | +i=1 |
| 146 | +while [ "$i" -le "$n" ]; do |
| 147 | + if [ -s "$RESDIR/$i.res" ]; then |
| 148 | + if [ "$fail" = 0 ]; then |
| 149 | + printf '%struncation guard: 64-bit -> word narrowing on 32-bit (%s):%s\n' \ |
| 150 | + "$RED" "$CC" "$RESET" >&2 |
| 151 | + fi |
| 152 | + cat "$RESDIR/$i.res" >&2 |
| 153 | + fail=1 |
| 154 | + fi |
| 155 | + i=$((i + 1)) |
| 156 | +done |
| 157 | +rm -rf "$RESDIR" |
| 158 | + |
| 159 | +if [ "$fail" = 1 ]; then |
| 160 | + printf 'A value wider than the target word is truncated on 32-bit. Keep it in the\n' >&2 |
| 161 | + printf 'unsigned-long-long bound layer (kasld_addr_in_*); or, if the narrowing is\n' >&2 |
| 162 | + printf 'intentional in a 32-bit-only TU, add it to ALLOW in tests/check-truncation.\n' >&2 |
| 163 | + exit 1 |
| 164 | +fi |
| 165 | + |
| 166 | +printf '%struncation guard: OK%s (compiled for 32-bit via %s; no silent narrowing)\n' \ |
| 167 | + "$GREEN" "$RESET" "$CC" |
0 commit comments