Skip to content

Commit 66d35bd

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 9a5f6f6 commit 66d35bd

File tree

1 file changed

+179
-5
lines changed

1 file changed

+179
-5
lines changed

check_kernel_commits.py

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import re
66
import sys
77
import textwrap
8+
import os
9+
from typing import Optional
810

911
def run_git(repo, args):
1012
"""Run a git command in the given repository and return its output as a string."""
@@ -50,14 +52,15 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
5052
"""
5153
Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive.
5254
Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length.
55+
Returns a list of tuples: (full_hash, display_string)
5356
"""
5457
results = []
5558
# Get all commits with 'Fixes:' in the message
5659
output = run_git(repo, [
5760
'log', upstream_ref, '--grep', 'Fixes:', '-i', '--format=%H %h %s (%an)%x0a%B%x00'
5861
]).strip()
5962
if not output:
60-
return ""
63+
return []
6164
# Each commit is separated by a NUL character and a newline
6265
commits = output.split('\x00\x0a')
6366
# Prepare hash prefixes from 12 down to 6
@@ -78,11 +81,11 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
7881
for prefix in hash_prefixes:
7982
if m.group(1).lower().startswith(prefix.lower()):
8083
if not commit_exists_in_branch(repo, pr_branch, full_hash):
81-
results.append(' '.join(header.split()[1:]))
84+
results.append((full_hash, ' '.join(header.split()[1:])))
8285
break
8386
else:
8487
continue
85-
return "\n".join(results)
88+
return results
8689

8790
def commit_exists_in_branch(repo, pr_branch, upstream_hash_):
8891
"""
@@ -104,17 +107,75 @@ def wrap_paragraph(text, width=80, initial_indent='', subsequent_indent=''):
104107
break_on_hyphens=False)
105108
return wrapper.fill(text)
106109

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

116150
upstream_ref = args.upstream_ref
117151

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

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

0 commit comments

Comments
 (0)