Skip to content

Commit 0567eb9

Browse files
hanno-beckermkannwischer
authored andcommitted
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 the specified proofs - 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 34086b2 commit 0567eb9

File tree

3 files changed

+159
-15
lines changed

3 files changed

+159
-15
lines changed

proofs/cbmc/README.md

Lines changed: 5 additions & 5 deletions
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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 144 additions & 10 deletions
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"
@@ -664,20 +681,93 @@ class Tests:
664681
self.check_fail()
665682

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

682772
if p.returncode != 0:
683773
self.fail(f"CBMC proofs for k={mlkem_k}")
@@ -895,6 +985,50 @@ def cli():
895985
default="ALL",
896986
)
897987

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

0 commit comments

Comments
 (0)