Skip to content

Commit d660fde

Browse files
hanno-beckermkannwischer
authored andcommitted
CBMC: Add script to check that contracts have proofs
This commit adds a script `scripts/check-contracts` which looks for functions with `__contract__` annotations, and checks that a proof for them has been given -- with a few stated exceptions. The script is added to CI. The approach is not perfect since we do not consider which compile-time options may influence the behaviour of a function -- but it's a start. Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent 99d8180 commit d660fde

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

scripts/check-contracts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2024-2025 The mlkem-native project authors
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
#
6+
# Looks for CBMC contracts without proof
7+
#
8+
9+
import re
10+
import sys
11+
import subprocess
12+
import pathlib
13+
14+
def get_c_source_files():
15+
return get_files("mlkem/**/*.c")
16+
17+
def get_header_files():
18+
return get_files("mlkem/**/*.h")
19+
20+
def get_files(pattern):
21+
return list(map(str, pathlib.Path().glob(pattern)))
22+
23+
def gen_proofs():
24+
cmd_str = ["./proofs/cbmc/list_proofs.sh"]
25+
p = subprocess.run(cmd_str, capture_output=True, universal_newlines=False)
26+
proofs = filter(lambda s: s.strip() != "", p.stdout.decode().split("\n"))
27+
return proofs
28+
29+
def gen_contracts():
30+
files = get_c_source_files() + get_header_files()
31+
32+
for filename in files:
33+
with open(filename, "r") as f:
34+
content = f.read()
35+
36+
contract_pattern = r'(\w+)\s*\([^)]*\)\s*\n?\s*__contract__'
37+
matches = re.finditer(contract_pattern, content)
38+
for m in matches:
39+
line = content.count('\n', 0, m.start())
40+
yield (filename, line, m.group(1).removeprefix("mlk_"))
41+
42+
def is_exception(funcname):
43+
# The functions passing this filter are known not to have a proof
44+
45+
if funcname.endswith("_native"):
46+
# CBMC proofs are axiomatized against contracts of the backends
47+
return True
48+
49+
if funcname == "ct_get_optblocker_u64":
50+
# As documented in the code, this contract is treated as an axiom
51+
return True
52+
53+
if funcname in ["memcmp", "randombytes"]:
54+
# External functions
55+
return True
56+
57+
if funcname in ["zeroize"]:
58+
# Implemented using inline ASM or external functions
59+
return True
60+
61+
return False
62+
63+
def check_contracts():
64+
contracts = set(gen_contracts())
65+
proofs = set(gen_proofs())
66+
67+
bad = []
68+
69+
# Print contracts without proofs
70+
for (filename, line, funcname) in contracts:
71+
if funcname in proofs:
72+
continue
73+
74+
if is_exception(funcname):
75+
print(f"{filename}:{line}:{funcname} has contract but no proof, "
76+
"but is listed as exception")
77+
continue
78+
79+
print(f"{filename}:{line}:{funcname}: has contract but no proof. FAIL",
80+
file=sys.stderr)
81+
bad.append(funcname)
82+
83+
return len(bad) == 0
84+
85+
def _main():
86+
if check_contracts() != True:
87+
sys.exit(1)
88+
89+
if __name__ == "__main__":
90+
_main()

scripts/lint

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,21 @@ echo "::endgroup::"
135135
if ! $SUCCESS; then
136136
exit 1
137137
fi
138+
139+
check-contracts()
140+
{
141+
if python3 $ROOT/scripts/check-contracts >/dev/null; then
142+
echo ":white_check_mark: Check contracts" >>"$GITHUB_STEP_SUMMARY"
143+
else
144+
echo ":x: Check contracts" >>"$GITHUB_STEP_SUMMARY"
145+
SUCCESS=false
146+
fi
147+
}
148+
149+
echo "::group::Check contracts"
150+
check-contracts
151+
echo "::endgroup::"
152+
153+
if ! $SUCCESS; then
154+
exit 1
155+
fi

0 commit comments

Comments
 (0)