55import re
66import sys
77import textwrap
8+ import os
89
910def 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
8789def 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+
107138def 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 Exception :
245+ # Silently ignore errors when checking bugfix CVEs
246+ pass
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 Exception 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