Skip to content

Commit 84c7b90

Browse files
committed
Add tool to check bindgen structs
Add a small python script that uses pahole (man 1 pahole) to test if the bindings created between two firecracker binaries match. Currently, this compares for the size of all structs created through bindgen. Signed-off-by: Babis Chalios <[email protected]>
1 parent ff2ee3d commit 84c7b90

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

tools/bindgen.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,5 @@ for crate in src/net_gen; do
116116
(cd $crate; patch -p1) <$patch
117117
done
118118
done
119+
120+
echo "Bindings created correctly! You might want to run ./tools/test_bindings.py to test for ABI incompatibilities"

tools/test_bindings.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Script used to check if bindgen-generated code creates structs that differ from previously created
6+
onces.
7+
8+
The script uses `pahole` (man 1 pahole) to gather debug information from two firecracker binaries
9+
(script's arguments). It parses pahole output and gathers struct information in a dictionary of the
10+
form:
11+
12+
```
13+
{
14+
"struct_name": {"size": size_in_bytes, "alignment": alignment_in_bytes},
15+
...
16+
}
17+
```
18+
19+
It also, filters structure names using the "bindings" filter for keeping only bindgen related
20+
structs.
21+
22+
*NOTE*: this assumes that all bindgen-related structs live under a crate or module name with
23+
"bindings" in it. At the moment, this is true.
24+
25+
It then iterates through the structs of the firecracker binary built from the older version and
26+
checks if there are mismatches with the struct info from the second binary (newer version)
27+
28+
### Usage
29+
30+
1. Create the two binaries
31+
32+
```
33+
# First create the binary with existing bindings
34+
$ git checkout main
35+
$ ./tools/devtool build
36+
$ cp ./build/cargo_target/x86_64-unknown-linux-musl/debug/firecracker firecracker_old
37+
38+
# Second create the binary with new bindings
39+
$ git checkout new_bindings
40+
$ ./tools/devtool build
41+
$ cp ./build/cargo_target/x86_64-unknown-linux-musl/debug/firecracker firecracker_new
42+
43+
# Run the script
44+
$ python3 ./tools/test_bindings.py firecracker_old firecracker_new
45+
```
46+
"""
47+
48+
import sys
49+
import re
50+
import logging
51+
import subprocess
52+
import argparse
53+
54+
logging.basicConfig(level=logging.DEBUG)
55+
log = logging.getLogger(__name__)
56+
57+
58+
def parse_pahole(pahole_output):
59+
"""Gather bindings related structs from pahole output
60+
61+
Parse pahole output and gather struct information filtering for the 'bindings' keyword.
62+
The information gathered is the struct size and its alignment.
63+
64+
@param fname: File including pahole output
65+
@return: A dictionary where keys are struct names and values struct size and alignment
66+
"""
67+
ret = {}
68+
69+
# regular expression matches the name of the struct, its size and alignment
70+
structs = re.findall(
71+
rb"struct (.*?)\{.*?/\* size: (\d+).*?\*/.*?\n\} "
72+
rb"__attribute__\(\(__aligned__\((\d+)\)\)\)\;",
73+
pahole_output,
74+
flags=re.DOTALL,
75+
)
76+
77+
for struct in structs:
78+
struct_name = str(struct[0])
79+
size = int(struct[1])
80+
alignment = int(struct[2])
81+
82+
if "bindings" in struct_name:
83+
ret[struct_name] = {"size": size, "alignment": alignment}
84+
85+
return ret
86+
87+
88+
def pahole(binary: str) -> str:
89+
"""Runs pahole on a binary and returns its output as a str
90+
91+
If pahole fails this will raise a `CalledProcessError`
92+
93+
@param binary: binary to run pahole on
94+
@return: On success, it will return the stdout of the pahole process
95+
"""
96+
result = subprocess.run(
97+
["pahole", binary], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
98+
)
99+
return result.stdout
100+
101+
102+
def check_pahole_mismatches(old: str, new: str) -> bool:
103+
"""Checks for pahole mismatches in pahole information between two binaries
104+
105+
@param old: old Firecracker binary
106+
@param new: new Firecracker binary
107+
@return: false if no mismatches found, true otherwise
108+
"""
109+
pahole_structs_1 = parse_pahole(pahole(old))
110+
pahole_structs_2 = parse_pahole(pahole(new))
111+
112+
# We go through all the structs existing in the old firecracker binary and check for mismatches
113+
# in the new one.
114+
for name, prop_1 in pahole_structs_1.items():
115+
# Note that the reverse, i.e. a name existing in the new binary but not in the old binary,
116+
# is not a problem. That would mean we are making use of some new struct from
117+
# bindgen-generated code. That does not break ABI compatibility.
118+
if name not in pahole_structs_2:
119+
log.warning("struct '%s' does not exist in new binary", name)
120+
continue
121+
122+
prop_2 = pahole_structs_2[name]
123+
# Size mismatches are hard errors
124+
if prop_1["size"] != prop_2["size"]:
125+
log.error("size of '%s' does not match in two binaries", name)
126+
log.error("old: %s", prop_1["size"])
127+
log.error("new: %s", prop_2["size"])
128+
return True
129+
130+
# Alignment mismatches just cause warnings
131+
if prop_1["alignment"] != prop_2["alignment"]:
132+
log.warning("alignment of '%s' does not match in two binaries", name)
133+
log.warning("old: %s", prop_1["alignment"])
134+
log.warning("new: %s", prop_2["alignment"])
135+
else:
136+
log.info("struct '%s' matches", name)
137+
138+
return False
139+
140+
141+
if __name__ == "__main__":
142+
parser = argparse.ArgumentParser(
143+
description="Check bindings ABI compatibility for Firecracker"
144+
)
145+
parser.add_argument(
146+
"firecracker_old",
147+
type=str,
148+
metavar="old-firecracker-binary",
149+
help="Firecracker binary with old bindings",
150+
)
151+
parser.add_argument(
152+
"firecracker_new",
153+
type=str,
154+
metavar="new-firecracker-binary",
155+
help="Firecracker binary with new bindings",
156+
)
157+
args = parser.parse_args()
158+
159+
if check_pahole_mismatches(args.firecracker_old, args.firecracker_new):
160+
log.error("Structure layout mismatch")
161+
sys.exit(1)
162+
else:
163+
log.info("Structure layout matches")
164+
165+
sys.exit(0)

0 commit comments

Comments
 (0)