Skip to content

Commit d9f9ba1

Browse files
tmlemanlgirdwood
authored andcommitted
scripts: fuzz: add IPC3 libFuzzer dictionary generator
The native_sim libFuzzer harness consults an external dictionary (`-dict=`) for known byte sequences to splice into mutated inputs. The IPC3 dictionary shipped until now was hand-written: thirteen opaque `kwN=` entries, two of which were corpus-derived byte blobs rather than real dispatch constants, covering only a subset of the topology / PM / stream command space. It had no documented provenance and silently drifted out of sync as header.h changed. Add a generator that harvests the SOF_GLB_TYPE() / SOF_CMD_TYPE() combines the global type with its command type: cmd = (glb_type << 28) | (cmd_type << 16) so each token lands directly in a leaf handler when the mutator splices it into the cmd field at offset 4 of a message (the size dword at offset 0 is rewritten by the harness, so cmd is the only dispatch-relevant header field). The bare global types are emitted too, as a 4-byte splice for messages the fuzzer builds from scratch. The parser is intentionally narrow: it only matches the two SOF_*_TYPE() macros, which keeps the script free of preprocessor logic while covering the full IPC3 dispatch surface (topology, PM, component, DAI, stream, trace, probe, debug and test groups). The cmd-group-to-global-type mapping is an explicit table so a renamed or newly added group is an obvious one-line edit. A missing header is a hard error. Usage: python3 scripts/gen_fuzz_ipc3_dict.py -o <output.dict> Coverage impact, measured on the coverage-instrumented harness (IPC3, empty corpus, 5 seeds x 60 s, paired dict-minus-nodict means): libFuzzer edges (cov) +7 libFuzzer features (ft) +266 (positive on every seed) IPC subsystem lines +9 (+0.4 pp of src/ipc) IPC3's dispatch surface is small enough that line coverage saturates either way at 60 s; the dictionary's consistent benefit is in feature (edge x counter) combinations. The change is primarily about completeness and maintainability, replacing opaque hand-tuned tokens with a documented, header-synchronised superset (64 entries). The output is not committed; it is regenerated from the header on demand (for example by CI into a temporary file) and deployments may extend it with site-specific tokens. Signed-off-by: Tomasz Leman <tomasz.m.leman@intel.com>
1 parent 2918412 commit d9f9ba1

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

scripts/gen_fuzz_ipc3_dict.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
# Copyright(c) 2026 Intel Corporation. All rights reserved.
4+
#
5+
# Generate dictionary/ipc3.dict for the SOF IPC3 libFuzzer harness.
6+
#
7+
# libFuzzer's dictionary is consulted by mutation strategies that
8+
# inject known byte sequences into the candidate inputs. For SOF
9+
# IPC3 the single most valuable token is the 32-bit "cmd" dword that
10+
# lives at offset 4 of every message (struct sof_ipc_cmd_hdr: the
11+
# size dword is rewritten by the harness, so only cmd matters for
12+
# dispatch). That dword packs two fields:
13+
#
14+
# glb_type : bits 28..31 (SOF_GLB_TYPE)
15+
# cmd_type : bits 16..27 (SOF_CMD_TYPE)
16+
#
17+
# The firmware switches on glb_type first and then on cmd_type, so a
18+
# token that is only a bare glb_type reaches a handler but stalls at
19+
# its inner switch. We therefore emit, for every command group, the
20+
# fully-combined "glb_type | cmd_type" dword that lands directly in a
21+
# leaf handler, plus the bare glb_type values on their own (useful as
22+
# a 4-byte splice when the fuzzer is building a message from scratch).
23+
#
24+
# The constants are harvested directly from src/include/ipc/header.h
25+
# so the dictionary stays in sync with firmware code; only the
26+
# SOF_GLB_TYPE() / SOF_CMD_TYPE() #defines are parsed, which keeps the
27+
# harvester simple and robust.
28+
#
29+
# Usage:
30+
# python3 scripts/gen_fuzz_ipc3_dict.py [-o dictionary/ipc3.dict]
31+
#
32+
# The output is regenerated on demand from the header (for example by
33+
# CI, into a temporary file) and is intentionally not committed to the
34+
# tree; regenerate it whenever the harvested header changes.
35+
36+
import argparse
37+
import re
38+
import sys
39+
from pathlib import Path
40+
41+
# Path to the IPC3 header, relative to the SOF repo root (the checkout
42+
# that contains this script's `scripts/` directory).
43+
HEADER_REL = Path("src/include/ipc/header.h")
44+
45+
# Bit layout of the command dword (see SOF_GLB_TYPE / SOF_CMD_TYPE in
46+
# src/include/ipc/header.h).
47+
GLB_TYPE_SHIFT = 28
48+
CMD_TYPE_SHIFT = 16
49+
50+
# Command groups: each SOF_CMD_TYPE() value is only meaningful when
51+
# OR-ed with the global type that selects its handler. This table
52+
# maps a cmd #define name prefix to the SOF_GLB_TYPE() #define that
53+
# dispatches it. Order controls the layout of the emitted file.
54+
CMD_GROUPS = [
55+
("SOF_IPC_TPLG_", "SOF_IPC_GLB_TPLG_MSG"),
56+
("SOF_IPC_PM_", "SOF_IPC_GLB_PM_MSG"),
57+
("SOF_IPC_COMP_", "SOF_IPC_GLB_COMP_MSG"),
58+
("SOF_IPC_DAI_", "SOF_IPC_GLB_DAI_MSG"),
59+
("SOF_IPC_STREAM_", "SOF_IPC_GLB_STREAM_MSG"),
60+
("SOF_IPC_TRACE_", "SOF_IPC_GLB_TRACE_MSG"),
61+
("SOF_IPC_PROBE_", "SOF_IPC_GLB_PROBE"),
62+
("SOF_IPC_DEBUG_", "SOF_IPC_GLB_DEBUG"),
63+
("SOF_IPC_TEST_", "SOF_IPC_GLB_TEST"),
64+
]
65+
66+
GLB_RE = re.compile(
67+
r"#define\s+(?P<name>SOF_IPC_\w+)\s+SOF_GLB_TYPE\(\s*"
68+
r"(?P<value>0x[0-9a-fA-F]+|\d+)U?L?\s*\)"
69+
)
70+
CMD_RE = re.compile(
71+
r"#define\s+(?P<name>SOF_IPC_\w+)\s+SOF_CMD_TYPE\(\s*"
72+
r"(?P<value>0x[0-9a-fA-F]+|\d+)U?L?\s*\)"
73+
)
74+
75+
76+
def u32_le(value):
77+
"""Encode an unsigned 32-bit value as 4 little-endian bytes."""
78+
value &= 0xFFFFFFFF
79+
return bytes([
80+
value & 0xFF,
81+
(value >> 8) & 0xFF,
82+
(value >> 16) & 0xFF,
83+
(value >> 24) & 0xFF,
84+
])
85+
86+
87+
def fmt_dict_entry(name, raw_bytes):
88+
"""Format a libFuzzer dictionary line: name="\\xNN\\xNN...".
89+
90+
libFuzzer accepts ASCII printables unescaped; we escape everything
91+
except space..~ (excluding the quote and backslash) which keeps the
92+
file readable while remaining unambiguous.
93+
"""
94+
pieces = []
95+
for b in raw_bytes:
96+
if 0x20 <= b <= 0x7E and b not in (0x22, 0x5C):
97+
pieces.append(chr(b))
98+
else:
99+
pieces.append(f"\\x{b:02x}")
100+
return f'{name}="{"".join(pieces)}"'
101+
102+
103+
def harvest(repo_root):
104+
"""Parse header.h, returning (glb_defs, cmd_defs) name->value dicts."""
105+
path = repo_root / HEADER_REL
106+
if not path.is_file():
107+
print(f"error: {path} not found", file=sys.stderr)
108+
sys.exit(1)
109+
text = path.read_text()
110+
111+
glb_defs = {}
112+
for m in GLB_RE.finditer(text):
113+
glb_defs[m.group("name")] = int(m.group("value"), 0) << GLB_TYPE_SHIFT
114+
115+
cmd_defs = {}
116+
for m in CMD_RE.finditer(text):
117+
cmd_defs[m.group("name")] = int(m.group("value"), 0) << CMD_TYPE_SHIFT
118+
119+
return glb_defs, cmd_defs
120+
121+
122+
def build_sections(glb_defs, cmd_defs):
123+
"""Return list of (section_label, [(name, raw_bytes), ...])."""
124+
sections = []
125+
126+
# Bare global message types.
127+
glb_entries = [(name, u32_le(val)) for name, val in glb_defs.items()]
128+
if glb_entries:
129+
sections.append(("Global message types (glb_type only)", glb_entries))
130+
131+
# Combined glb_type | cmd_type dwords, one section per group.
132+
for prefix, glb_name in CMD_GROUPS:
133+
glb_val = glb_defs.get(glb_name)
134+
if glb_val is None:
135+
print(f"warning: {glb_name} not found in header, "
136+
f"skipping group {prefix}*", file=sys.stderr)
137+
continue
138+
entries = []
139+
for name, cmd_val in cmd_defs.items():
140+
if not name.startswith(prefix):
141+
continue
142+
entries.append((name, u32_le(glb_val | cmd_val)))
143+
if entries:
144+
sections.append((f"{glb_name} commands", entries))
145+
146+
return sections
147+
148+
149+
def render(sections):
150+
out_lines = [
151+
"# SOF IPC3 libFuzzer dictionary",
152+
"#",
153+
"# Generated by scripts/gen_fuzz_ipc3_dict.py from the IPC3",
154+
"# header src/include/ipc/header.h. Do not edit by hand;",
155+
"# regenerate instead.",
156+
"#",
157+
"# Each entry is a 4-byte little-endian command dword. Command",
158+
"# entries combine the global type and the command type so they",
159+
"# land directly in a leaf handler when libFuzzer's CMP /",
160+
"# dictionary mutators splice them into the cmd field at offset 4",
161+
"# of a message.",
162+
"",
163+
]
164+
for label, entries in sections:
165+
out_lines.append(f"# --- {label} ---")
166+
for name, raw in entries:
167+
out_lines.append(fmt_dict_entry(name, raw))
168+
out_lines.append("")
169+
return "\n".join(out_lines)
170+
171+
172+
def main():
173+
parser = argparse.ArgumentParser(
174+
description="Generate the SOF IPC3 libFuzzer dictionary from the "
175+
"in-tree IPC3 header.")
176+
parser.add_argument(
177+
"-o", "--output",
178+
help="Output path (default: stdout)",
179+
)
180+
parser.add_argument(
181+
"--repo-root",
182+
default=None,
183+
help="SOF repository root (default: auto-detected from script "
184+
"location)",
185+
)
186+
args = parser.parse_args()
187+
188+
if args.repo_root:
189+
repo_root = Path(args.repo_root).resolve()
190+
else:
191+
# Script lives at <repo>/scripts/gen_fuzz_ipc3_dict.py.
192+
repo_root = Path(__file__).resolve().parents[1]
193+
194+
glb_defs, cmd_defs = harvest(repo_root)
195+
sections = build_sections(glb_defs, cmd_defs)
196+
if not sections:
197+
print("error: no dictionary entries harvested; refusing to write "
198+
"an empty dictionary", file=sys.stderr)
199+
sys.exit(1)
200+
text = render(sections)
201+
if args.output:
202+
Path(args.output).write_text(text)
203+
else:
204+
sys.stdout.write(text)
205+
206+
207+
if __name__ == "__main__":
208+
main()

0 commit comments

Comments
 (0)