verify the annotation against what it annotates
ISHIKAWA: three silent miscompilations found the same day shared one shape.
method type tracked by per-function name sets, fed from annotations
machine el_val_t erases everything at the C boundary
material no propagation through expressions
measurement nothing verifies an annotation against what it annotates
root cause El has type ANNOTATIONS and no type CHECKING. The annotation
feeds dispatch and is never itself verified.
MEASURED, and it is not merely a wrong answer
let x: Int = "hello" ; x + 1 -> printed 4343631981, a string POINTER
interpreted as an integer
let s: String = 42 ; println -> dereferenced address 42
The first leaks a raw memory address into program output. The second is an
arbitrary-read primitive if the integer is ever attacker-influenced.
PREDICTIONS AND RESULTS
P1 let x: Int = "hello" compiles clean TRUE
P2 let s: String = 42 compiles clean TRUE
P3 the annotation drives dispatch, unverified TRUE
P4 same root cause as all three bugs found today TRUE
P5 checking literal-vs-annotation catches both TRUE
P6 zero false positives across the compiler's source TRUE
The emitter only RECORDS the mismatch; tools/check/annotations.sh decides,
consistent with every other check landed today.
INCOMPLETE, stated rather than hidden: only literals are checked.
let x: Int = some_string_fn() still passes, because signatures.rel carries
Int/Instant/Duration and no String entries. That is a DATA gap, not a capability
limit -- every El function declares its return type in source and codegen
already holds ret_type on every FnDef.
105/105 native, 5/5 annotation_query.sh, fixpoint ok.
This commit is contained in:
@@ -1526,6 +1526,37 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if str_eq(ltype, "Int") {
|
||||
add_int_name(name)
|
||||
}
|
||||
// ANNOTATION CHECKING. El has type annotations and, until now, no type
|
||||
// checking: the annotation fed dispatch and was never itself verified.
|
||||
// That is the root cause of every silent miscompilation found on
|
||||
// 2026-08-17, and the failure is not merely a wrong answer:
|
||||
//
|
||||
// let x: Int = "hello" ; x + 1 -> printed 4343631981, a string
|
||||
// POINTER interpreted as an integer
|
||||
// let s: String = 42 ; print -> dereferenced address 42
|
||||
//
|
||||
// The second is an arbitrary-read primitive if the integer is ever
|
||||
// attacker-influenced. Literals are checked here because they are
|
||||
// unambiguous; the emitter only RECORDS the mismatch, the query decides.
|
||||
let vkind: String = val["expr"]
|
||||
if str_eq(ltype, "Int") {
|
||||
if str_eq(vkind, "Str") {
|
||||
record_call(state_get("__cg_current_fn"), "typemismatch:Int:String:" + name)
|
||||
}
|
||||
}
|
||||
if str_eq(ltype, "String") {
|
||||
if str_eq(vkind, "Int") {
|
||||
record_call(state_get("__cg_current_fn"), "typemismatch:String:Int:" + name)
|
||||
}
|
||||
if str_eq(vkind, "Float") {
|
||||
record_call(state_get("__cg_current_fn"), "typemismatch:String:Float:" + name)
|
||||
}
|
||||
}
|
||||
if str_eq(ltype, "Float") {
|
||||
if str_eq(vkind, "Str") {
|
||||
record_call(state_get("__cg_current_fn"), "typemismatch:Float:String:" + name)
|
||||
}
|
||||
}
|
||||
// UNANNOTATED let: take the type from what the initialiser RETURNS.
|
||||
// Without this, `let a = str_len(s)` loses the Int and a later `a + b`
|
||||
// lowers to el_str_concat on two integers -- silently, with no error,
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# annotation_query.sh — a declared type must match what it annotates.
|
||||
#
|
||||
# El had annotations and no checking: the annotation fed dispatch and was never
|
||||
# verified against the value, so a mismatch did not fail, it REINTERPRETED
|
||||
# MEMORY. let x: Int = "hello" printed 4343631981 (a string pointer used as an
|
||||
# integer); let s: String = 42 dereferenced address 42.
|
||||
set -uo pipefail
|
||||
ELC="${1:?usage: annotation_query.sh <elc>}"
|
||||
LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0
|
||||
chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; }
|
||||
cd "$LANG_DIR"
|
||||
|
||||
printf 'fn main() { let x: Int = "hello" println("x") }\n' > "$W/a.el"
|
||||
EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/a.el" >/dev/null 2>&1
|
||||
out=$(./tools/check/annotations.sh "$W/r.txt" 2>&1); rc=$?
|
||||
chk "Int annotated on a String literal is caught" "1" "$rc"
|
||||
chk "and names the variable" "1" "$(echo "$out" | grep -c "'x' is declared Int")"
|
||||
|
||||
printf 'fn main() { let s: String = 42 println(s) }\n' > "$W/b.el"
|
||||
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/b.el" >/dev/null 2>&1
|
||||
./tools/check/annotations.sh "$W/r2.txt" >/dev/null 2>&1
|
||||
chk "String annotated on an Int literal is caught" "1" "$?"
|
||||
|
||||
printf 'fn main() { let n: Int = 42 let s: String = "ok" println(s + int_to_str(n)) }\n' > "$W/c.el"
|
||||
EL_RELATIONS_OUT="$W/r3.txt" "$ELC" "$W/c.el" >/dev/null 2>&1
|
||||
./tools/check/annotations.sh "$W/r3.txt" >/dev/null 2>&1
|
||||
chk "correct annotations are clean" "0" "$?"
|
||||
|
||||
EL_RELATIONS_OUT="$W/r4.txt" "$ELC" elc-cli.el >/dev/null 2>&1
|
||||
./tools/check/annotations.sh "$W/r4.txt" >/dev/null 2>&1
|
||||
chk "the compiler's own source is clean — no false positives" "0" "$?"
|
||||
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# annotations.sh — verify that a declared type matches what it annotates.
|
||||
#
|
||||
# El had annotations and no checking. The annotation fed dispatch (deciding
|
||||
# whether `a + b` is arithmetic or concatenation) and was never verified against
|
||||
# the value, so a mismatch did not fail -- it reinterpreted memory:
|
||||
#
|
||||
# let x: Int = "hello" a string pointer used as an integer
|
||||
# let s: String = 42 address 42 dereferenced as a string
|
||||
#
|
||||
# The second is an arbitrary-read primitive if the integer is influenced.
|
||||
set -uo pipefail
|
||||
REL="${1:?usage: annotations.sh <relations-file>}"
|
||||
[ -f "$REL" ] || exit 0
|
||||
locate() {
|
||||
awk -v L="$1" '$2=="spans" && $3<=L && $4>=L {printf "%s:%d", $1, L-$3+1; exit}' "$REL" 2>/dev/null
|
||||
}
|
||||
V=0
|
||||
while read -r caller _ rest; do
|
||||
[ "${rest#typemismatch:}" = "$rest" ] && continue
|
||||
body="${rest#typemismatch:}"
|
||||
declared="${body%%:*}"; body="${body#*:}"
|
||||
actual="${body%%:*}"; var="${body#*:}"
|
||||
printf "type error in %s: '%s' is declared %s but assigned a %s literal — the annotation drives dispatch, so the value will be reinterpreted rather than rejected\n" \
|
||||
"$caller" "$var" "$declared" "$actual"
|
||||
V=$((V+1))
|
||||
done < <(sort -u "$REL")
|
||||
[ "$V" -eq 0 ] && echo "annotations: clean"
|
||||
exit "$V"
|
||||
Reference in New Issue
Block a user