Skip to content

Commit 50b11f9

Browse files
authored
feat: Add CBMC proof-running GitHub Action (#763)
This commit adds a GitHub Action that runs the CBMC proofs upon every push and pull request. This is intended to replace the current CBMC CI.
1 parent a17c204 commit 50b11f9

File tree

4 files changed

+326
-6
lines changed

4 files changed

+326
-6
lines changed

.github/workflows/proof_ci.yaml

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
# CBMC starter kit 2.9
4+
name: Run CBMC proofs
5+
on:
6+
push:
7+
branches-ignore:
8+
- gh-pages
9+
pull_request:
10+
branches-ignore:
11+
- gh-pages
12+
workflow_dispatch:
13+
14+
# USAGE
15+
#
16+
# If you need to use different versions for tools like CBMC, modify this file:
17+
# .github/workflows/proof_ci_resources/config.yaml
18+
#
19+
# If you want the CI to use a different GitHub-hosted runner (which must still
20+
# be running Ubuntu 20.04), modify the value of this key:
21+
# jobs.run_cbmc_proofs.runs-on
22+
23+
jobs:
24+
run_cbmc_proofs:
25+
runs-on: cbmc_ubuntu-latest_32-core
26+
name: run_cbmc_proofs
27+
permissions:
28+
contents: read
29+
id-token: write
30+
pull-requests: read
31+
steps:
32+
- name: Check out repository and submodules recursively
33+
uses: actions/checkout@v3
34+
with:
35+
submodules: 'recursive'
36+
- name: Parse config file
37+
run: |
38+
CONFIG_FILE='.github/workflows/proof_ci_resources/config.yaml'
39+
for setting in cadical-tag cbmc-version cbmc-viewer-version kissat-tag litani-version proofs-dir run-cbmc-proofs-command; do
40+
VAR=$(echo $setting | tr "[:lower:]" "[:upper:]" | tr - _)
41+
echo "${VAR}"=$(yq .$setting $CONFIG_FILE) >> $GITHUB_ENV
42+
done
43+
- name: Ensure CBMC, CBMC viewer, Litani versions have been specified
44+
shell: bash
45+
run: |
46+
should_exit=false
47+
if [ "${{ env.CBMC_VERSION }}" == "" ]; then
48+
echo "You must specify a CBMC version (e.g. 'latest' or '5.70.0')"
49+
should_exit=true
50+
fi
51+
if [ "${{ env.CBMC_VIEWER_VERSION }}" == "" ]; then
52+
echo "You must specify a CBMC viewer version (e.g. 'latest' or '3.6')"
53+
should_exit=true
54+
fi
55+
if [ "${{ env.LITANI_VERSION }}" == "" ]; then
56+
echo "You must specify a Litani version (e.g. 'latest' or '1.27.0')"
57+
should_exit=true
58+
fi
59+
if [[ "$should_exit" == true ]]; then exit 1; fi
60+
- name: Install latest CBMC
61+
if: ${{ env.CBMC_VERSION == 'latest' }}
62+
shell: bash
63+
run: |
64+
# Search within 5 most recent releases for latest available package
65+
CBMC_REL="https://api.github.com/repos/diffblue/cbmc/releases?page=1&per_page=5"
66+
CBMC_DEB=$(curl -s $CBMC_REL | jq -r '.[].assets[].browser_download_url' | grep -e 'ubuntu-20.04' | head -n 1)
67+
CBMC_ARTIFACT_NAME=$(basename $CBMC_DEB)
68+
curl -o $CBMC_ARTIFACT_NAME -L $CBMC_DEB
69+
sudo dpkg -i $CBMC_ARTIFACT_NAME
70+
rm ./$CBMC_ARTIFACT_NAME
71+
- name: Install CBMC ${{ env.CBMC_VERSION }}
72+
if: ${{ env.CBMC_VERSION != 'latest' }}
73+
shell: bash
74+
run: |
75+
curl -o cbmc.deb -L \
76+
https://github.com/diffblue/cbmc/releases/download/cbmc-${{ env.CBMC_VERSION }}/ubuntu-20.04-cbmc-${{ env.CBMC_VERSION }}-Linux.deb
77+
sudo dpkg -i ./cbmc.deb
78+
rm ./cbmc.deb
79+
- name: Install latest CBMC viewer
80+
if: ${{ env.CBMC_VIEWER_VERSION == 'latest' }}
81+
shell: bash
82+
run: |
83+
CBMC_VIEWER_REL="https://api.github.com/repos/model-checking/cbmc-viewer/releases/latest"
84+
CBMC_VIEWER_VERSION=$(curl -s $CBMC_VIEWER_REL | jq -r .name | sed 's/viewer-//')
85+
pip3 install cbmc-viewer==$CBMC_VIEWER_VERSION
86+
- name: Install CBMC viewer ${{ env.CBMC_VIEWER_VERSION }}
87+
if: ${{ env.CBMC_VIEWER_VERSION != 'latest' }}
88+
shell: bash
89+
run: |
90+
sudo apt-get update
91+
sudo apt-get install --no-install-recommends --yes \
92+
build-essential universal-ctags
93+
pip3 install cbmc-viewer==${{ env.CBMC_VIEWER_VERSION }}
94+
- name: Install latest Litani
95+
if: ${{ env.LITANI_VERSION == 'latest' }}
96+
shell: bash
97+
run: |
98+
# Search within 5 most recent releases for latest available package
99+
LITANI_REL="https://api.github.com/repos/awslabs/aws-build-accumulator/releases?page=1&per_page=5"
100+
LITANI_DEB=$(curl -s $LITANI_REL | jq -r '.[].assets[0].browser_download_url' | head -n 1)
101+
DBN_PKG_FILENAME=$(basename $LITANI_DEB)
102+
curl -L $LITANI_DEB -o $DBN_PKG_FILENAME
103+
sudo apt-get update
104+
sudo apt-get install --no-install-recommends --yes ./$DBN_PKG_FILENAME
105+
rm ./$DBN_PKG_FILENAME
106+
- name: Install Litani ${{ env.LITANI_VERSION }}
107+
if: ${{ env.LITANI_VERSION != 'latest' }}
108+
shell: bash
109+
run: |
110+
curl -o litani.deb -L \
111+
https://github.com/awslabs/aws-build-accumulator/releases/download/${{ env.LITANI_VERSION }}/litani-${{ env.LITANI_VERSION }}.deb
112+
sudo apt-get update
113+
sudo apt-get install --no-install-recommends --yes ./litani.deb
114+
rm ./litani.deb
115+
- name: Install ${{ env.KISSAT_TAG }} kissat
116+
if: ${{ env.KISSAT_TAG != '' }}
117+
shell: bash
118+
run: |
119+
if ${{ env.KISSAT_TAG == 'latest' }}
120+
then
121+
KISSAT_REL="https://api.github.com/repos/arminbiere/kissat/releases/latest"
122+
KISSAT_TAG_NAME=$(curl -s $KISSAT_REL | jq -r '.tag_name')
123+
else
124+
KISSAT_TAG_NAME=${{ env.KISSAT_TAG }}
125+
fi
126+
echo "Installing kissat $KISSAT_TAG_NAME"
127+
git clone https://github.com/arminbiere/kissat.git \
128+
&& cd kissat \
129+
&& git checkout $KISSAT_TAG_NAME \
130+
&& ./configure \
131+
&& cd build \
132+
&& make -j;
133+
echo "$(pwd)" >> $GITHUB_PATH
134+
- name: Install ${{ env.CADICAL_TAG }} cadical
135+
if: ${{ env.CADICAL_TAG != '' }}
136+
shell: bash
137+
run: |
138+
if ${{ env.CADICAL_TAG == 'latest' }}
139+
then
140+
CADICAL_REL="https://api.github.com/repos/arminbiere/cadical/releases/latest"
141+
CADICAL_TAG_NAME=$(curl -s $CADICAL_REL | jq -r '.tag_name')
142+
else
143+
CADICAL_TAG_NAME=${{ env.CADICAL_TAG }}
144+
fi
145+
echo "Installing cadical $CADICAL_TAG_NAME"
146+
git clone https://github.com/arminbiere/cadical.git \
147+
&& cd cadical \
148+
&& git checkout $CADICAL_TAG_NAME \
149+
&& ./configure \
150+
&& cd build \
151+
&& make -j;
152+
echo "$(pwd)" >> $GITHUB_PATH
153+
- name: Run CBMC proofs
154+
shell: bash
155+
env:
156+
EXTERNAL_SAT_SOLVER: kissat
157+
working-directory: ${{ env.PROOFS_DIR }}
158+
run: ${{ env.RUN_CBMC_PROOFS_COMMAND }}
159+
- name: Check repository visibility
160+
shell: bash
161+
run: |
162+
VIZ="${{ fromJson(toJson(github.event.repository)).visibility }}";
163+
echo "REPO_VISIBILITY=${VIZ}" | tee -a "${GITHUB_ENV}";
164+
- name: Set name for zip artifact with CBMC proof results
165+
id: artifact
166+
if: ${{ env.REPO_VISIBILITY == 'public' }}
167+
run: |
168+
echo "name=cbmc_proof_results_${{ fromJson(toJson(github.event.repository)).name }}_$(date +%Y_%m_%d_%H_%M_%S)" >> $GITHUB_OUTPUT
169+
- name: Create zip artifact with CBMC proof results
170+
if: ${{ env.REPO_VISIBILITY == 'public' }}
171+
shell: bash
172+
run: |
173+
FINAL_REPORT_DIR=$PROOFS_DIR/output/latest/html
174+
pushd $FINAL_REPORT_DIR \
175+
&& zip -r ${{ steps.artifact.outputs.name }}.zip . \
176+
&& popd \
177+
&& mv $FINAL_REPORT_DIR/${{ steps.artifact.outputs.name }}.zip .
178+
- name: Upload zip artifact of CBMC proof results to GitHub Actions
179+
if: ${{ env.REPO_VISIBILITY == 'public' }}
180+
uses: actions/upload-artifact@v3
181+
with:
182+
name: ${{ steps.artifact.outputs.name }}
183+
path: ${{ steps.artifact.outputs.name }}.zip
184+
- name: CBMC proof results
185+
shell: bash
186+
run: |
187+
python3 ${{ env.PROOFS_DIR }}/lib/summarize.py \
188+
--run-file ${{ env.PROOFS_DIR }}/output/latest/html/run.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
cadical-tag: latest
2+
cbmc-version: "5.63.0"
3+
cbmc-viewer-version: latest
4+
kissat-tag: latest
5+
litani-version: latest
6+
proofs-dir: verification/cbmc/proofs
7+
run-cbmc-proofs-command: ./run-cbmc-proofs.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
# SPDX-License-Identifier: MIT-0
5+
6+
7+
import logging
8+
import pathlib
9+
import shutil
10+
import subprocess
11+
12+
13+
_TOOLS = [
14+
"cadical",
15+
"cbmc",
16+
"cbmc-viewer",
17+
"cbmc-starter-kit-update",
18+
"kissat",
19+
"litani",
20+
]
21+
22+
23+
def _format_versions(table):
24+
lines = [
25+
"<table>",
26+
'<tr><td colspan="2" style="font-weight: bold">Tool Versions</td></tr>',
27+
]
28+
for tool, version in table.items():
29+
if version:
30+
v_str = f'<code><pre style="margin: 0">{version}</pre></code>'
31+
else:
32+
v_str = '<em>not found</em>'
33+
lines.append(
34+
f'<tr><td style="font-weight: bold; padding-right: 1em; '
35+
f'text-align: right;">{tool}:</td>'
36+
f'<td>{v_str}</td></tr>')
37+
lines.append("</table>")
38+
return "\n".join(lines)
39+
40+
41+
def _get_tool_versions():
42+
ret = {}
43+
for tool in _TOOLS:
44+
err = f"Could not determine version of {tool}: "
45+
ret[tool] = None
46+
if not shutil.which(tool):
47+
logging.error("%s'%s' not found on $PATH", err, tool)
48+
continue
49+
cmd = [tool, "--version"]
50+
proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE)
51+
try:
52+
out, _ = proc.communicate(timeout=10)
53+
except subprocess.TimeoutExpired:
54+
logging.error("%s'%s --version' timed out", err, tool)
55+
continue
56+
if proc.returncode:
57+
logging.error(
58+
"%s'%s --version' returned %s", err, tool, str(proc.returncode))
59+
continue
60+
ret[tool] = out.strip()
61+
return ret
62+
63+
64+
def main():
65+
exe_name = pathlib.Path(__file__).name
66+
logging.basicConfig(format=f"{exe_name}: %(message)s")
67+
68+
table = _get_tool_versions()
69+
out = _format_versions(table)
70+
print(out)
71+
72+
73+
if __name__ == "__main__":
74+
main()

verification/cbmc/proofs/lib/summarize.py

+57-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: MIT-0
33

4+
import argparse
45
import json
56
import logging
7+
import os
8+
import sys
9+
10+
11+
DESCRIPTION = """Print 2 tables in GitHub-flavored Markdown that summarize
12+
an execution of CBMC proofs."""
13+
14+
15+
def get_args():
16+
"""Parse arguments for summarize script."""
17+
parser = argparse.ArgumentParser(description=DESCRIPTION)
18+
for arg in [{
19+
"flags": ["--run-file"],
20+
"help": "path to the Litani run.json file",
21+
"required": True,
22+
}]:
23+
flags = arg.pop("flags")
24+
parser.add_argument(*flags, **arg)
25+
return parser.parse_args()
626

727

828
def _get_max_length_per_column_list(data):
@@ -56,6 +76,7 @@ def _get_status_and_proof_summaries(run_dict):
5676
run_dict
5777
A dictionary representing a Litani run.
5878
79+
5980
Returns
6081
-------
6182
A list of 2 lists.
@@ -70,8 +91,9 @@ def _get_status_and_proof_summaries(run_dict):
7091
count_statuses[status_pretty_name] += 1
7192
except KeyError:
7293
count_statuses[status_pretty_name] = 1
73-
proof = proof_pipeline["name"]
74-
proofs.append([proof, status_pretty_name])
94+
if proof_pipeline["name"] == "print_tool_versions":
95+
continue
96+
proofs.append([proof_pipeline["name"], status_pretty_name])
7597
statuses = [["Status", "Count"]]
7698
for status, count in count_statuses.items():
7799
statuses.append([status, str(count)])
@@ -83,10 +105,39 @@ def print_proof_results(out_file):
83105
Print 2 strings that summarize the proof results.
84106
When printing, each string will render as a GitHub flavored Markdown table.
85107
"""
108+
output = "## Summary of CBMC proof results\n\n"
109+
with open(out_file, encoding='utf-8') as run_json:
110+
run_dict = json.load(run_json)
111+
status_table, proof_table = _get_status_and_proof_summaries(run_dict)
112+
for summary in (status_table, proof_table):
113+
output += _get_rendered_table(summary)
114+
115+
print(output)
116+
sys.stdout.flush()
117+
118+
github_summary_file = os.getenv("GITHUB_STEP_SUMMARY")
119+
if github_summary_file:
120+
with open(github_summary_file, "a") as handle:
121+
print(output, file=handle)
122+
handle.flush()
123+
else:
124+
logging.warning(
125+
"$GITHUB_STEP_SUMMARY not set, not writing summary file")
126+
127+
msg = (
128+
"Click the 'Summary' button to view a Markdown table "
129+
"summarizing all proof results")
130+
if run_dict["status"] != "success":
131+
logging.error("Not all proofs passed.")
132+
logging.error(msg)
133+
sys.exit(1)
134+
logging.info(msg)
135+
136+
137+
if __name__ == '__main__':
138+
args = get_args()
139+
logging.basicConfig(format="%(levelname)s: %(message)s")
86140
try:
87-
with open(out_file, encoding='utf-8') as run_json:
88-
run_dict = json.load(run_json)
89-
for summary in _get_status_and_proof_summaries(run_dict):
90-
print(_get_rendered_table(summary))
141+
print_proof_results(args.run_file)
91142
except Exception as ex: # pylint: disable=broad-except
92143
logging.critical("Could not print results. Exception: %s", str(ex))

0 commit comments

Comments
 (0)