Skip to content

Commit 0d6058d

Browse files
committed
CBMC: Make tests cbmc more convenient to use
- By default, don't print full CBMC output, but only success/failure. Use --verbose to see full output. Also, measure the time - Add --single-step to run one proof a time, and show a log line indicating success or failure - Add --timeout to register a timeout for CBMC proofs - Add -p to run only one proof - Add --start-with to run all proofs starting from the given one. This may be useful if all proofs up to function N succeeded previously, and now functions N until the end needs to be checked. - -l/--list-functions just prints the list of all available functions. The listing of available functions is backed by a one-line script `proofs/cbmc/list_proofs.sh`. Signed-off-by: Hanno Becker <[email protected]>
1 parent a4a43db commit 0d6058d

File tree

3 files changed

+164
-15
lines changed

3 files changed

+164
-15
lines changed

proofs/cbmc/README.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ MLKEM_K={2,3,4} run-cbmc-proofs.py --summarize
2525

2626
If `GITHUB_STEP_SUMMARY` is set, the proof summary will be appended to it.
2727

28-
# Covered functions
29-
30-
Each proved function has an eponymous sub-directory of its own. The shell command
28+
Alternatively, you can use the [tests](../../scripts/tests) script, see
3129

3230
```
33-
find . -name cbmc-proof.txt
31+
tests cbmc --help
3432
```
3533

36-
yields a list of the subdirectories, and thus function names, that have a proof.
34+
# Covered functions
35+
36+
Each proved function has an eponymous sub-directory of its own. Use [list_proofs.sh](list_proofs.sh) to see the list of functions covered.

proofs/cbmc/list_proofs.sh

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2024 The mlkem-native project authors
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# This tiny script just lists the proof directories in proof/cbmc,
6+
# which are those containing a *harness.c file.
7+
8+
ROOT=$(git rev-parse --show-toplevel)
9+
cd $ROOT
10+
ls -1 proofs/cbmc/**/*harness.c | cut -d '/' -f 3

scripts/tests

+149-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import platform
1111
import argparse
1212
import os
1313
import sys
14+
import time
1415
import logging
1516
import subprocess
1617
import json
@@ -125,6 +126,9 @@ def config_logger(verbose):
125126

126127
def logger(test_type, scheme, cross_prefix, opt):
127128
"""Emit line indicating the processing of the given test"""
129+
130+
test_desc = str(test_type)
131+
128132
compile_mode = "cross" if cross_prefix else "native"
129133
if opt is None:
130134
opt_label = ""
@@ -133,14 +137,14 @@ def logger(test_type, scheme, cross_prefix, opt):
133137
else:
134138
opt_label = " no_opt"
135139

136-
if test_type.is_example():
140+
if isinstance(test_type, TEST_TYPES) and test_type.is_example():
137141
sz = 40
138142
else:
139143
sz = 18
140144

141145
return logging.getLogger(
142146
"{0:<{1}} {2:<11} {3:<17}".format(
143-
(test_type.desc()),
147+
test_desc,
144148
sz,
145149
str(scheme),
146150
"({}{}):".format(compile_mode, opt_label),
@@ -175,6 +179,16 @@ class SCHEME(Enum):
175179
if self == SCHEME.MLKEM1024:
176180
return "1024"
177181

182+
def from_k(k):
183+
if isinstance(k, str):
184+
k = int(k)
185+
if k == 2:
186+
return SCHEME.MLKEM512
187+
if k == 3:
188+
return SCHEME.MLKEM768
189+
if k == 4:
190+
return SCHEME.MLKEM1024
191+
178192

179193
class TEST_TYPES(Enum):
180194
FUNC = 1
@@ -216,6 +230,9 @@ class TEST_TYPES(Enum):
216230
f"Could not find example {s}. Examples: {list(map(lambda e: str.lower(e.name), TEST_TYPES.examples()))}"
217231
)
218232

233+
def __str__(self):
234+
return self.desc()
235+
219236
def desc(self):
220237
if self == TEST_TYPES.FUNC:
221238
return "Functional Test"
@@ -662,20 +679,99 @@ class Tests:
662679
self.check_fail()
663680

664681
def cbmc(self):
682+
683+
def list_proofs():
684+
cmd_str = ["./proofs/cbmc/list_proofs.sh"]
685+
p = subprocess.run(cmd_str, capture_output=True, universal_newlines=False)
686+
proofs = filter(lambda s: s.strip() != "", p.stdout.decode().split("\n"))
687+
return list(proofs)
688+
689+
if self.args.list_functions:
690+
for p in list_proofs():
691+
print(p)
692+
exit(0)
693+
694+
def run_cbmc_single_step(mlkem_k, proofs):
695+
envvars = {"MLKEM_K": mlkem_k}
696+
scheme = SCHEME.from_k(mlkem_k)
697+
num_proofs = len(proofs)
698+
for i, func in enumerate(proofs):
699+
log = logger(f"CBMC ({i+1}/{num_proofs})", scheme, None, None)
700+
log.info(f"Starting CBMC proof for {func}")
701+
start = time.time()
702+
if self.args.verbose is False:
703+
extra_args = {
704+
"stdout": subprocess.DEVNULL,
705+
"stderr": subprocess.DEVNULL,
706+
}
707+
else:
708+
extra_args = {}
709+
try:
710+
p = subprocess.run(
711+
[
712+
"python3",
713+
"run-cbmc-proofs.py",
714+
"--summarize",
715+
"--no-coverage",
716+
"-p",
717+
func,
718+
]
719+
+ self.make_j(),
720+
cwd="proofs/cbmc",
721+
env=os.environ.copy() | envvars,
722+
capture_output=True,
723+
timeout=self.args.timeout,
724+
)
725+
except subprocess.TimeoutExpired:
726+
log.error(f" TIMEOUT (after {self.args.timeout}s)")
727+
log.error(p.stderr)
728+
self.fail(f"CBMC proof for {func}")
729+
if self.args.fail_upon_error:
730+
log.error(
731+
"Aborting proofs, as requested by -f/--fail-upon-error"
732+
)
733+
exit(1)
734+
continue
735+
736+
end = time.time()
737+
dur = int(end - start)
738+
if p.returncode != 0:
739+
log.error(f" FAILED (after {dur}s)")
740+
log.error(p.stderr.decode())
741+
self.fail(f"CBMC proof for {func}")
742+
else:
743+
log.info(f" SUCCESS (after {dur}s)")
744+
665745
def run_cbmc(mlkem_k):
746+
proofs = list_proofs()
747+
if self.args.start_with is not None:
748+
try:
749+
idx = proofs.index(self.args.start_with)
750+
proofs = proofs[idx:]
751+
except ValueError:
752+
log.error(
753+
"Could not find function {self.args.start_with}. Running all proofs"
754+
)
755+
if self.args.proof is not None:
756+
try:
757+
idx = proofs.index(self.args.proof)
758+
proofs = [proofs[idx]]
759+
except ValueError:
760+
log.error(
761+
"Could not find function {self.args.}. Running all proofs"
762+
)
763+
764+
if self.args.single_step:
765+
run_cbmc_single_step(mlkem_k, proofs)
766+
return
666767
envvars = {"MLKEM_K": mlkem_k}
667-
p = subprocess.Popen(
668-
[
669-
"python3",
670-
"run-cbmc-proofs.py",
671-
"--summarize",
672-
"--no-coverage",
673-
]
768+
p = subprocess.run(
769+
["python3", "run-cbmc-proofs.py", "--summarize", "--no-coverage", "-p"]
770+
+ proofs
674771
+ self.make_j(),
675772
cwd="proofs/cbmc",
676773
env=os.environ.copy() | envvars,
677774
)
678-
p.communicate()
679775

680776
if p.returncode != 0:
681777
self.fail(f"CBMC proofs for k={mlkem_k}")
@@ -893,6 +989,49 @@ def cli():
893989
default="ALL",
894990
)
895991

992+
cbmc_parser.add_argument(
993+
"--single-step",
994+
help="Run one proof a time. This is useful for debugging",
995+
action="store_true",
996+
default=False,
997+
)
998+
999+
cbmc_parser.add_argument(
1000+
"--start-with",
1001+
help="When --single-step is set, start with given proof and proceed in alphabetical order",
1002+
default=None,
1003+
)
1004+
1005+
cbmc_parser.add_argument(
1006+
"-p",
1007+
"--proof",
1008+
help="Only run the proof for the specified function.",
1009+
default=None,
1010+
)
1011+
1012+
cbmc_parser.add_argument(
1013+
"--timeout",
1014+
help="Timeout for individual CBMC proofs, in seconds",
1015+
type=int,
1016+
default=3600,
1017+
)
1018+
1019+
cbmc_parser.add_argument(
1020+
"-f",
1021+
"--fail-upon-error",
1022+
help="Stop upon first CBMC proof failure",
1023+
action="store_true",
1024+
default=False,
1025+
)
1026+
1027+
cbmc_parser.add_argument(
1028+
"-l",
1029+
"--list-functions",
1030+
help="Don't run any proofs, but list all functions for which CBMC proofs are available",
1031+
action="store_true",
1032+
default=False,
1033+
)
1034+
8961035
# func arguments
8971036
func_parser = cmd_subparsers.add_parser(
8981037
"func",

0 commit comments

Comments
 (0)