Skip to content

Commit 0297f97

Browse files
JasonGrossclaude
andcommitted
Model one-operand imul as a signed multiply in the assembly checker
Fixes scrutineer security finding #2514. The one-operand form of `imul r/m` (rdx:rax := rax * src, or ax := al * src for 8-bit operands) shared a single branch with `mul` in both the symbolic executor (src/Assembly/Symbolic.v, SymexNormalInstruction) and the concrete semantics (src/Assembly/WithBedrock/Semantics.v, DenoteNormalInstruction). Both computed the *unsigned* full product. On real x86-64 hardware `mul` is unsigned but one-operand `imul` is signed (Intel SDM Vol. 2A, IMUL), so the high word written to rdx (ah for 8-bit) differs whenever an operand has its top bit set; the low word agrees. For example, with rax = 0xffffffff00000001 (the top limb of the P-256 prime) and rcx = 3, `imul rcx` leaves rdx = 0xffffffffffffffff on hardware, but the model computed 0x2. Consequently the equivalence checker (`--hints-file`) could certify assembly that uses `imul rcx` where the reference computation needs the unsigned high word from `mul rcx` / `mulx`, i.e. accept non-equivalent assembly. The two- and three-operand forms of `imul` only write the low half and were already correct. The fix splits the shared branch: - `mul` keeps its previous (unsigned) behaviour unchanged. - One-operand `imul` now computes the high word as `keep s ((Z.signed s rax * Z.signed s src) >> s)` in the concrete semantics, and in the symbolic executor as `shr s (mulZ (signed rax) (signed src)) s` where `signed x` is spelled with existing operators as `addZ (add s x (2^(s-1))) (-(2^(s-1)))`, which is exactly `Z.signed s x` unfolded. The low word is still the (unsigned) `mulZ` product, which agrees with the signed one after truncation. Flags are havocked as before. The 8-bit form (ah:al) is handled by the same code. - The symbolic high-word computation lives in a new definition `SignedMulHigh` rather than inline in `SymexNormalInstruction`, with its own `same_reg_some_of_success` / `same_mem_addressed_of_success` instances in EquivalenceProofs.v (derived by typeclass resolution, like `Symeval`). Inlining it made the generic tactics of `SymexNormalInstruction_reg_same`/`_mem_same` blow up (they are exponential in the number of binds of a branch), and nesting the whole expression in a single `Symeval` made the `Qed` of `SymexNornalInstruction_R` take over an hour; with the separate definition all three files compile in about the same time as before. - SymbolicProofs.v unfolds `SignedMulHigh` alongside `SymexNormalInstruction` and gains a small case for the new branch relating the unfolded sign extension to `Z.signed`. A new test file src/Assembly/WithBedrock/SemanticsTests.v pins down the hardware values of `mul`/`imul` (64- and 8-bit, top bit set or clear) for both the concrete semantics and the symbolic executor; the imul cases fail against the previous model. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Rbn2gww3MGhvrh52fNjpD
1 parent 5691ca0 commit 0297f97

5 files changed

Lines changed: 180 additions & 3 deletions

File tree

src/Assembly/EquivalenceProofs.v

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2845,6 +2845,12 @@ Typeclasses Opaque Symeval.
28452845
#[global]
28462846
Typeclasses Transparent AddressSize OperationSize.
28472847

2848+
(* TODO: move? *)
2849+
Local Instance SignedMulHigh_reg_same {opts : symbolic_options_computed_opt} {descr:description} {sz:OperationSize} {sa:AddressSize} src1 src2 : same_reg_some_of_success (SignedMulHigh src1 src2).
2850+
Proof. cbv [SignedMulHigh]; typeclasses eauto. Qed.
2851+
#[global]
2852+
Typeclasses Opaque SignedMulHigh.
2853+
28482854
(* TODO: move? *)
28492855
Local Instance SymexNormalInstruction_reg_same {opts : symbolic_options_computed_opt} {descr:description} instr : same_reg_some_of_success (SymexNormalInstruction instr).
28502856
Proof.
@@ -3015,6 +3021,12 @@ Typeclasses Opaque Symeval.
30153021
#[global]
30163022
Typeclasses Transparent AddressSize OperationSize.
30173023

3024+
(* TODO: move? *)
3025+
Local Instance SignedMulHigh_mem_same {opts : symbolic_options_computed_opt} {descr:description} {sz:OperationSize} {sa:AddressSize} src1 src2 : same_mem_addressed_of_success (SignedMulHigh src1 src2).
3026+
Proof. cbv [SignedMulHigh]; typeclasses eauto. Qed.
3027+
#[global]
3028+
Typeclasses Opaque SignedMulHigh.
3029+
30183030
(* TODO: move? *)
30193031
Local Instance SymexNormalInstruction_mem_same {opts : symbolic_options_computed_opt} {descr:description} instr : same_mem_addressed_of_success (SymexNormalInstruction instr).
30203032
Proof.

src/Assembly/Symbolic.v

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4184,6 +4184,25 @@ Definition rcrcnt s cnt : Z :=
41844184
Z.land cnt (Z.of_N s-1).
41854185

41864186
Notation "f @ ( x , y , .. , z )" := (PreApp f (@cons pre_expr x (@cons pre_expr y .. (@cons pre_expr z nil) ..))) (at level 10) : x86symex_scope.
4187+
4188+
(** [SignedMulHigh src1 src2] is the high [s] bits of the product of the
4189+
[s]-bit operands [src1] and [src2] interpreted as signed (two's
4190+
complement) values, i.e. what the one-operand form of [imul] writes to
4191+
[rdx] (resp. [ah]). [Z.signed s x] is spelled with existing operators
4192+
as [addZ (add s x 2^(s-1)) (-(2^(s-1)))]. This is a separate definition
4193+
rather than being inlined into [SymexNormalInstruction] so that the
4194+
proofs about [SymexNormalInstruction] can treat it as a unit, the way
4195+
they treat [Symeval]. *)
4196+
Definition SignedMulHigh {opts : symbolic_options_computed_opt} {descr:description} {s : OperationSize} {sa : AddressSize} (src1 src2 : ARG) : M idx :=
4197+
(let half : Z := Z.shiftl 1 (Z.of_N s-1) in
4198+
let signed_ (x : idx) : pre_expr
4199+
:= addZ@(add s@(x, PreApp (const half) nil), PreApp (const (-half)) nil) in
4200+
a <- GetOperand src1;
4201+
b <- GetOperand src2;
4202+
xa <- Symeval (signed_ a);
4203+
xb <- Symeval (signed_ b);
4204+
p <- Symeval (mulZ@(xa,xb));
4205+
Symeval (shr s@(p, PreApp (const (Z.of_N s)) nil)))%x86symex.
41874206
Definition SymexNormalInstruction {opts : symbolic_options_computed_opt} {descr:description} (instr : NormalInstruction) : M unit :=
41884207
let stack_addr_size : AddressSize := 64%N in
41894208
let sa : AddressSize := 64%N in
@@ -4298,7 +4317,7 @@ Definition SymexNormalInstruction {opts : symbolic_options_computed_opt} {descr:
42984317
vh <- Symeval (shrZ@(v,PreARG (Z.of_N s)));
42994318
_ <- SetOperand lo v;
43004319
SetOperand hi vh
4301-
| (Syntax.mul | imul), [src2] =>
4320+
| Syntax.mul, [src2] =>
43024321
let src1 : ARG := rax in
43034322
v <- Symeval (mulZ@(src1,src2));
43044323
vh <- Symeval (shrZ@(v,PreARG (Z.of_N s)));
@@ -4309,6 +4328,22 @@ Definition SymexNormalInstruction {opts : symbolic_options_computed_opt} {descr:
43094328
_ <- SetOperand (lo:ARG) v;
43104329
_ <- SetOperand (hi:ARG) vh;
43114330
HavocFlags (* This is conservative and can be made more precise *)
4331+
| imul, [src2] =>
4332+
(* One-operand [imul] is a *signed* widening multiply (Intel SDM Vol. 2A,
4333+
IMUL): rdx:rax (ah:al for s = 8) := signed(rax) * signed(src2). The low
4334+
half agrees with the unsigned product, but the high half does not
4335+
whenever an operand has its top bit set, so it must not share the
4336+
unsigned [mul] branch above. *)
4337+
let src1 : ARG := rax in
4338+
v <- Symeval (mulZ@(src1,src2));
4339+
vh <- SignedMulHigh src1 src2;
4340+
lo <- resize_reg rax;
4341+
hi <- (if (s =? 8)%N
4342+
then ret ah
4343+
else resize_reg rdx);
4344+
_ <- SetOperand (lo:ARG) v;
4345+
_ <- SetOperand (hi:ARG) vh;
4346+
HavocFlags (* This is conservative and can be made more precise *)
43124347
| Syntax.shl, [dst; cnt] =>
43134348
let cnt := andZ@(cnt, (PreApp (const (Z.of_N s-1)%Z) nil)) in
43144349
v <- Symeval (shl s@(dst, cnt));

src/Assembly/WithBedrock/Semantics.v

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ Definition DenoteNormalInstruction (st : machine_state) (instr : NormalInstructi
245245
let v := v1 * v2 in
246246
st <- SetOperand sa s st lo v;
247247
SetOperand sa s st hi (Z.shiftr v (Z.of_N s))
248-
| (Syntax.mul | imul), [src2] =>
248+
| Syntax.mul, [src2] =>
249249
let src1 : ARG := rax in
250250
v1 <- DenoteOperand sa s st src1;
251251
v2 <- DenoteOperand sa s st src2;
@@ -257,6 +257,24 @@ Definition DenoteNormalInstruction (st : machine_state) (instr : NormalInstructi
257257
st <- SetOperand sa s st lo v;
258258
st <- SetOperand sa s st hi (Z.shiftr v (Z.of_N s));
259259
Some (HavocFlags st) (* conservative *)
260+
| imul, [src2] =>
261+
(* One-operand [imul] is a *signed* widening multiply (Intel SDM Vol. 2A,
262+
IMUL): rdx:rax (ah:al for s = 8) := signed(rax) * signed(src2). The low
263+
half agrees with the unsigned product [v1 * v2], but the high half does
264+
not whenever an operand has its top bit set, so it must not share the
265+
unsigned [mul] branch above. *)
266+
let src1 : ARG := rax in
267+
v1 <- DenoteOperand sa s st src1;
268+
v2 <- DenoteOperand sa s st src2;
269+
let v := v1 * v2 in
270+
let vs := Z.signed s v1 * Z.signed s v2 in
271+
lo <- resize_reg rax;
272+
hi <- (if (s =? 8)%N
273+
then Some ah
274+
else resize_reg rdx);
275+
st <- SetOperand sa s st lo v;
276+
st <- SetOperand sa s st hi (Z.land (Z.shiftr vs (Z.of_N s)) (Z.ones (Z.of_N s)));
277+
Some (HavocFlags st) (* conservative *)
260278
| imul, ([src1 as dst; src2] | [dst; src1; src2]) =>
261279
v1 <- DenoteOperand sa s st src1;
262280
v2 <- DenoteOperand sa s st src2;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
(** Regression tests for the semantics of one-operand [mul] and [imul].
2+
3+
The one-operand form of [imul] is a *signed* widening multiply
4+
(rdx:rax := signed(rax) * signed(src)), whereas [mul] is unsigned. The
5+
two agree on the low half of the product but not on the high half
6+
whenever an operand has its top bit set. The concrete semantics
7+
([Semantics.DenoteNormalInstruction]) and the symbolic executor
8+
([Symbolic.SymexNormalInstruction]) used to share a single unsigned
9+
branch for both mnemonics (scrutineer finding #2514); these tests pin
10+
down the hardware behaviour for both models, on the same inputs.
11+
12+
The expected values were obtained by executing the instructions on
13+
x86-64 hardware. *)
14+
From Coq Require Import ZArith.
15+
From Coq Require Import List.
16+
From Coq Require Import String.
17+
Require Import Crypto.Util.ErrorT.
18+
Require Import Crypto.Util.Tuple.
19+
Require Import Crypto.Assembly.Syntax.
20+
Require Import Crypto.Assembly.Symbolic.
21+
Require Import Crypto.Assembly.Equivalence.
22+
Require Import Crypto.Assembly.WithBedrock.Semantics.
23+
Require Import coqutil.Map.Interface.
24+
Import ListNotations.
25+
Local Open Scope Z_scope.
26+
Local Open Scope list_scope.
27+
Local Open Scope string_scope.
28+
29+
(** ** Concrete semantics *)
30+
31+
Definition initial_machine_state (rax_val rcx_val : Z) : machine_state
32+
:= {| machine_reg_state := Semantics.set_reg (Semantics.set_reg (Tuple.repeat 0 _) rax rax_val) rcx rcx_val
33+
; machine_flag_state := havoc_flags
34+
; machine_mem_state := map.empty |}.
35+
36+
Definition denote_one (instr : NormalInstruction) (rax_val rcx_val : Z) (out : REG) : option Z
37+
:= option_map (fun st : machine_state => Semantics.get_reg st out) (DenoteNormalInstruction (initial_machine_state rax_val rcx_val) instr).
38+
39+
Definition mul_rcx : NormalInstruction := {| Syntax.prefix := None ; Syntax.op := Syntax.mul ; Syntax.args := [reg rcx] |}.
40+
Definition imul_rcx : NormalInstruction := {| Syntax.prefix := None ; Syntax.op := Syntax.imul ; Syntax.args := [reg rcx] |}.
41+
Definition mul_cl : NormalInstruction := {| Syntax.prefix := None ; Syntax.op := Syntax.mul ; Syntax.args := [reg cl] |}.
42+
Definition imul_cl : NormalInstruction := {| Syntax.prefix := None ; Syntax.op := Syntax.imul ; Syntax.args := [reg cl] |}.
43+
44+
(** rax = 0xffffffff00000001 (the top limb of the P-256 prime; bit 63 set), rcx = 3 *)
45+
Example denote_mul_rcx_hi : denote_one mul_rcx 0xffffffff00000001 3 rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
46+
Example denote_mul_rcx_lo : denote_one mul_rcx 0xffffffff00000001 3 rax = Some 0xfffffffd00000003. Proof. vm_compute; reflexivity. Qed.
47+
Example denote_imul_rcx_hi : denote_one imul_rcx 0xffffffff00000001 3 rdx = Some 0xffffffffffffffff. Proof. vm_compute; reflexivity. Qed.
48+
Example denote_imul_rcx_lo : denote_one imul_rcx 0xffffffff00000001 3 rax = Some 0xfffffffd00000003. Proof. vm_compute; reflexivity. Qed.
49+
50+
(** both operands with bit 63 set: rax = -2, rcx = -2^63+1 as signed values *)
51+
Example denote_mul_rcx_hi' : denote_one mul_rcx 0xfffffffffffffffe 0x8000000000000001 rdx = Some 0x7fffffffffffffff. Proof. vm_compute; reflexivity. Qed.
52+
Example denote_imul_rcx_hi' : denote_one imul_rcx 0xfffffffffffffffe 0x8000000000000001 rdx = Some 0x0. Proof. vm_compute; reflexivity. Qed.
53+
Example denote_mul_rcx_lo' : denote_one mul_rcx 0xfffffffffffffffe 0x8000000000000001 rax = Some 0xfffffffffffffffe. Proof. vm_compute; reflexivity. Qed.
54+
Example denote_imul_rcx_lo' : denote_one imul_rcx 0xfffffffffffffffe 0x8000000000000001 rax = Some 0xfffffffffffffffe. Proof. vm_compute; reflexivity. Qed.
55+
56+
(** no top bit set: signed and unsigned agree *)
57+
Example denote_mul_rcx_hi'' : denote_one mul_rcx 0x123456789 0x23456789a rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
58+
Example denote_imul_rcx_hi'' : denote_one imul_rcx 0x123456789 0x23456789a rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
59+
60+
(** 8-bit form writes ah:al; al = 0xff (-1), cl = 3 *)
61+
Example denote_mul_cl_ah : denote_one mul_cl 0xff 3 ah = Some 0x2. Proof. vm_compute; reflexivity. Qed.
62+
Example denote_mul_cl_al : denote_one mul_cl 0xff 3 al = Some 0xfd. Proof. vm_compute; reflexivity. Qed.
63+
Example denote_imul_cl_ah : denote_one imul_cl 0xff 3 ah = Some 0xff. Proof. vm_compute; reflexivity. Qed.
64+
Example denote_imul_cl_al : denote_one imul_cl 0xff 3 al = Some 0xfd. Proof. vm_compute; reflexivity. Qed.
65+
(** the untouched upper bytes of rax are preserved *)
66+
Example denote_imul_cl_rax : denote_one imul_cl 0x11223344556677ff 3 rax = Some 0x112233445566fffd. Proof. vm_compute; reflexivity. Qed.
67+
68+
(** ** Symbolic executor *)
69+
70+
Local Instance test_symbolic_options : symbolic_options_computed_opt
71+
:= {| asm_rewriting_passes := default_rewriting_passes (rewriting_pipeline:=default_rewrite_pass_order) (rewriting_pass_filter:=fun _ => true)
72+
; asm_debug_symex_asm_first_computed := false
73+
; asm_node_reveal_depth_computed := default_node_reveal_depth |}.
74+
75+
(** Symbolically execute [instr] from a state where every register holds a
76+
fresh symbol, then evaluate the requested output register with the
77+
symbols for [rax] and [rcx] instantiated to the given values (and every
78+
other register to [0]). *)
79+
Definition symex_one (instr : NormalInstruction) (rax_val rcx_val : Z) (out : REG) : option Z
80+
:= let st := init_symbolic_state dag.empty in
81+
let ctx : symbol -> option Z
82+
:= fun n => if (n =? reg_index rax)%N then Some rax_val
83+
else if (n =? reg_index rcx)%N then Some rcx_val
84+
else Some 0 in
85+
match (_ <- SymexNormalInstruction (descr:=Build_description "SemanticsTests" true) instr;
86+
GetReg (descr:=Build_description "SemanticsTests" true) out)%x86symex st with
87+
| Success (i, st) => interp_expr ctx (reveal st 100 i)
88+
| Error _ => None
89+
end.
90+
91+
Example symex_mul_rcx_hi : symex_one mul_rcx 0xffffffff00000001 3 rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
92+
Example symex_mul_rcx_lo : symex_one mul_rcx 0xffffffff00000001 3 rax = Some 0xfffffffd00000003. Proof. vm_compute; reflexivity. Qed.
93+
Example symex_imul_rcx_hi : symex_one imul_rcx 0xffffffff00000001 3 rdx = Some 0xffffffffffffffff. Proof. vm_compute; reflexivity. Qed.
94+
Example symex_imul_rcx_lo : symex_one imul_rcx 0xffffffff00000001 3 rax = Some 0xfffffffd00000003. Proof. vm_compute; reflexivity. Qed.
95+
96+
Example symex_mul_rcx_hi' : symex_one mul_rcx 0xfffffffffffffffe 0x8000000000000001 rdx = Some 0x7fffffffffffffff. Proof. vm_compute; reflexivity. Qed.
97+
Example symex_imul_rcx_hi' : symex_one imul_rcx 0xfffffffffffffffe 0x8000000000000001 rdx = Some 0x0. Proof. vm_compute; reflexivity. Qed.
98+
Example symex_mul_rcx_lo' : symex_one mul_rcx 0xfffffffffffffffe 0x8000000000000001 rax = Some 0xfffffffffffffffe. Proof. vm_compute; reflexivity. Qed.
99+
Example symex_imul_rcx_lo' : symex_one imul_rcx 0xfffffffffffffffe 0x8000000000000001 rax = Some 0xfffffffffffffffe. Proof. vm_compute; reflexivity. Qed.
100+
101+
Example symex_mul_rcx_hi'' : symex_one mul_rcx 0x123456789 0x23456789a rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
102+
Example symex_imul_rcx_hi'' : symex_one imul_rcx 0x123456789 0x23456789a rdx = Some 0x2. Proof. vm_compute; reflexivity. Qed.
103+
104+
Example symex_mul_cl_ah : symex_one mul_cl 0xff 3 ah = Some 0x2. Proof. vm_compute; reflexivity. Qed.
105+
Example symex_mul_cl_al : symex_one mul_cl 0xff 3 al = Some 0xfd. Proof. vm_compute; reflexivity. Qed.
106+
Example symex_imul_cl_ah : symex_one imul_cl 0xff 3 ah = Some 0xff. Proof. vm_compute; reflexivity. Qed.
107+
Example symex_imul_cl_al : symex_one imul_cl 0xff 3 al = Some 0xfd. Proof. vm_compute; reflexivity. Qed.
108+
Example symex_imul_cl_rax : symex_one imul_cl 0x11223344556677ff 3 rax = Some 0x112233445566fffd. Proof. vm_compute; reflexivity. Qed.

src/Assembly/WithBedrock/SymbolicProofs.v

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ Lemma SymexNornalInstruction_R {opts : symbolic_options_computed_opt} {descr:des
11101110
exists m', Semantics.DenoteNormalInstruction m instr = Some m' /\ R s' m' /\ s :< s'.
11111111
Proof using Type.
11121112
intros [] s' H.
1113-
case instr as [op args]; cbv [SymexNormalInstruction OperationSize] in H.
1113+
case instr as [op args]; cbv [SymexNormalInstruction SignedMulHigh OperationSize] in H.
11141114
repeat (repeat destruct_one_match_hyp; repeat step01).
11151115

11161116
all : repeat
@@ -1361,6 +1361,10 @@ Proof using Type.
13611361
Unshelve. all : match goal with H : context[push] |- _ => idtac | H : context[pop] |- _ => idtac | _ => shelve end; shelve_unifiable.
13621362
all: rewrite !Z.land_ones by lia; push_Zmod; pull_Zmod; f_equal; lia.
13631363

1364+
Unshelve. all : match goal with H : context[Syntax.imul] |- _ => idtac | _ => shelve end; shelve_unifiable.
1365+
(* one-operand imul: the symbolic sign extension [(x + 2^(s-1)) land ones s - 2^(s-1)] is [Z.signed s x] unfolded *)
1366+
all: cbv [Z.signed]; rewrite ?Z.add_opp_r, ?(Z.add_comm _ (Z.shiftl 1 _)); reflexivity.
1367+
13641368
Unshelve. all: shelve_unifiable.
13651369
all: fail_if_goals_remain ().
13661370
Qed.

0 commit comments

Comments
 (0)