From 30986d2a7814a4a4b5f9d710db0e402764f558bf Mon Sep 17 00:00:00 2001 From: Hanno Becker Date: Fri, 31 May 2024 16:28:14 +0100 Subject: [PATCH] CBMC: Demonstrate use of CBMC in example of poly_compress C code in MLKEM-C-AArch64 should be free of undefined behaviour. The C Bounded Model Checker (CBMC) is a tool which can prove absence of various classes of undefined behaviour. This commit is a first step towards using CBMC to harden MLKEM-C-AArch64. - Setup CBMC boilerplate using cbmc-starter-kit: All proof harnesses, scripts and Makefiles needed to run CBMC are in the `cbmc/` directory. Harnesses are organized by the (main) function to which they apply, and stored in separate directories with the name of the function. - Demonstrate the use of CBMC in the example of the decompression funcion `poly_decompress`: Specify (in natural language, in a function contract, and in the test harness) that the coefficients in the output polynomial of this function are unsigned canonical. - Guided by CBMC, make smaller changes to `poly_decompress` to avoid integer truncation warnings. The previous code did _not_ trigger undefined behaviour to my understanding (signed -> unsigned conversions are always defined), but it is arguably still better style to make truncations explicit. To run the `poly_decompress` proofs along: ``` cd cbmc/proofs/poly_decompress make ``` To run all CBMC tests (only `poly_decompress` so far, but more will hopefully be added) and print a summary, ``` cd cbmc/proofs KYBER_K={2,3,4} run-cbmc-proofs.py --summarize ``` Signed-off-by: Hanno Becker --- cbmc/.gitignore | 19 + cbmc/include/README.md | 6 + cbmc/proofs/Makefile-project-defines | 33 + cbmc/proofs/Makefile-project-targets | 7 + cbmc/proofs/Makefile-project-testing | 8 + cbmc/proofs/Makefile-template-defines | 20 + cbmc/proofs/Makefile.common | 1010 +++++++++++++++++ cbmc/proofs/Makefile_params.common | 17 + cbmc/proofs/README.md | 29 + cbmc/proofs/lib/__init__.py | 0 cbmc/proofs/lib/print_tool_versions.py | 74 ++ cbmc/proofs/lib/summarize.py | 143 +++ cbmc/proofs/poly_decompress/Makefile | 58 + cbmc/proofs/poly_decompress/README.md | 12 + cbmc/proofs/poly_decompress/cbmc-proof.txt | 1 + cbmc/proofs/poly_decompress/cbmc-viewer.json | 7 + .../poly_decompress/poly_decompress_harness.c | 37 + cbmc/proofs/run-cbmc-proofs.py | 438 +++++++ cbmc/sources/README.md | 6 + cbmc/stubs/README.md | 6 + mlkem/poly.c | 37 +- 21 files changed, 1958 insertions(+), 10 deletions(-) create mode 100644 cbmc/.gitignore create mode 100644 cbmc/include/README.md create mode 100644 cbmc/proofs/Makefile-project-defines create mode 100644 cbmc/proofs/Makefile-project-targets create mode 100644 cbmc/proofs/Makefile-project-testing create mode 100644 cbmc/proofs/Makefile-template-defines create mode 100644 cbmc/proofs/Makefile.common create mode 100644 cbmc/proofs/Makefile_params.common create mode 100644 cbmc/proofs/README.md create mode 100644 cbmc/proofs/lib/__init__.py create mode 100755 cbmc/proofs/lib/print_tool_versions.py create mode 100644 cbmc/proofs/lib/summarize.py create mode 100644 cbmc/proofs/poly_decompress/Makefile create mode 100644 cbmc/proofs/poly_decompress/README.md create mode 100644 cbmc/proofs/poly_decompress/cbmc-proof.txt create mode 100644 cbmc/proofs/poly_decompress/cbmc-viewer.json create mode 100644 cbmc/proofs/poly_decompress/poly_decompress_harness.c create mode 100755 cbmc/proofs/run-cbmc-proofs.py create mode 100644 cbmc/sources/README.md create mode 100644 cbmc/stubs/README.md diff --git a/cbmc/.gitignore b/cbmc/.gitignore new file mode 100644 index 000000000..5def9fe98 --- /dev/null +++ b/cbmc/.gitignore @@ -0,0 +1,19 @@ +# Emitted when running CBMC proofs +proofs/**/logs +proofs/**/gotos +proofs/**/report +proofs/**/html +proofs/output + +# Emitted by CBMC Viewer +TAGS-* + +# Emitted by litani +.ninja_deps +.ninja_log +.litani_cache_dir + +# These files should be overwritten whenever prepare.py runs +cbmc-batch.yaml + +__pycache__/ diff --git a/cbmc/include/README.md b/cbmc/include/README.md new file mode 100644 index 000000000..36bdd94e6 --- /dev/null +++ b/cbmc/include/README.md @@ -0,0 +1,6 @@ +CBMC proof include files +======================== + +This directory contains include files written for CBMC proof. It is +common to write some code to model aspects of the system under test, +and the header files for this code go here. diff --git a/cbmc/proofs/Makefile-project-defines b/cbmc/proofs/Makefile-project-defines new file mode 100644 index 000000000..a772c3898 --- /dev/null +++ b/cbmc/proofs/Makefile-project-defines @@ -0,0 +1,33 @@ +# -*- mode: makefile -*- +# The first line sets the emacs major mode to Makefile + +################################################################ +# Use this file to give project-specific definitions of the command +# line arguments to pass to CBMC tools like goto-cc to build the goto +# binaries and cbmc to do the property and coverage checking. +# +# Use this file to override most default definitions of variables in +# Makefile.common. +################################################################ + +# Flags to pass to goto-cc for compilation (typically those passed to gcc -c) +# COMPILE_FLAGS = + +# Flags to pass to goto-cc for linking (typically those passed to gcc) +# LINK_FLAGS = + +# Flag to pass to goto-cc to make all static symbols globally visible. Leave it +# unset or set it to --export-file-local-symbols to enable this behavior. Else, +# selectively enable access to such symbols via each proof's Makefile. +EXPORT_FILE_LOCAL_SYMBOLS = + +# Preprocessor include paths -I... +# Consider adding +# INCLUDES += -I$(CBMC_ROOT)/include +# You will want to decide what order that comes in relative to the other +# include directories in your project. +# +# INCLUDES = + +# Preprocessor definitions -D... +# DEFINES = diff --git a/cbmc/proofs/Makefile-project-targets b/cbmc/proofs/Makefile-project-targets new file mode 100644 index 000000000..bf681eed1 --- /dev/null +++ b/cbmc/proofs/Makefile-project-targets @@ -0,0 +1,7 @@ +# -*- mode: makefile -*- +# The first line sets the emacs major mode to Makefile + +################################################################ +# Use this file to give project-specific targets, including targets +# that may depend on targets defined in Makefile.common. +################################################################ diff --git a/cbmc/proofs/Makefile-project-testing b/cbmc/proofs/Makefile-project-testing new file mode 100644 index 000000000..03b9a0526 --- /dev/null +++ b/cbmc/proofs/Makefile-project-testing @@ -0,0 +1,8 @@ +# -*- mode: makefile -*- +# The first line sets the emacs major mode to Makefile + +################################################################ +# Use this file to define project-specific targets and definitions for +# unit testing or continuous integration that may depend on targets +# defined in Makefile.common +################################################################ diff --git a/cbmc/proofs/Makefile-template-defines b/cbmc/proofs/Makefile-template-defines new file mode 100644 index 000000000..73e8427a4 --- /dev/null +++ b/cbmc/proofs/Makefile-template-defines @@ -0,0 +1,20 @@ + +# Absolute path to the root of the source tree. +# +SRCDIR ?= $(abspath $(PROOF_ROOT)/../..) + + +# How to invoke litani. +# Use "litani" when litani is present in PATH. +# +LITANI ?= litani + + +# Name of this proof project, displayed in proof reports. For example, +# "s2n" or "Amazon FreeRTOS". For projects with multiple proof roots, +# this may be overridden on the command-line to Make, for example +# +# make PROJECT_NAME="FreeRTOS MQTT" report +# +PROJECT_NAME = "mlkem-c-aarch64" + diff --git a/cbmc/proofs/Makefile.common b/cbmc/proofs/Makefile.common new file mode 100644 index 000000000..455b65139 --- /dev/null +++ b/cbmc/proofs/Makefile.common @@ -0,0 +1,1010 @@ +# -*- mode: makefile -*- +# The first line sets the emacs major mode to Makefile + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 AND Apache-2.0 + +CBMC_STARTER_KIT_VERSION = CBMC starter kit 2.10 + +################################################################ +# The CBMC Starter Kit depends on the files Makefile.common and +# run-cbmc-proofs.py. They are installed by the setup script +# cbmc-starter-kit-setup and updated to the latest version by the +# update script cbmc-starter-kit-update. For more information about +# the starter kit and these files and these scripts, see +# https://model-checking.github.io/cbmc-starter-kit +# +# Makefile.common implements what we consider to be some best +# practices for using cbmc for software verification. +# +# Section I gives default values for a large number of Makefile +# variables that control +# * how your code is built (include paths, etc), +# * what program transformations are applied to your code (loop +# unwinding, etc), and +# * what properties cbmc checks for in your code (memory safety, etc). +# +# These variables are defined below with definitions of the form +# VARIABLE ?= DEFAULT_VALUE +# meaning VARIABLE is set to DEFAULT_VALUE if VARIABLE has not already +# been given a value. +# +# For your project, you can override these default values with +# project-specific definitions in Makefile-project-defines. +# +# For any individual proof, you can override these default values and +# project-specific values with proof-specific definitions in the +# Makefile for your proof. +# +# The definitions in the proof Makefile override definitions in the +# project Makefile-project-defines which override definitions in this +# Makefile.common. +# +# Section II uses the values defined in Section I to build your code, run +# your proof, and build a report of your results. You should not need +# to modify or override anything in Section II, but you may want to +# read it to understand how the values defined in Section I control +# things. +# +# To use Makefile.common, set variables as described above as needed, +# and then for each proof, +# +# * Create a subdirectory . +# * Write a proof harness (a function) with the name +# in a file with the name /.c +# * Write a makefile with the name /Makefile that looks +# something like +# +# HARNESS_FILE= +# HARNESS_ENTRY= +# PROOF_UID= +# +# PROJECT_SOURCES += $(SRCDIR)/libraries/api_1.c +# PROJECT_SOURCES += $(SRCDIR)/libraries/api_2.c +# +# PROOF_SOURCES += $(PROOFDIR)/harness.c +# PROOF_SOURCES += $(SRCDIR)/cbmc/proofs/stub_a.c +# PROOF_SOURCES += $(SRCDIR)/cbmc/proofs/stub_b.c +# +# UNWINDSET += foo.0:3 +# UNWINDSET += bar.1:6 +# +# REMOVE_FUNCTION_BODY += api_stub_a +# REMOVE_FUNCTION_BODY += api_stub_b +# +# DEFINES = -DDEBUG=0 +# +# include ../Makefile.common +# +# * Change directory to and run make +# +# The proof setup script cbmc-starter-kit-setup-proof from the CBMC +# Starter Kit will do most of this for, creating a directory and +# writing a basic Makefile and proof harness into it that you can edit +# as described above. +# +# Warning: If you get results that are hard to explain, consider +# running "make clean" or "make veryclean" before "make" if you get +# results that are hard to explain. Dependency handling in this +# Makefile.common may not be perfect. + +SHELL=/bin/bash + +default: report + +################################################################ +################################################################ +## Section I: This section gives common variable definitions. +## +## Override these definitions in Makefile-project-defines or +## your proof Makefile. +## +## Remember that Makefile.common and Makefile-project-defines are +## included into the proof Makefile in your proof directory, so all +## relative pathnames defined there should be relative to your proof +## directory. + +################################################################ +# Define the layout of the source tree and the proof subtree +# +# Generally speaking, +# +# SRCDIR = the root of the repository +# CBMC_ROOT = /srcdir/cbmc +# PROOF_ROOT = /srcdir/cbmc/proofs +# PROOF_SOURCE = /srcdir/cbmc/sources +# PROOF_INCLUDE = /srcdir/cbmc/include +# PROOF_STUB = /srcdir/cbmc/stubs +# PROOFDIR = the directory containing the Makefile for your proof +# +# The path /srcdir/cbmc used in the example above is determined by the +# setup script cbmc-starter-kit-setup. Projects usually create a cbmc +# directory somewhere in the source tree, and run the setup script in +# that directory. The value of CBMC_ROOT becomes the absolute path to +# that directory. +# +# The location of that cbmc directory in the source tree affects the +# definition of SRCDIR, which is defined in terms of the relative path +# from a proof directory to the repository root. The definition is +# usually determined by the setup script cbmc-starter-kit-setup and +# written to Makefile-template-defines, but you can override it for a +# project in Makefile-project-defines and for a specific proof in the +# Makefile for the proof. + +# Absolute path to the directory containing this Makefile.common +# See https://ftp.gnu.org/old-gnu/Manuals/make-3.80/html_node/make_17.html +# +# Note: We compute the absolute paths to the makefiles in MAKEFILE_LIST +# before we filter the list of makefiles for %/Makefile.common. +# Otherwise an invocation of the form "make -f Makefile.common" will set +# MAKEFILE_LIST to "Makefile.common" which will fail to match the +# pattern %/Makefile.common. +# +MAKEFILE_PATHS = $(foreach makefile,$(MAKEFILE_LIST),$(abspath $(makefile))) +PROOF_ROOT = $(dir $(filter %/Makefile.common,$(MAKEFILE_PATHS))) + +CBMC_ROOT = $(shell dirname $(PROOF_ROOT)) +PROOF_SOURCE = $(CBMC_ROOT)/sources +PROOF_INCLUDE = $(CBMC_ROOT)/include +PROOF_STUB = $(CBMC_ROOT)/stubs + +# Project-specific definitions to override default definitions below +# * Makefile-project-defines will never be overwritten +# * Makefile-template-defines may be overwritten when the starter +# kit is updated +sinclude $(PROOF_ROOT)/Makefile-project-defines +sinclude $(PROOF_ROOT)/Makefile-template-defines + +# SRCDIR is the path to the root of the source tree +# This is a default definition that is frequently overridden in +# another Makefile, see the discussion of SRCDIR above. +SRCDIR ?= $(abspath ../..) + +# PROOFDIR is the path to the directory containing the proof harness +PROOFDIR ?= $(abspath .) + +################################################################ +# Define how to run CBMC + +# Do property checking with the external SAT solver given by +# EXTERNAL_SAT_SOLVER. Do coverage checking with the default solver, +# since coverage checking requires the use of an incremental solver. +# The EXTERNAL_SAT_SOLVER variable is typically set (if it is at all) +# as an environment variable or as a makefile variable in +# Makefile-project-defines. +# +# For a particular proof, if the default solver is faster, do property +# checking with the default solver by including this definition in the +# proof Makefile: +# USE_EXTERNAL_SAT_SOLVER = +# +ifneq ($(strip $(EXTERNAL_SAT_SOLVER)),) + USE_EXTERNAL_SAT_SOLVER ?= --external-sat-solver $(EXTERNAL_SAT_SOLVER) +endif +CHECKFLAGS += $(USE_EXTERNAL_SAT_SOLVER) + +# Job pools +# For version of Litani that are new enough (where `litani print-capabilities` +# prints "pools"), proofs for which `EXPENSIVE = true` is set can be added to a +# "job pool" that restricts how many expensive proofs are run at a time. All +# other proofs will be built in parallel as usual. +# +# In more detail: all compilation, instrumentation, and report jobs are run with +# full parallelism as usual, even for expensive proofs. The CBMC jobs for +# non-expensive proofs are also run in parallel. The only difference is that the +# CBMC safety checks and coverage checks for expensive proofs are run with a +# restricted parallelism level. At any one time, only N of these jobs are run at +# once, amongst all the proofs. +# +# To configure N, Litani needs to be initialized with a pool called "expensive". +# For example, to only run two CBMC safety/coverage jobs at a time from amongst +# all the proofs, you would initialize litani like +# litani init --pools expensive:2 +# The run-cbmc-proofs.py script takes care of this initialization through the +# --expensive-jobs-parallelism flag. +# +# To enable this feature, set +# the ENABLE_POOLS variable when running Make, like +# `make ENABLE_POOLS=true report` +# The run-cbmc-proofs.py script takes care of this through the +# --restrict-expensive-jobs flag. + +ifeq ($(strip $(ENABLE_POOLS)),) + POOL = +else ifeq ($(strip $(EXPENSIVE)),) + POOL = +else + POOL = --pool expensive +endif + +# Similar to the pool feature above. If Litani is new enough, enable +# profiling CBMC's memory use. +ifeq ($(strip $(ENABLE_MEMORY_PROFILING)),) + MEMORY_PROFILING = +else + MEMORY_PROFILING = --profile-memory +endif + +# Property checking flags +# +# Each variable below controls a specific property checking flag +# within CBMC. If desired, a property flag can be disabled within +# a particular proof by nulling the corresponding variable. For +# instance, the following line: +# +# CHECK_FLAG_POINTER_CHECK = +# +# would disable the --pointer-check CBMC flag within: +# * an entire project when added to Makefile-project-defines +# * a specific proof when added to the harness Makefile + +CBMC_FLAG_MALLOC_MAY_FAIL ?= --malloc-may-fail +CBMC_FLAG_MALLOC_FAIL_NULL ?= --malloc-fail-null +CBMC_FLAG_BOUNDS_CHECK ?= --bounds-check +CBMC_FLAG_CONVERSION_CHECK ?= --conversion-check +CBMC_FLAG_DIV_BY_ZERO_CHECK ?= --div-by-zero-check +CBMC_FLAG_FLOAT_OVERFLOW_CHECK ?= --float-overflow-check +CBMC_FLAG_NAN_CHECK ?= --nan-check +CBMC_FLAG_POINTER_CHECK ?= --pointer-check +CBMC_FLAG_POINTER_OVERFLOW_CHECK ?= --pointer-overflow-check +CBMC_FLAG_POINTER_PRIMITIVE_CHECK ?= --pointer-primitive-check +CBMC_FLAG_SIGNED_OVERFLOW_CHECK ?= --signed-overflow-check +CBMC_FLAG_UNDEFINED_SHIFT_CHECK ?= --undefined-shift-check +CBMC_FLAG_UNSIGNED_OVERFLOW_CHECK ?= --unsigned-overflow-check +CBMC_FLAG_UNWINDING_ASSERTIONS ?= --unwinding-assertions +CBMC_DEFAULT_UNWIND ?= --unwind 1 +CBMC_FLAG_FLUSH ?= --flush + +# CBMC flags used for property checking and coverage checking + +CBMCFLAGS += $(CBMC_FLAG_FLUSH) + +# CBMC flags used for property checking + +CHECKFLAGS += $(CBMC_FLAG_BOUNDS_CHECK) +CHECKFLAGS += $(CBMC_FLAG_CONVERSION_CHECK) +CHECKFLAGS += $(CBMC_FLAG_DIV_BY_ZERO_CHECK) +CHECKFLAGS += $(CBMC_FLAG_FLOAT_OVERFLOW_CHECK) +CHECKFLAGS += $(CBMC_FLAG_NAN_CHECK) +CHECKFLAGS += $(CBMC_FLAG_POINTER_CHECK) +CHECKFLAGS += $(CBMC_FLAG_POINTER_OVERFLOW_CHECK) +CHECKFLAGS += $(CBMC_FLAG_POINTER_PRIMITIVE_CHECK) +CHECKFLAGS += $(CBMC_FLAG_SIGNED_OVERFLOW_CHECK) +CHECKFLAGS += $(CBMC_FLAG_UNDEFINED_SHIFT_CHECK) +CHECKFLAGS += $(CBMC_FLAG_UNSIGNED_OVERFLOW_CHECK) + +# Additional CBMC flag to CBMC control verbosity. +# +# Meaningful values are +# 0 none +# 1 only errors +# 2 + warnings +# 4 + results +# 6 + status/phase information +# 8 + statistical information +# 9 + progress information +# 10 + debug info +# +# Uncomment the following line or set in Makefile-project-defines +# CBMC_VERBOSITY ?= --verbosity 4 + +# Additional CBMC flag to control how CBMC treats static variables. +# +# NONDET_STATIC is a list of flags of the form --nondet-static +# and --nondet-static-exclude VAR. The --nondet-static flag causes +# CBMC to initialize static variables with unconstrained value +# (ignoring initializers and default zero-initialization). The +# --nondet-static-exclude VAR excludes VAR for the variables +# initialized with unconstrained values. +NONDET_STATIC ?= + +# Flags to pass to goto-cc for compilation and linking +COMPILE_FLAGS ?= -Wall +LINK_FLAGS ?= -Wall +EXPORT_FILE_LOCAL_SYMBOLS ?= --export-file-local-symbols + +# During instrumentation, it adds models of C library functions +ADD_LIBRARY_FLAG := --add-library + +# Preprocessor include paths -I... +INCLUDES ?= +INCLUDES += -I$(PROOFDIR) +INCLUDES += -I$(SRCDIR)/mlkem +INCLUDES += -I$(SRCDIR)/fips202 + +# Preprocessor definitions -D... +DEFINES ?= + +# CBMC object model +# +# CBMC_OBJECT_BITS is the number of bits in a pointer CBMC uses for +# the id of the object to which a pointer is pointing. CBMC uses 8 +# bits for the object id by default. The remaining bits in the pointer +# are used for offset into the object. This limits the size of the +# objects that CBMC can model. This Makefile defines this bound on +# object size to be CBMC_MAX_OBJECT_SIZE. You are likely to get +# unexpected results if you try to malloc an object larger than this +# bound. +CBMC_OBJECT_BITS ?= 8 + +# CBMC loop unwinding (Normally set in the proof Makefile) +# +# UNWINDSET is a list of pairs of the form foo.1:4 meaning that +# CBMC should unwind loop 1 in function foo no more than 4 times. +# For historical reasons, the number 4 is one more than the number +# of times CBMC actually unwinds the loop. +UNWINDSET ?= + +# CBMC early loop unwinding (Normally set in the proof Makefile) +# +# Most users can ignore this variable. +# +# This variable exists to support the use of loop and function +# contracts, two features under development for CBMC. Checking the +# assigns clause for function contracts and loop invariants currently +# assumes loop-free bodies for loops and functions with contracts +# (possibly after replacing nested loops with their own loop +# contracts). To satisfy this requirement, it may be necessary to +# unwind some loops before the function contract and loop invariant +# transformations are applied to the goto program. This variable +# CPROVER_LIBRARY_UNWINDSET is identical to UNWINDSET, and we assume that the +# loops mentioned in CPROVER_LIBRARY_UNWINDSET and UNWINDSET are disjoint. +CPROVER_LIBRARY_UNWINDSET ?= + +# CBMC function removal (Normally set set in the proof Makefile) +# +# REMOVE_FUNCTION_BODY is a list of function names. CBMC will "undefine" +# the function, and CBMC will treat the function as having no side effects +# and returning an unconstrained value of the appropriate return type. +# The list should include the names of functions being stubbed out. +REMOVE_FUNCTION_BODY ?= + +# CBMC function pointer restriction (Normally set in the proof Makefile) +# +# RESTRICT_FUNCTION_POINTER is a list of function pointer restriction +# instructions of the form: +# +# .function_pointer_call./[,]* +# +# The function pointer call number in the specified function gets +# rewritten to a case switch over a finite list of functions. +# If some possible target functions are omitted from the list a counter +# example trace will be found by CBMC, i.e. the transformation is sound. +# If the target functions are file-local symbols, then mangled names must +# be used. +RESTRICT_FUNCTION_POINTER ?= + +# The project source files (Normally set set in the proof Makefile) +# +# PROJECT_SOURCES is the list of project source files to compile, +# including the source file defining the function under test. +PROJECT_SOURCES ?= + +# The proof source files (Normally set in the proof Makefile) +# +# PROOF_SOURCES is the list of proof source files to compile, including +# the proof harness, and including any function stubs being used. +PROOF_SOURCES ?= + +# The number of seconds that CBMC should be allowed to run for before +# being forcefully terminated. Currently, this is set to be less than +# the time limit for a CodeBuild job, which is eight hours. If a proof +# run takes longer than the time limit of the CI environment, the +# environment will halt the proof run without updating the Litani +# report, making the proof run appear to "hang". +CBMC_TIMEOUT ?= 21600 + +# CBMC string abstraction +# +# Replace all uses of char * by a struct that carries that string, +# and also the underlying allocation and the C string length. +STRING_ABSTRACTION ?= +ifdef STRING_ABSTRACTION + ifneq ($(strip $(STRING_ABSTRACTION)),) + CBMC_STRING_ABSTRACTION := --string-abstraction + endif +endif + +# Optional configuration library flags +OPT_CONFIG_LIBRARY ?= +CBMC_OPT_CONFIG_LIBRARY := $(CBMC_FLAG_MALLOC_MAY_FAIL) $(CBMC_FLAG_MALLOC_FAIL_NULL) $(CBMC_STRING_ABSTRACTION) + +# Proof writers could add function contracts in their source code. +# These contracts are ignored by default, but may be enabled in two distinct +# contexts using the following two variables: +# 1. To check whether one or more function contracts are sound with respect to +# the function implementation, CHECK_FUNCTION_CONTRACTS should be a list of +# function names. Use CHECK_FUNCTION_CONTRACTS_REC to check contracts on +# recursive functions. +# 2. To replace calls to certain functions with their correspondent function +# contracts, USE_FUNCTION_CONTRACTS should be a list of function names. +# One must check separately whether a function contract is sound before +# replacing it in calling contexts. +CHECK_FUNCTION_CONTRACTS ?= +CBMC_CHECK_FUNCTION_CONTRACTS := $(patsubst %,--enforce-contract %, $(CHECK_FUNCTION_CONTRACTS)) + +CHECK_FUNCTION_CONTRACTS_REC ?= +CBMC_CHECK_FUNCTION_CONTRACTS_REC := $(patsubst %,--enforce-contract-rec %, $(CHECK_FUNCTION_CONTRACTS_REC)) + +USE_FUNCTION_CONTRACTS ?= +CBMC_USE_FUNCTION_CONTRACTS := $(patsubst %,--replace-call-with-contract %, $(USE_FUNCTION_CONTRACTS)) + +CODE_CONTRACTS := $(CHECK_FUNCTION_CONTRACTS)$(USE_FUNCTION_CONTRACTS)$(APPLY_LOOP_CONTRACTS) + +# Proof writers may also apply function contracts using the Dynamic Frame +# Condition Checking (DFCC) mode. For more information on DFCC, +# please see https://diffblue.github.io/cbmc/contracts-dev-spec-dfcc.html. +USE_DYNAMIC_FRAMES ?= +ifdef USE_DYNAMIC_FRAMES + ifneq ($(strip $(USE_DYNAMIC_FRAMES)),) + CBMC_USE_DYNAMIC_FRAMES := $(CBMC_OPT_CONFIG_LIBRARY) --dfcc $(HARNESS_ENTRY) $(CBMC_CHECK_FUNCTION_CONTRACTS_REC) + endif +endif + +# Similarly, proof writers could also add loop contracts in their source code +# to obtain unbounded correctness proofs. Unlike function contracts, loop +# contracts are not reusable and thus are checked and used simultaneously. +# These contracts are also ignored by default, but may be enabled by setting +# the APPLY_LOOP_CONTRACTS variable. +APPLY_LOOP_CONTRACTS ?= +ifdef APPLY_LOOP_CONTRACTS + ifneq ($(strip $(APPLY_LOOP_CONTRACTS)),) + CBMC_APPLY_LOOP_CONTRACTS := --apply-loop-contracts + endif +endif + +# The default unwind should only be used in DFCC mode without loop contracts. +# When loop contracts are applied, we only unwind specified loops. +# If any loops remain after loop contracts have been applied, CBMC might try +# to unwind the program indefinetly, because we do not pass default unwind +# (i.e., --unwind 1) to CBMC when in DFCC mode. +# We must not use a default unwind command in DFCC mode, because contract instrumentation +# introduces loops encoding write set inclusion checks that must be dynamically unwound during +# symex. +ifneq ($(strip $(USE_DYNAMIC_FRAMES)),) +ifneq ($(strip $(APPLY_LOOP_CONTRACTS)),) + UNWIND_0500_FLAGS=$(CBMC_UNWINDSET) $(CBMC_CPROVER_LIBRARY_UNWINDSET) $(CBMC_FLAG_UNWINDING_ASSERTIONS) + UNWIND_0500_DESC="$(PROOF_UID): unwinding specified subset of loops" +else + UNWIND_0500_FLAGS=$(CBMC_UNWINDSET) $(CBMC_CPROVER_LIBRARY_UNWINDSET) $(CBMC_DEFAULT_UNWIND) $(CBMC_FLAG_UNWINDING_ASSERTIONS) + UNWIND_0500_DESC="$(PROOF_UID): unwinding all loops" +endif +endif + +# Silence makefile output (eg, long litani commands) unless VERBOSE is set. +ifndef VERBOSE + MAKEFLAGS := $(MAKEFLAGS) -s +endif + +################################################################ +################################################################ +## Section II: This section defines the process of running a proof +## +## There should be no reason to edit anything below this line. + +################################################################ +# Paths + +CBMC ?= cbmc +GOTO_ANALYZER ?= goto-analyzer +GOTO_CC ?= goto-cc +GOTO_INSTRUMENT ?= goto-instrument +CRANGLER ?= crangler +VIEWER ?= cbmc-viewer +VIEWER2 ?= cbmc-viewer +CMAKE ?= cmake + +GOTODIR ?= $(PROOFDIR)/gotos +LOGDIR ?= $(PROOFDIR)/logs + +PROJECT ?= project +PROOF ?= proof + +HARNESS_GOTO ?= $(GOTODIR)/$(HARNESS_FILE) +PROJECT_GOTO ?= $(GOTODIR)/$(PROJECT) +PROOF_GOTO ?= $(GOTODIR)/$(PROOF) + +################################################################ +# Useful macros for values that are hard to reference +SPACE :=$() $() +COMMA :=, + +################################################################ +# Set C compiler defines + +CBMCFLAGS += --object-bits $(CBMC_OBJECT_BITS) +COMPILE_FLAGS += --object-bits $(CBMC_OBJECT_BITS) + +DEFINES += -DCBMC=1 +DEFINES += -DCBMC_OBJECT_BITS=$(CBMC_OBJECT_BITS) +DEFINES += -DCBMC_MAX_OBJECT_SIZE="(SIZE_MAX>>(CBMC_OBJECT_BITS+1))" + +ifndef KYBER_K + $(error KYBER_K not set) +endif + +DEFINES += -DKYBER_K=$(KYBER_K) + +# CI currently assumes cbmc invocation has at most one --unwindset + +# UNWINDSET is designed for user code (i.e., proof and project code) +ifdef UNWINDSET + ifneq ($(strip $(UNWINDSET)),) + CBMC_UNWINDSET := --unwindset $(subst $(SPACE),$(COMMA),$(strip $(UNWINDSET))) + endif +endif + +# CPROVER_LIBRARY_UNWINDSET is designed for CPROVER library functions +ifdef CPROVER_LIBRARY_UNWINDSET + ifneq ($(strip $(CPROVER_LIBRARY_UNWINDSET)),) + CBMC_CPROVER_LIBRARY_UNWINDSET := --unwindset $(subst $(SPACE),$(COMMA),$(strip $(CPROVER_LIBRARY_UNWINDSET))) + endif +endif + +CBMC_REMOVE_FUNCTION_BODY := $(patsubst %,--remove-function-body %, $(REMOVE_FUNCTION_BODY)) + +ifdef RESTRICT_FUNCTION_POINTER + ifneq ($(strip $(RESTRICT_FUNCTION_POINTER)),) + CBMC_RESTRICT_FUNCTION_POINTER := $(patsubst %,--restrict-function-pointer %, $(RESTRICT_FUNCTION_POINTER)) + endif +endif + +################################################################ +# Targets for rewriting source files with crangler + +# Construct crangler configuration files +# +# REWRITTEN_SOURCES is a list of crangler output files source.i. +# This target assumes that for each source.i +# * source.i_SOURCE is the path to a source file, +# * source.i_FUNCTIONS is a list of functions (may be empty) +# * source.i_OBJECTS is a list of variables (may be empty) +# This target constructs the crangler configuration file source.i.json +# of the form +# { +# "sources": [ "/proj/code.c" ], +# "includes": [ "/proj/include" ], +# "defines": [ "VAR=1" ], +# "functions": [ {"function_name": ["remove static"]} ], +# "objects": [ {"variable_name": ["remove static"]} ], +# "output": "source.i" +# } +# to remove the static attribute from function_name and variable_name +# in the source file source.c and write the result to source.i. +# +# This target assumes that filenames include no spaces and that +# the INCLUDES and DEFINES variables include no spaces after -I +# and -D. For example, use "-DVAR=1" and not "-D VAR=1". +# +# Define *_SOURCE, *_FUNCTIONS, and *_OBJECTS in the proof Makefile. +# The string source.i is usually an absolute path $(PROOFDIR)/code.i +# to a file in the proof directory that contains the proof Makefile. +# The proof Makefile usually includes the definitions +# $(PROOFDIR)/code.i_SOURCE = /proj/code.c +# $(PROOFDIR)/code.i_FUNCTIONS = function_name +# $(PROOFDIR)/code.i_OBJECTS = variable_name +# Because these definitions refer to PROOFDIR that is defined in this +# Makefile.common, these definitions must appear after the inclusion +# of Makefile.common in the proof Makefile. +# +$(foreach rs,$(REWRITTEN_SOURCES),$(eval $(rs).json: $($(rs)_SOURCE))) +$(foreach rs,$(REWRITTEN_SOURCES),$(rs).json): + echo '{'\ + '"sources": ['\ + '"$($(@:.json=)_SOURCE)"'\ + '],'\ + '"includes": ['\ + '$(subst $(SPACE),$(COMMA),$(patsubst -I%,"%",$(strip $(INCLUDES))))' \ + '],'\ + '"defines": ['\ + '$(subst $(SPACE),$(COMMA),$(patsubst -D%,"%",$(subst ",\",$(strip $(DEFINES)))))' \ + '],'\ + '"functions": ['\ + '{'\ + '$(subst ~, ,$(subst $(SPACE),$(COMMA),$(patsubst %,"%":["remove~static"],$($(@:.json=)_FUNCTIONS))))' \ + '}'\ + '],'\ + '"objects": ['\ + '{'\ + '$(subst ~, ,$(subst $(SPACE),$(COMMA),$(patsubst %,"%":["remove~static"],$($(@:.json=)_OBJECTS))))' \ + '}'\ + '],'\ + '"output": "$(@:.json=)"'\ + '}' > $@ + +# Rewrite source files with crangler +# +$(foreach rs,$(REWRITTEN_SOURCES),$(eval $(rs): $(rs).json)) +$(REWRITTEN_SOURCES): + $(LITANI) add-job \ + --command \ + '$(CRANGLER) $@.json' \ + --inputs $($@_SOURCE) \ + --outputs $@ \ + --stdout-file $(LOGDIR)/crangler-$(subst /,_,$(subst .,_,$@))-log.txt \ + --interleave-stdout-stderr \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): removing static" + +################################################################ +# Build targets that make the relevant .goto files + +# Compile project sources +$(PROJECT_GOTO)0100.goto: $(PROJECT_SOURCES) $(REWRITTEN_SOURCES) + $(LITANI) add-job \ + --command \ + '$(GOTO_CC) $(CBMC_VERBOSITY) $(COMPILE_FLAGS) $(EXPORT_FILE_LOCAL_SYMBOLS) $(INCLUDES) $(DEFINES) $^ -o $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/project_sources-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): building project binary" + +# Compile proof sources +$(PROOF_GOTO)0100.goto: $(PROOF_SOURCES) + $(LITANI) add-job \ + --command \ + '$(GOTO_CC) $(CBMC_VERBOSITY) $(COMPILE_FLAGS) $(EXPORT_FILE_LOCAL_SYMBOLS) $(INCLUDES) $(DEFINES) $^ -o $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/proof_sources-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): building proof binary" + +# Remove function bodies from project sources +$(PROJECT_GOTO)0200.goto: $(PROJECT_GOTO)0100.goto + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(CBMC_REMOVE_FUNCTION_BODY) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/remove_function_body-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): removing function bodies from project sources" + +# Link project and proof sources into the proof harness +$(HARNESS_GOTO)0100.goto: $(PROOF_GOTO)0100.goto $(PROJECT_GOTO)0200.goto + $(LITANI) add-job \ + --command '$(GOTO_CC) $(CBMC_VERBOSITY) --function $(HARNESS_ENTRY) $^ $(LINK_FLAGS) -o $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/link_proof_project-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): linking project to proof" + +# Restrict function pointers +$(HARNESS_GOTO)0200.goto: $(HARNESS_GOTO)0100.goto + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(CBMC_RESTRICT_FUNCTION_POINTER) --remove-function-pointers $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/restrict_function_pointer-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): restricting function pointers in project sources" + +# Fill static variable with unconstrained values +$(HARNESS_GOTO)0300.goto: $(HARNESS_GOTO)0200.goto +ifneq ($(strip $(CODE_CONTRACTS)),) + $(LITANI) add-job \ + --command 'cp $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/nondet_static-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): not setting static variables to nondet (will do during contract instrumentation)" +else + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(NONDET_STATIC) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/nondet_static-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): setting static variables to nondet" +endif + +# Link CPROVER library if DFCC mode is on +$(HARNESS_GOTO)0400.goto: $(HARNESS_GOTO)0300.goto +ifneq ($(strip $(USE_DYNAMIC_FRAMES)),) + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(ADD_LIBRARY_FLAG) $(CBMC_OPT_CONFIG_LIBRARY) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/linking-library-models-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): linking CPROVER library" +else + $(LITANI) add-job \ + --command 'cp $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/linking-library-models-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): not linking CPROVER library" +endif + +# Early unwind all loops on DFCC mode; otherwise, only unwind loops in proof and project code +$(HARNESS_GOTO)0500.goto: $(HARNESS_GOTO)0400.goto +ifneq ($(strip $(USE_DYNAMIC_FRAMES)),) + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(UNWIND_0500_FLAGS) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/unwind_loops-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description $(UNWIND_0500_DESC) +else ifneq ($(strip $(CODE_CONTRACTS)),) + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) $(CBMC_UNWINDSET) $(CBMC_FLAG_UNWINDING_ASSERTIONS) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/unwind_loops-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): unwinding loops in proof and project code" +else + $(LITANI) add-job \ + --command 'cp $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/unwind_loops-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): not unwinding loops" +endif + +# Replace function contracts, check function contracts, instrument for loop contracts +$(HARNESS_GOTO)0600.goto: $(HARNESS_GOTO)0500.goto + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_USE_DYNAMIC_FRAMES) $(NONDET_STATIC) $(CBMC_VERBOSITY) $(CBMC_CHECK_FUNCTION_CONTRACTS) $(CBMC_USE_FUNCTION_CONTRACTS) $(CBMC_APPLY_LOOP_CONTRACTS) $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/check_function_contracts-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): checking function contracts" + +# Omit initialization of unused global variables (reduces problem size) +$(HARNESS_GOTO)0700.goto: $(HARNESS_GOTO)0600.goto + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) --slice-global-inits $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/slice_global_inits-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): slicing global initializations" + +# Omit unused functions (sharpens coverage calculations) +$(HARNESS_GOTO)0800.goto: $(HARNESS_GOTO)0700.goto + $(LITANI) add-job \ + --command \ + '$(GOTO_INSTRUMENT) $(CBMC_VERBOSITY) --drop-unused-functions $^ $@' \ + --inputs $^ \ + --outputs $@ \ + --stdout-file $(LOGDIR)/drop_unused_functions-log.txt \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): dropping unused functions" + +# Final name for proof harness +$(HARNESS_GOTO).goto: $(HARNESS_GOTO)0800.goto + $(LITANI) add-job \ + --command 'cp $< $@' \ + --inputs $^ \ + --outputs $@ \ + --pipeline-name "$(PROOF_UID)" \ + --ci-stage build \ + --description "$(PROOF_UID): copying final goto-binary" + +################################################################ +# Targets to run the analysis commands + +ifdef CBMCFLAGS + ifeq ($(strip $(CODE_CONTRACTS)),) + CBMCFLAGS += $(CBMC_UNWINDSET) $(CBMC_CPROVER_LIBRARY_UNWINDSET) $(CBMC_DEFAULT_UNWIND) $(CBMC_OPT_CONFIG_LIBRARY) + else ifeq ($(strip $(USE_DYNAMIC_FRAMES)),) + CBMCFLAGS += $(CBMC_CPROVER_LIBRARY_UNWINDSET) $(CBMC_OPT_CONFIG_LIBRARY) + endif +endif + +$(LOGDIR)/result.xml: $(HARNESS_GOTO).goto + $(LITANI) add-job \ + $(POOL) \ + --command \ + '$(CBMC) $(CBMC_VERBOSITY) $(CBMCFLAGS) $(CBMC_FLAG_UNWINDING_ASSERTIONS) $(CHECKFLAGS) --trace --xml-ui $<' \ + --inputs $^ \ + --outputs $@ \ + --ci-stage test \ + --stdout-file $@ \ + $(MEMORY_PROFILING) \ + --ignore-returns 10 \ + --timeout $(CBMC_TIMEOUT) \ + --pipeline-name "$(PROOF_UID)" \ + --tags "stats-group:safety checks" \ + --stderr-file $(LOGDIR)/result-err-log.txt \ + --description "$(PROOF_UID): checking safety properties" + +$(LOGDIR)/property.xml: $(HARNESS_GOTO).goto + $(LITANI) add-job \ + --command \ + '$(CBMC) $(CBMC_VERBOSITY) $(CBMCFLAGS) $(CBMC_FLAG_UNWINDING_ASSERTIONS) $(CHECKFLAGS) --show-properties --xml-ui $<' \ + --inputs $^ \ + --outputs $@ \ + --ci-stage test \ + --stdout-file $@ \ + --ignore-returns 10 \ + --pipeline-name "$(PROOF_UID)" \ + --stderr-file $(LOGDIR)/property-err-log.txt \ + --description "$(PROOF_UID): printing safety properties" + +$(LOGDIR)/coverage.xml: $(HARNESS_GOTO).goto + $(LITANI) add-job \ + $(POOL) \ + --command \ + '$(CBMC) $(CBMC_VERBOSITY) $(CBMCFLAGS) $(COVERFLAGS) --cover location --xml-ui $<' \ + --inputs $^ \ + --outputs $@ \ + --ci-stage test \ + --stdout-file $@ \ + $(MEMORY_PROFILING) \ + --ignore-returns 10 \ + --timeout $(CBMC_TIMEOUT) \ + --pipeline-name "$(PROOF_UID)" \ + --tags "stats-group:coverage computation" \ + --stderr-file $(LOGDIR)/coverage-err-log.txt \ + --description "$(PROOF_UID): calculating coverage" + +COVERAGE ?= $(LOGDIR)/coverage.xml +VIEWER_COVERAGE_FLAG ?= --coverage $(COVERAGE) + +$(PROOFDIR)/report: $(LOGDIR)/result.xml $(LOGDIR)/property.xml $(COVERAGE) + $(LITANI) add-job \ + --command " $(VIEWER) \ + --result $(LOGDIR)/result.xml \ + $(VIEWER_COVERAGE_FLAG) \ + --property $(LOGDIR)/property.xml \ + --srcdir $(SRCDIR) \ + --goto $(HARNESS_GOTO).goto \ + --reportdir $(PROOFDIR)/report \ + --config $(PROOFDIR)/cbmc-viewer.json" \ + --inputs $^ \ + --outputs $(PROOFDIR)/report \ + --pipeline-name "$(PROOF_UID)" \ + --stdout-file $(LOGDIR)/viewer-log.txt \ + --ci-stage report \ + --description "$(PROOF_UID): generating report" + +litani-path: + @echo $(LITANI) + +# ############################################################## +# Phony Rules +# +# These rules provide a convenient way to run a single proof up to a +# certain stage. Users can browse into a proof directory and run +# "make -Bj 3 report" to generate a report for just that proof, or +# "make goto" to build the goto binary. Under the hood, this runs litani +# for just that proof. + +_goto: $(HARNESS_GOTO).goto +goto: + @ echo Running 'litani init' + $(LITANI) init --project $(PROJECT_NAME) + @ echo Running 'litani add-job' + $(MAKE) -B _goto + @ echo Running 'litani build' + $(LITANI) run-build + +_result: $(LOGDIR)/result.txt +result: + @ echo Running 'litani init' + $(LITANI) init --project $(PROJECT_NAME) + @ echo Running 'litani add-job' + $(MAKE) -B _result + @ echo Running 'litani build' + $(LITANI) run-build + +_property: $(LOGDIR)/property.xml +property: + @ echo Running 'litani init' + $(LITANI) init --project $(PROJECT_NAME) + @ echo Running 'litani add-job' + $(MAKE) -B _property + @ echo Running 'litani build' + $(LITANI) run-build + +_coverage: $(LOGDIR)/coverage.xml +coverage: + @ echo Running 'litani init' + $(LITANI) init --project $(PROJECT_NAME) + @ echo Running 'litani add-job' + $(MAKE) -B _coverage + @ echo Running 'litani build' + $(LITANI) run-build + +_report: $(PROOFDIR)/report +report: + @ echo Running 'litani init' + $(LITANI) init --project $(PROJECT_NAME) + @ echo Running 'litani add-job' + $(MAKE) -B _report + @ echo Running 'litani build' + $(LITANI) run-build + +_report_no_coverage: + $(MAKE) COVERAGE="" VIEWER_COVERAGE_FLAG="" _report +report-no-coverage: + $(MAKE) COVERAGE="" VIEWER_COVERAGE_FLAG=" " report + +################################################################ +# Targets to clean up after ourselves +clean: + -$(RM) $(DEPENDENT_GOTOS) + -$(RM) TAGS* + -$(RM) *~ \#* + -$(RM) $(REWRITTEN_SOURCES) $(foreach rs,$(REWRITTEN_SOURCES),$(rs).json) + +veryclean: clean + -$(RM) -r report + -$(RM) -r $(LOGDIR) $(GOTODIR) + +.PHONY: \ + _coverage \ + _goto \ + _property \ + _report \ + _report_no_coverage \ + clean \ + coverage \ + goto \ + litani-path \ + property \ + report \ + report-no-coverage \ + result \ + setup_dependencies \ + testdeps \ + veryclean \ + # + +################################################################ + +# Run "make echo-proof-uid" to print the proof ID of a proof. This can be +# used by scripts to ensure that every proof has an ID, that there are +# no duplicates, etc. + +.PHONY: echo-proof-uid +echo-proof-uid: + @echo $(PROOF_UID) + +.PHONY: echo-project-name +echo-project-name: + @echo $(PROJECT_NAME) + +################################################################ + +# Project-specific targets requiring values defined above +sinclude $(PROOF_ROOT)/Makefile-project-targets + +# CI-specific targets to drive cbmc in CI +sinclude $(PROOF_ROOT)/Makefile-project-testing + +################################################################ diff --git a/cbmc/proofs/Makefile_params.common b/cbmc/proofs/Makefile_params.common new file mode 100644 index 000000000..40b339603 --- /dev/null +++ b/cbmc/proofs/Makefile_params.common @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +ifndef KYBER_K + $(warning KYBER_K not set -- defaulting to KYBER_K=3) +endif + +KYBER_K ?= 3 + +ifeq ($(KYBER_K),2) + KYBER_NAMESPACE = pqcrystals_kyber512_ref_ +else ifeq ($(KYBER_K),3) + KYBER_NAMESPACE=pqcrystals_kyber768_ref_ +else ifeq ($(KYBER_K),4) + KYBER_NAMESPACE=pqcrystals_kyber1024_ref_ +else + $(error Invalid value of KYBER_K) +endif \ No newline at end of file diff --git a/cbmc/proofs/README.md b/cbmc/proofs/README.md new file mode 100644 index 000000000..de18841a9 --- /dev/null +++ b/cbmc/proofs/README.md @@ -0,0 +1,29 @@ +CBMC proofs +=========== + +# Overview + +This directory contains [CBMC](https://github.com/diffblue/cbmc) proofs for the absence +of certain classes of undefined behaviour for parts of the C-code in MLKEM-C-AArch64. + +Proofs are organized by functions, with the harnesses and proofs for each function +in a separate directory. + +TODO: Provide more information about CBMC and the properties it proves + +# Usage + +To run all proofs, print a summary at the end and reflect overall +success/failure in the error code, use + +``` +KYBER_K={2,3,4} run-cbmc-proofs.py --summarize +``` + +If `GITHUB_STEP_SUMMARY` is set, the proof summary will be appended to it. + +# Covered functions + +The following functions are currently covered: + +- `poly.c`: `poly_compress` diff --git a/cbmc/proofs/lib/__init__.py b/cbmc/proofs/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cbmc/proofs/lib/print_tool_versions.py b/cbmc/proofs/lib/print_tool_versions.py new file mode 100755 index 000000000..bdeb429e3 --- /dev/null +++ b/cbmc/proofs/lib/print_tool_versions.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + + +import logging +import pathlib +import shutil +import subprocess + + +_TOOLS = [ + "cadical", + "cbmc", + "cbmc-viewer", + "cbmc-starter-kit-update", + "kissat", + "litani", +] + + +def _format_versions(table): + lines = [ + "", + '', + ] + for tool, version in table.items(): + if version: + v_str = f'
{version}
' + else: + v_str = 'not found' + lines.append( + f'' + f'') + lines.append("
Tool Versions
{tool}:{v_str}
") + return "\n".join(lines) + + +def _get_tool_versions(): + ret = {} + for tool in _TOOLS: + err = f"Could not determine version of {tool}: " + ret[tool] = None + if not shutil.which(tool): + logging.error("%s'%s' not found on $PATH", err, tool) + continue + cmd = [tool, "--version"] + proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE) + try: + out, _ = proc.communicate(timeout=10) + except subprocess.TimeoutExpired: + logging.error("%s'%s --version' timed out", err, tool) + continue + if proc.returncode: + logging.error( + "%s'%s --version' returned %s", err, tool, str(proc.returncode)) + continue + ret[tool] = out.strip() + return ret + + +def main(): + exe_name = pathlib.Path(__file__).name + logging.basicConfig(format=f"{exe_name}: %(message)s") + + table = _get_tool_versions() + out = _format_versions(table) + print(out) + + +if __name__ == "__main__": + main() diff --git a/cbmc/proofs/lib/summarize.py b/cbmc/proofs/lib/summarize.py new file mode 100644 index 000000000..50dbcc33c --- /dev/null +++ b/cbmc/proofs/lib/summarize.py @@ -0,0 +1,143 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import argparse +import json +import logging +import os +import sys + + +DESCRIPTION = """Print 2 tables in GitHub-flavored Markdown that summarize +an execution of CBMC proofs.""" + + +def get_args(): + """Parse arguments for summarize script.""" + parser = argparse.ArgumentParser(description=DESCRIPTION) + for arg in [{ + "flags": ["--run-file"], + "help": "path to the Litani run.json file", + "required": True, + }]: + flags = arg.pop("flags") + parser.add_argument(*flags, **arg) + return parser.parse_args() + + +def _get_max_length_per_column_list(data): + ret = [len(item) + 1 for item in data[0]] + for row in data[1:]: + for idx, item in enumerate(row): + ret[idx] = max(ret[idx], len(item) + 1) + return ret + + +def _get_table_header_separator(max_length_per_column_list): + line_sep = "" + for max_length_of_word_in_col in max_length_per_column_list: + line_sep += "|" + "-" * (max_length_of_word_in_col + 1) + line_sep += "|\n" + return line_sep + + +def _get_entries(max_length_per_column_list, row_data): + entries = [] + for row in row_data: + entry = "" + for idx, word in enumerate(row): + max_length_of_word_in_col = max_length_per_column_list[idx] + space_formatted_word = (max_length_of_word_in_col - len(word)) * " " + entry += "| " + word + space_formatted_word + entry += "|\n" + entries.append(entry) + return entries + + +def _get_rendered_table(data): + table = [] + max_length_per_column_list = _get_max_length_per_column_list(data) + entries = _get_entries(max_length_per_column_list, data) + for idx, entry in enumerate(entries): + if idx == 1: + line_sep = _get_table_header_separator(max_length_per_column_list) + table.append(line_sep) + table.append(entry) + table.append("\n") + return "".join(table) + + +def _get_status_and_proof_summaries(run_dict): + """Parse a dict representing a Litani run and create lists summarizing the + proof results. + + Parameters + ---------- + run_dict + A dictionary representing a Litani run. + + + Returns + ------- + A list of 2 lists. + The first sub-list maps a status to the number of proofs with that status. + The second sub-list maps each proof to its status. + """ + count_statuses = {} + proofs = [["Proof", "Status"]] + for proof_pipeline in run_dict["pipelines"]: + status_pretty_name = proof_pipeline["status"].title().replace("_", " ") + try: + count_statuses[status_pretty_name] += 1 + except KeyError: + count_statuses[status_pretty_name] = 1 + if proof_pipeline["name"] == "print_tool_versions": + continue + proofs.append([proof_pipeline["name"], status_pretty_name]) + statuses = [["Status", "Count"]] + for status, count in count_statuses.items(): + statuses.append([status, str(count)]) + return [statuses, proofs] + + +def print_proof_results(out_file): + """ + Print 2 strings that summarize the proof results. + When printing, each string will render as a GitHub flavored Markdown table. + """ + output = "## Summary of CBMC proof results\n\n" + with open(out_file, encoding='utf-8') as run_json: + run_dict = json.load(run_json) + status_table, proof_table = _get_status_and_proof_summaries(run_dict) + for summary in (status_table, proof_table): + output += _get_rendered_table(summary) + + print(output) + sys.stdout.flush() + + github_summary_file = os.getenv("GITHUB_STEP_SUMMARY") + if github_summary_file: + with open(github_summary_file, "a") as handle: + print(output, file=handle) + handle.flush() + else: + logging.warning( + "$GITHUB_STEP_SUMMARY not set, not writing summary file") + + msg = ( + "Click the 'Summary' button to view a Markdown table " + "summarizing all proof results") + if run_dict["status"] != "success": + logging.error("Not all proofs passed.") + logging.error(msg) + sys.exit(1) + logging.info(msg) + + +if __name__ == '__main__': + args = get_args() + logging.basicConfig(format="%(levelname)s: %(message)s") + try: + print_proof_results(args.run_file) + except Exception as ex: # pylint: disable=broad-except + logging.critical("Could not print results. Exception: %s", str(ex)) diff --git a/cbmc/proofs/poly_decompress/Makefile b/cbmc/proofs/poly_decompress/Makefile new file mode 100644 index 000000000..72367fdd8 --- /dev/null +++ b/cbmc/proofs/poly_decompress/Makefile @@ -0,0 +1,58 @@ +include ../Makefile_params.common + +HARNESS_ENTRY = harness +HARNESS_FILE = poly_decompress_harness + +# This should be a unique identifier for this proof, and will appear on the +# Litani dashboard. It can be human-readable and contain spaces if you wish. +PROOF_UID = poly_decompress + +DEFINES += + +REMOVE_FUNCTION_BODY += +UNWINDSET += + +PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c +PROJECT_SOURCES += $(SRCDIR)/mlkem/poly.c + +# For KYBER_K=2,3 a loop with 128 iterations is used. +# For KYBER_K=4, a nested loop with 32 and 8 iterations is used. +ifneq ($(KYBER_K),4) + UNWINDSET += $(KYBER_NAMESPACE)poly_decompress.0:129 +else + UNWINDSET += $(KYBER_NAMESPACE)poly_decompress.1:33 + UNWINDSET += $(KYBER_NAMESPACE)poly_decompress.0:9 +endif + +FUNCTION_NAME = $(KYBER_NAMESPACE)poly_decompress + +# TODO: I would like to comment this out to have CBMC validate the +# function contract as annotated in the source, rather than being forced +# to replicate it in the harness. But somehow this doesn't work...? + +# USE_FUNCTION_CONTRACTS = $(FUNCTION_NAME) + +# USE_DYNAMIC_FRAMES = 1 +# EXTERNAL_SAT_SOLVER = cadical +# CBMCFLAGS += --smt2 + +# If this proof is found to consume huge amounts of RAM, you can set the +# EXPENSIVE variable. With new enough versions of the proof tools, this will +# restrict the number of EXPENSIVE CBMC jobs running at once. See the +# documentation in Makefile.common under the "Job Pools" heading for details. +# EXPENSIVE = true + +# If you require access to a file-local ("static") function or object to conduct +# your proof, set the following (and do not include the original source file +# ("mlkem/poly.c") in PROJECT_SOURCES). +# REWRITTEN_SOURCES = $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i +# include ../Makefile.common +# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_SOURCE = $(SRCDIR)/mlkem/poly.c +# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_FUNCTIONS = foo bar +# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_OBJECTS = baz +# Care is required with variables on the left-hand side: REWRITTEN_SOURCES must +# be set before including Makefile.common, but any use of variables on the +# left-hand side requires those variables to be defined. Hence, _SOURCE, +# _FUNCTIONS, _OBJECTS is set after including Makefile.common. + +include ../Makefile.common diff --git a/cbmc/proofs/poly_decompress/README.md b/cbmc/proofs/poly_decompress/README.md new file mode 100644 index 000000000..e590d3e3c --- /dev/null +++ b/cbmc/proofs/poly_decompress/README.md @@ -0,0 +1,12 @@ +poly_decompress proof +============== + +This directory contains a memory safety proof for poly_decompress. + +To run the proof. +------------- +* Follow these [tool installation instructions](https://github.com/awslabs/aws-templates-for-cbmc-proofs/wiki/Installation) to install cbmc and cbmc-viewer. +* Add `cbmc`, `goto-cc`, `goto-instrument`, `goto-analyzer`, and `cbmc-viewer` + to your path. +* Run `make`. +* Open `report/html/index.html` in a web browser. diff --git a/cbmc/proofs/poly_decompress/cbmc-proof.txt b/cbmc/proofs/poly_decompress/cbmc-proof.txt new file mode 100644 index 000000000..6ed46f125 --- /dev/null +++ b/cbmc/proofs/poly_decompress/cbmc-proof.txt @@ -0,0 +1 @@ +# This file marks this directory as containing a CBMC proof. diff --git a/cbmc/proofs/poly_decompress/cbmc-viewer.json b/cbmc/proofs/poly_decompress/cbmc-viewer.json new file mode 100644 index 000000000..b73d0fd50 --- /dev/null +++ b/cbmc/proofs/poly_decompress/cbmc-viewer.json @@ -0,0 +1,7 @@ +{ "expected-missing-functions": + [ + + ], + "proof-name": "poly_decompress", + "proof-root": "cbmc/proofs" +} diff --git a/cbmc/proofs/poly_decompress/poly_decompress_harness.c b/cbmc/proofs/poly_decompress/poly_decompress_harness.c new file mode 100644 index 000000000..3fb6c4526 --- /dev/null +++ b/cbmc/proofs/poly_decompress/poly_decompress_harness.c @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 AND Apache-2.0 + +/* + * Insert copyright notice + */ + +/** + * @file poly_decompress_harness.c + * @brief Implements the proof harness for poly_decompress function. + */ + +/* + * Insert project header files that + * - include the declaration of the function + * - include the types needed to declare function arguments + */ +#include "poly.h" + +/** + * @brief Starting point for formal analysis + * + */ +void harness(void) +{ + poly r; + uint8_t a[KYBER_POLYCOMPRESSEDBYTES]; + + poly_decompress(&r, a); + + // TODO: We're replicating the post-condition of the function contract of + // poly_decompress here. Ideally, we'd use CBMC's function contract mechanism + // here, but there are still issues. cf. a similar comment in the Makefile. + __CPROVER_assert(__CPROVER_forall { \ + unsigned i; (i < KYBER_N) ==> ( 0 <= r.coeffs[i] && r.coeffs[i] < KYBER_Q ) + }, "failed to prove post-condition for poly_decompress"); +} diff --git a/cbmc/proofs/run-cbmc-proofs.py b/cbmc/proofs/run-cbmc-proofs.py new file mode 100755 index 000000000..b10492cea --- /dev/null +++ b/cbmc/proofs/run-cbmc-proofs.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + + +import argparse +import asyncio +import json +import logging +import math +import os +import pathlib +import re +import subprocess +import sys +import tempfile +import uuid + +from lib.summarize import print_proof_results + + +DESCRIPTION = "Configure and run all CBMC proofs in parallel" + +# Keep the epilog hard-wrapped at 70 characters, as it gets printed +# verbatim in the terminal. 70 characters stops here --------------> | +EPILOG = """ +This tool automates the process of running `make report` in each of +the CBMC proof directories. The tool calculates the dependency graph +of all tasks needed to build, run, and report on all the proofs, and +executes these tasks in parallel. + +The tool is roughly equivalent to doing this: + + litani init --project "my-cool-project"; + + find . -name cbmc-proof.txt | while read -r proof; do + pushd $(dirname ${proof}); + + # The `make _report` rule adds a single proof to litani + # without running it + make _report; + + popd; + done + + litani run-build; + +except that it is much faster and provides some convenience options. +The CBMC CI runs this script with no arguments to build and run all +proofs in parallel. The value of "my-cool-project" is taken from the +PROJECT_NAME variable in Makefile-project-defines. + +The --no-standalone argument omits the `litani init` and `litani +run-build`; use it when you want to add additional proof jobs, not +just the CBMC ones. In that case, you would run `litani init` +yourself; then run `run-cbmc-proofs --no-standalone`; add any +additional jobs that you want to execute with `litani add-job`; and +finally run `litani run-build`. + +The litani dashboard will be written under the `output` directory; the +cbmc-viewer reports remain in the `$PROOF_DIR/report` directory. The +HTML dashboard from the latest Litani run will always be symlinked to +`output/latest/html/index.html`, so you can keep that page open in +your browser and reload the page whenever you re-run this script. +""" +# 70 characters stops here ----------------------------------------> | + + +def get_project_name(): + cmd = [ + "make", + "--no-print-directory", + "-f", "Makefile.common", + "echo-project-name", + ] + logging.debug(" ".join(cmd)) + proc = subprocess.run(cmd, universal_newlines=True, stdout=subprocess.PIPE, check=False) + if proc.returncode: + logging.critical("could not run make to determine project name") + sys.exit(1) + if not proc.stdout.strip(): + logging.warning( + "project name has not been set; using generic name instead. " + "Set the PROJECT_NAME value in Makefile-project-defines to " + "remove this warning") + return "" + return proc.stdout.strip() + + +def get_args(): + pars = argparse.ArgumentParser( + description=DESCRIPTION, epilog=EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter) + for arg in [{ + "flags": ["-j", "--parallel-jobs"], + "type": int, + "metavar": "N", + "help": "run at most N proof jobs in parallel", + }, { + "flags": ["--fail-on-proof-failure"], + "action": "store_true", + "help": "exit with return code `10' if any proof failed" + " (default: exit 0)", + }, { + "flags": ["--no-standalone"], + "action": "store_true", + "help": "only configure proofs: do not initialize nor run", + }, { + "flags": ["-p", "--proofs"], + "nargs": "+", + "metavar": "DIR", + "help": "only run proof in directory DIR (can pass more than one)", + }, { + "flags": ["--project-name"], + "metavar": "NAME", + "default": get_project_name(), + "help": "project name for report. Default: %(default)s", + }, { + "flags": ["--marker-file"], + "metavar": "FILE", + "default": "cbmc-proof.txt", + "help": ( + "name of file that marks proof directories. Default: " + "%(default)s"), + }, { + "flags": ["--no-memory-profile"], + "action": "store_true", + "help": "disable memory profiling, even if Litani supports it" + }, { + "flags": ["--no-expensive-limit"], + "action": "store_true", + "help": "do not limit parallelism of 'EXPENSIVE' jobs", + }, { + "flags": ["--expensive-jobs-parallelism"], + "metavar": "N", + "default": 1, + "type": int, + "help": ( + "how many proof jobs marked 'EXPENSIVE' to run in parallel. " + "Default: %(default)s"), + }, { + "flags": ["--verbose"], + "action": "store_true", + "help": "verbose output", + }, { + "flags": ["--debug"], + "action": "store_true", + "help": "debug output", + }, { + "flags": ["--summarize"], + "action": "store_true", + "help": "summarize proof results with two tables on stdout", + }, { + "flags": ["--version"], + "action": "version", + "version": "CBMC starter kit 2.10", + "help": "display version and exit" + }, { + "flags": ["--no-coverage"], + "action": "store_true", + "help": "do property checking without coverage checking" + }]: + flags = arg.pop("flags") + pars.add_argument(*flags, **arg) + return pars.parse_args() + + +def set_up_logging(verbose): + if verbose: + level = logging.DEBUG + else: + level = logging.WARNING + logging.basicConfig( + format="run-cbmc-proofs: %(message)s", level=level) + + +def task_pool_size(): + ret = os.cpu_count() + if ret is None or ret < 3: + return 1 + return ret - 2 + + +def print_counter(counter): + # pylint: disable=consider-using-f-string + print("\rConfiguring CBMC proofs: " + "{complete:{width}} / {total:{width}}".format(**counter), end="", file=sys.stderr) + + +def get_proof_dirs(proof_root, proof_list, marker_file): + if proof_list is not None: + proofs_remaining = list(proof_list) + else: + proofs_remaining = [] + + for root, _, fyles in os.walk(proof_root): + proof_name = str(pathlib.Path(root).name) + if root != str(proof_root) and ".litani_cache_dir" in fyles: + pathlib.Path(f"{root}/.litani_cache_dir").unlink() + if proof_list and proof_name not in proof_list: + continue + if proof_list and proof_name in proofs_remaining: + proofs_remaining.remove(proof_name) + if marker_file in fyles: + yield root + + if proofs_remaining: + logging.critical( + "The following proofs were not found: %s", + ", ".join(proofs_remaining)) + sys.exit(1) + + +def run_build(litani, jobs, fail_on_proof_failure, summarize): + cmd = [str(litani), "run-build"] + if jobs: + cmd.extend(["-j", str(jobs)]) + if fail_on_proof_failure: + cmd.append("--fail-on-pipeline-failure") + if summarize: + out_file = pathlib.Path(tempfile.gettempdir(), "run.json").resolve() + cmd.extend(["--out-file", str(out_file)]) + + logging.debug(" ".join(cmd)) + proc = subprocess.run(cmd, check=False) + + if proc.returncode and not fail_on_proof_failure: + logging.critical("Failed to run litani run-build") + sys.exit(1) + + if summarize: + print_proof_results(out_file) + out_file.unlink() + + if proc.returncode: + logging.error("One or more proofs failed") + sys.exit(10) + +def get_litani_path(proof_root): + cmd = [ + "make", + "--no-print-directory", + f"PROOF_ROOT={proof_root}", + "-f", "Makefile.common", + "litani-path", + ] + logging.debug(" ".join(cmd)) + proc = subprocess.run(cmd, universal_newlines=True, stdout=subprocess.PIPE, check=False) + if proc.returncode: + logging.critical("Could not determine path to litani") + sys.exit(1) + return proc.stdout.strip() + + +def get_litani_capabilities(litani_path): + cmd = [litani_path, "print-capabilities"] + proc = subprocess.run( + cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False) + if proc.returncode: + return [] + try: + return json.loads(proc.stdout) + except RuntimeError: + logging.warning("Could not load litani capabilities: '%s'", proc.stdout) + return [] + + +def check_uid_uniqueness(proof_dir, proof_uids): + with (pathlib.Path(proof_dir) / "Makefile").open() as handle: + for line in handle: + match = re.match(r"^PROOF_UID\s*=\s*(?P\w+)", line) + if not match: + continue + if match["uid"] not in proof_uids: + proof_uids[match["uid"]] = proof_dir + return + + logging.critical( + "The Makefile in directory '%s' should have a different " + "PROOF_UID than the Makefile in directory '%s'", + proof_dir, proof_uids[match["uid"]]) + sys.exit(1) + + logging.critical( + "The Makefile in directory '%s' should contain a line like", proof_dir) + logging.critical("PROOF_UID = ...") + logging.critical("with a unique identifier for the proof.") + sys.exit(1) + + +def should_enable_memory_profiling(litani_caps, args): + if args.no_memory_profile: + return False + return "memory_profile" in litani_caps + + +def should_enable_pools(litani_caps, args): + if args.no_expensive_limit: + return False + return "pools" in litani_caps + + +async def configure_proof_dirs( # pylint: disable=too-many-arguments + queue, counter, proof_uids, enable_pools, enable_memory_profiling, report_target, debug): + while True: + print_counter(counter) + path = str(await queue.get()) + + check_uid_uniqueness(path, proof_uids) + + pools = ["ENABLE_POOLS=true"] if enable_pools else [] + profiling = [ + "ENABLE_MEMORY_PROFILING=true"] if enable_memory_profiling else [] + + # Allow interactive tasks to preempt proof configuration + proc = await asyncio.create_subprocess_exec( + "nice", "-n", "15", "make", *pools, + *profiling, "-B", report_target, "" if debug else "--quiet", cwd=path, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + logging.debug("returncode: %s", str(proc.returncode)) + logging.debug("stdout:") + for line in stdout.decode().splitlines(): + logging.debug(line) + logging.debug("stderr:") + for line in stderr.decode().splitlines(): + logging.debug(line) + + counter["fail" if proc.returncode else "pass"].append(path) + counter["complete"] += 1 + + print_counter(counter) + queue.task_done() + + +def add_tool_version_job(): + cmd = [ + "litani", "add-job", + "--command", "./lib/print_tool_versions.py .", + "--description", "printing out tool versions", + "--phony-outputs", str(uuid.uuid4()), + "--pipeline-name", "print_tool_versions", + "--ci-stage", "report", + "--tags", "front-page-text", + ] + proc = subprocess.run(cmd) + if proc.returncode: + logging.critical("Could not add tool version printing job") + sys.exit(1) + + +async def main(): # pylint: disable=too-many-locals + args = get_args() + set_up_logging(args.verbose) + + proof_root = pathlib.Path(os.getcwd()) + litani = get_litani_path(proof_root) + + litani_caps = get_litani_capabilities(litani) + enable_pools = should_enable_pools(litani_caps, args) + init_pools = [ + "--pools", f"expensive:{args.expensive_jobs_parallelism}" + ] if enable_pools else [] + + if not args.no_standalone: + cmd = [ + str(litani), "init", *init_pools, "--project", args.project_name, + "--no-print-out-dir", + ] + + if "output_directory_flags" in litani_caps: + out_prefix = proof_root / "output" + out_symlink = out_prefix / "latest" + out_index = out_symlink / "html" / "index.html" + cmd.extend([ + "--output-prefix", str(out_prefix), + "--output-symlink", str(out_symlink), + ]) + print( + "\nFor your convenience, the output of this run will be symbolically linked to ", + out_index, "\n") + + logging.debug(" ".join(cmd)) + proc = subprocess.run(cmd, check=False) + if proc.returncode: + logging.critical("Failed to run litani init") + sys.exit(1) + + proof_dirs = list(get_proof_dirs( + proof_root, args.proofs, args.marker_file)) + if not proof_dirs: + logging.critical("No proof directories found") + sys.exit(1) + + proof_queue = asyncio.Queue() + for proof_dir in proof_dirs: + proof_queue.put_nowait(proof_dir) + + counter = { + "pass": [], + "fail": [], + "complete": 0, + "total": len(proof_dirs), + "width": int(math.log10(len(proof_dirs))) + 1 + } + + proof_uids = {} + tasks = [] + + enable_memory_profiling = should_enable_memory_profiling(litani_caps, args) + report_target = "_report_no_coverage" if args.no_coverage else "_report" + + for _ in range(task_pool_size()): + task = asyncio.create_task(configure_proof_dirs( + proof_queue, counter, proof_uids, enable_pools, + enable_memory_profiling, report_target, args.debug)) + tasks.append(task) + + await proof_queue.join() + + add_tool_version_job() + + print_counter(counter) + print("", file=sys.stderr) + + if counter["fail"]: + logging.critical( + "Failed to configure the following proofs:\n%s", "\n".join( + [str(f) for f in counter["fail"]])) + sys.exit(1) + + if not args.no_standalone: + run_build(litani, args.parallel_jobs, args.fail_on_proof_failure, args.summarize) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cbmc/sources/README.md b/cbmc/sources/README.md new file mode 100644 index 000000000..4f706b23d --- /dev/null +++ b/cbmc/sources/README.md @@ -0,0 +1,6 @@ +CBMC proof source code +====================== + +This directory contains source code written for CBMC proofs. It is +common to write some code to model aspects of the system under test, +and this code goes here. diff --git a/cbmc/stubs/README.md b/cbmc/stubs/README.md new file mode 100644 index 000000000..3b7823dd6 --- /dev/null +++ b/cbmc/stubs/README.md @@ -0,0 +1,6 @@ +CBMC proof stubs +====================== + +This directory contains the stubs written for CBMC proofs. It is +common to stub out functionality like network send and receive methods +when writing a CBMC proof, and the code for these stubs goes here. diff --git a/mlkem/poly.c b/mlkem/poly.c index a481397d0..c85a1faca 100644 --- a/mlkem/poly.c +++ b/mlkem/poly.c @@ -78,8 +78,22 @@ void poly_compress(uint8_t r[KYBER_POLYCOMPRESSEDBYTES], const poly *a) { * Arguments: - poly *r: pointer to output polynomial * - const uint8_t *a: pointer to input byte array * (of length KYBER_POLYCOMPRESSEDBYTES bytes) +* +* Upon return, the coefficients of the output polynomial are unsigned-canonical +* (non-negative and smaller than KYBER_Q). +* **************************************************/ -void poly_decompress(poly *r, const uint8_t a[KYBER_POLYCOMPRESSEDBYTES]) { +void poly_decompress(poly *r, const uint8_t a[KYBER_POLYCOMPRESSEDBYTES]) +/* ------ CBMC contract ------ */ +__CPROVER_requires(__CPROVER_is_fresh(r, sizeof(*r))) +__CPROVER_requires(__CPROVER_is_fresh(a, sizeof(KYBER_POLYCOMPRESSEDBYTES))) +__CPROVER_ensures( \ + /* Output coefficients are unsigned canonical */ \ + __CPROVER_forall { \ + unsigned i; (i < KYBER_N) ==> ( 0 <= r->coeffs[i] && r->coeffs[i] < KYBER_Q ) + }) +/* --- End of CBMC contract --- */ +{ unsigned int i; #if (KYBER_POLYCOMPRESSEDBYTES == 128) @@ -92,18 +106,21 @@ void poly_decompress(poly *r, const uint8_t a[KYBER_POLYCOMPRESSEDBYTES]) { unsigned int j; uint8_t t[8]; for (i = 0; i < KYBER_N / 8; i++) { - t[0] = (a[0] >> 0); - t[1] = (a[0] >> 5) | (a[1] << 3); - t[2] = (a[1] >> 2); - t[3] = (a[1] >> 7) | (a[2] << 1); - t[4] = (a[2] >> 4) | (a[3] << 4); - t[5] = (a[3] >> 1); - t[6] = (a[3] >> 6) | (a[4] << 2); - t[7] = (a[4] >> 3); + // REF-CHANGE: Explicitly truncate to avoid warning about + // implicit truncation in CBMC. + t[0] = 0x1F & (a[0] >> 0); + t[1] = 0x1F & ((a[0] >> 5) | (a[1] << 3)); + t[2] = 0x1F & (a[1] >> 2); + t[3] = 0x1F & ((a[1] >> 7) | (a[2] << 1)); + t[4] = 0x1F & ((a[2] >> 4) | (a[3] << 4)); + t[5] = 0x1F & (a[3] >> 1); + t[6] = 0x1F & ((a[3] >> 6) | (a[4] << 2)); + t[7] = 0x1F & (a[4] >> 3); a += 5; for (j = 0; j < 8; j++) { - r->coeffs[8 * i + j] = ((uint32_t)(t[j] & 31) * KYBER_Q + 16) >> 5; + // REF-CHANGE: Truncation happened before + r->coeffs[8 * i + j] = ((uint32_t) t[j] * KYBER_Q + 16) >> 5; } } #else