Skip to content

Commit da974d3

Browse files
committed
[CKC] Add CVE verification with --check-cves option
Adds ability to verify that CVE references in PR commit messages correctly match the upstream commits they reference. Uses the kernel vulnerabilities database to cross-check CVE assignments against upstream commit hashes. The --check-cves flag enables validation that detects three error conditions: mismatched CVE assignments between PR and upstream commits, CVE references to upstream commits with no CVE assignment, and failures accessing the vulnerabilities database. Output format matches existing checker patterns with support for both plain text and markdown modes.
1 parent 281dd55 commit da974d3

File tree

1 file changed

+178
-5
lines changed

1 file changed

+178
-5
lines changed

check_kernel_commits.py

Lines changed: 178 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66
import sys
77
import textwrap
8+
import os
89

910
def run_git(repo, args):
1011
"""Run a git command in the given repository and return its output as a string."""
@@ -50,14 +51,15 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
5051
"""
5152
Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive.
5253
Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length.
54+
Returns a list of tuples: (full_hash, display_string)
5355
"""
5456
results = []
5557
# Get all commits with 'Fixes:' in the message
5658
output = run_git(repo, [
5759
'log', upstream_ref, '--grep', 'Fixes:', '-i', '--format=%H %h %s (%an)%x0a%B%x00'
5860
]).strip()
5961
if not output:
60-
return ""
62+
return []
6163
# Each commit is separated by a NUL character and a newline
6264
commits = output.split('\x00\x0a')
6365
# Prepare hash prefixes from 12 down to 6
@@ -78,11 +80,11 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
7880
for prefix in hash_prefixes:
7981
if m.group(1).lower().startswith(prefix.lower()):
8082
if not commit_exists_in_branch(repo, pr_branch, full_hash):
81-
results.append(' '.join(header.split()[1:]))
83+
results.append((full_hash, ' '.join(header.split()[1:])))
8284
break
8385
else:
8486
continue
85-
return "\n".join(results)
87+
return results
8688

8789
def commit_exists_in_branch(repo, pr_branch, upstream_hash_):
8890
"""
@@ -104,17 +106,75 @@ def wrap_paragraph(text, width=80, initial_indent='', subsequent_indent=''):
104106
break_on_hyphens=False)
105107
return wrapper.fill(text)
106108

109+
def extract_cve_from_message(msg):
110+
"""Extract CVE reference from commit message. Returns CVE ID or None.
111+
Only matches 'cve CVE-2025-12345', ignores 'cve-bf' and 'cve-pre' variants."""
112+
match = re.search(r'(?<!\S)cve\s+(CVE-\d{4}-\d+)', msg, re.IGNORECASE)
113+
if match:
114+
return match.group(1).upper()
115+
return None
116+
117+
def run_cve_search(vulns_repo, kernel_repo, query):
118+
"""
119+
Run the cve_search script from the vulns repo.
120+
Returns (success, output_message).
121+
"""
122+
cve_search_path = os.path.join(vulns_repo, 'scripts', 'cve_search')
123+
if not os.path.exists(cve_search_path):
124+
raise RuntimeError(f"cve_search script not found at {cve_search_path}")
125+
126+
env = os.environ.copy()
127+
env['CVEKERNELTREE'] = kernel_repo
128+
129+
result = subprocess.run([cve_search_path, query],
130+
text=True,
131+
capture_output=True,
132+
check=False,
133+
env=env)
134+
135+
# cve_search outputs results to stdout
136+
return result.returncode == 0, result.stdout.strip()
137+
107138
def main():
108139
parser = argparse.ArgumentParser(description="Check upstream references and Fixes: tags in PR branch commits.")
109140
parser.add_argument("--repo", help="Path to the git repo", required=True)
110141
parser.add_argument("--pr_branch", help="Name of the PR branch", required=True)
111142
parser.add_argument("--base_branch", help="Name of the base branch", required=True)
112143
parser.add_argument("--markdown", action='store_true', help="Output in Markdown, suitable for GitHub PR comments")
113144
parser.add_argument("--upstream-ref", default="origin/kernel-mainline", help="Reference to upstream mainline branch (default: origin/kernel-mainline)")
145+
parser.add_argument("--check-cves", action='store_true', help="Check that CVE references in commit messages match upstream commit hashes")
146+
parser.add_argument("--vulns-dir", default="../vulns", help="Path to the kernel vulnerabilities repo (default: ../vulns)")
114147
args = parser.parse_args()
115148

116149
upstream_ref = args.upstream_ref
117150

151+
# Set up vulns repo path if CVE checking is enabled
152+
vulns_repo = None
153+
if args.check_cves:
154+
vulns_repo = args.vulns_dir
155+
vulns_repo_url = "https://git.kernel.org/pub/scm/linux/security/vulns.git"
156+
157+
if os.path.exists(vulns_repo):
158+
# Repository exists, update it with git pull
159+
try:
160+
run_git(vulns_repo, ['pull'])
161+
except RuntimeError as e:
162+
print(f"WARNING: Failed to update vulns repo: {e}")
163+
print("Continuing with existing repository...")
164+
else:
165+
# Repository doesn't exist, clone it
166+
try:
167+
result = subprocess.run(['git', 'clone', vulns_repo_url, vulns_repo],
168+
text=True,
169+
capture_output=True,
170+
check=False)
171+
if result.returncode != 0:
172+
print(f"ERROR: Failed to clone vulns repo: {result.stderr}")
173+
sys.exit(1)
174+
except Exception as e:
175+
print(f"ERROR: Failed to clone vulns repo: {e}")
176+
sys.exit(1)
177+
118178
# Validate that all required refs exist before continuing
119179
missing_refs = []
120180
for refname, refval in [('upstream reference', upstream_ref),
@@ -168,8 +228,34 @@ def main():
168228
fixes = find_fixes_in_mainline(args.repo, args.pr_branch, upstream_ref, uhash)
169229
if fixes:
170230
any_findings = True
231+
232+
# Check CVEs for bugfix commits if enabled
233+
fix_cves = {}
234+
if args.check_cves:
235+
for fix_hash, fix_display in fixes:
236+
try:
237+
success, cve_output = run_cve_search(vulns_repo, args.repo, fix_hash)
238+
if success:
239+
# Parse the CVE from the result
240+
match = re.search(r'(CVE-\d{4}-\d+)\s+is assigned to git id', cve_output)
241+
if match:
242+
bugfix_cve = match.group(1)
243+
fix_cves[fix_hash] = bugfix_cve
244+
except (RuntimeError, subprocess.SubprocessError):
245+
# Log a warning instead of silently ignoring errors when checking bugfix CVEs
246+
print(f"Warning: Failed to check CVE for bugfix commit {fix_hash}: {e}", file=sys.stderr)
247+
248+
# Build the fixes display text with CVE info
249+
fixes_lines = []
250+
for fix_hash, display_str in fixes:
251+
if fix_hash in fix_cves:
252+
fixes_lines.append(f"{display_str} ({fix_cves[fix_hash]})")
253+
else:
254+
fixes_lines.append(display_str)
255+
fixes_text = "\n".join(fixes_lines)
256+
171257
if args.markdown:
172-
fixes_block = " " + fixes.replace("\n", "\n ")
258+
fixes_block = " " + fixes_text.replace("\n", "\n ")
173259
out_lines.append(
174260
f"- ⚠️ PR commit `{pr_commit_desc}` references upstream commit \n"
175261
f" `{short_uhash}` which has been referenced by a `Fixes:` tag in the upstream \n"
@@ -185,10 +271,97 @@ def main():
185271
subsequent_indent=' ' * len(prefix)) # spaces for '[FIXES] '
186272
)
187273
out_lines.append("") # blank line after 'Fixes tags:'
188-
for line in fixes.splitlines():
274+
for line in fixes_text.splitlines():
189275
out_lines.append(' ' + line)
190276
out_lines.append("") # blank line
191277

278+
# Check CVE if enabled
279+
if args.check_cves:
280+
cve_id = extract_cve_from_message(msg)
281+
282+
# Check if the upstream commit has a CVE associated with it
283+
try:
284+
success, cve_output = run_cve_search(vulns_repo, args.repo, uhash)
285+
if success:
286+
# Parse the output to get the CVE from the result
287+
# Expected format: "CVE-2024-35962 is assigned to git id 65acf6e0501ac8880a4f73980d01b5d27648b956"
288+
match = re.search(r'(CVE-\d{4}-\d+)\s+is assigned to git id', cve_output)
289+
if match:
290+
found_cve = match.group(1)
291+
292+
if cve_id:
293+
# PR commit has a CVE reference - check if it matches
294+
if found_cve != cve_id:
295+
any_findings = True
296+
if args.markdown:
297+
out_lines.append(
298+
f"- ❌ PR commit `{pr_commit_desc}` references `{cve_id}` but \n"
299+
f" upstream commit `{short_uhash}` is associated with `{found_cve}`\n"
300+
)
301+
else:
302+
prefix = "[CVE-MISMATCH] "
303+
header = (f"{prefix}PR commit {pr_commit_desc} references {cve_id} but "
304+
f"upstream commit {short_uhash} is associated with {found_cve}")
305+
out_lines.append(
306+
wrap_paragraph(header, width=80, initial_indent='',
307+
subsequent_indent=' ' * len(prefix))
308+
)
309+
out_lines.append("") # blank line
310+
else:
311+
# PR commit doesn't reference a CVE, but upstream has one
312+
any_findings = True
313+
if args.markdown:
314+
out_lines.append(
315+
f"- ⚠️ PR commit `{pr_commit_desc}` does not reference a CVE but \n"
316+
f" upstream commit `{short_uhash}` is associated with `{found_cve}`\n"
317+
)
318+
else:
319+
prefix = "[CVE-MISSING] "
320+
header = (f"{prefix}PR commit {pr_commit_desc} does not reference a CVE but "
321+
f"upstream commit {short_uhash} is associated with {found_cve}")
322+
out_lines.append(
323+
wrap_paragraph(header, width=80, initial_indent='',
324+
subsequent_indent=' ' * len(prefix))
325+
)
326+
out_lines.append("") # blank line
327+
else:
328+
# The upstream commit has no CVE assigned
329+
if cve_id:
330+
# PR commit claims a CVE but upstream has none
331+
any_findings = True
332+
if args.markdown:
333+
out_lines.append(
334+
f"- ❌ PR commit `{pr_commit_desc}` references `{cve_id}` but \n"
335+
f" upstream commit `{short_uhash}` has no CVE assigned\n"
336+
)
337+
else:
338+
prefix = "[CVE-NOTFOUND] "
339+
header = (f"{prefix}PR commit {pr_commit_desc} references {cve_id} but "
340+
f"upstream commit {short_uhash} has no CVE assigned")
341+
out_lines.append(
342+
wrap_paragraph(header, width=80, initial_indent='',
343+
subsequent_indent=' ' * len(prefix))
344+
)
345+
out_lines.append("") # blank line
346+
except (subprocess.SubprocessError, OSError) as e:
347+
# Error running cve_search
348+
if cve_id:
349+
any_findings = True
350+
if args.markdown:
351+
out_lines.append(
352+
f"- ⚠️ PR commit `{pr_commit_desc}` references `{cve_id}` but \n"
353+
f" failed to verify: {str(e)}\n"
354+
)
355+
else:
356+
prefix = "[CVE-ERROR] "
357+
header = (f"{prefix}PR commit {pr_commit_desc} references {cve_id} but "
358+
f"failed to verify: {str(e)}")
359+
out_lines.append(
360+
wrap_paragraph(header, width=80, initial_indent='',
361+
subsequent_indent=' ' * len(prefix))
362+
)
363+
out_lines.append("") # blank line
364+
192365
if any_findings:
193366
if args.markdown:
194367
print("## :mag: Upstream Linux Kernel Commit Check\n")

0 commit comments

Comments
 (0)