From b00ac4e34626eec9af5cf8f4f0a034103628d9bd Mon Sep 17 00:00:00 2001 From: Titouan Christophe Date: Fri, 3 Jul 2026 10:31:36 +0200 Subject: [PATCH] utils/checkpackagelib: add new check MissingCVEPatch To indicate that a patch fixes a vulnerability in Buildroot, the convention is: 1. In the patch file, add a tag 'CVE: ' 2. In .mk, and an entry to _IGNORE_CVES, and add a comment above that new entry to reference the patch file(s) However, as packages get bumped and their patches are added, removed or rebased; it happens that IGNORE_CVES get outdated. One important issue is marking a CVE as ignored, while the corresponding patch is not in Buildroot. To detect such cases, add a new checker to checkpackagelib that finds occurences of: # 000x-some-patch.patch PKG_IGNORE_CVES += CVE-XXXX-YYYY For each one of them, ensure that the mentioned patch files actually exist and contain the `CVE: ...` tag. Assisted-by: Claude:claude-opus-4.8 Signed-off-by: Titouan Christophe Signed-off-by: Thomas Petazzoni --- utils/checkpackagelib/lib_mk.py | 42 ++++++++++++++++++++ utils/checkpackagelib/test_lib_mk.py | 57 ++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/utils/checkpackagelib/lib_mk.py b/utils/checkpackagelib/lib_mk.py index 5d48e8929c..eb5fbf474c 100644 --- a/utils/checkpackagelib/lib_mk.py +++ b/utils/checkpackagelib/lib_mk.py @@ -6,6 +6,7 @@ import os import re +from pathlib import Path from checkpackagelib.base import _CheckFunction from checkpackagelib.lib import ConsecutiveEmptyLines # noqa: F401 @@ -117,6 +118,47 @@ class Indent(_CheckFunction): text] +class MissingCVEPatch(_CheckFunction): + PATCH_COMMENT = re.compile(r"^#\s*(\S+\.patch)\s*$") + IGNORE_CVES = re.compile(r"^[A-Z0-9_]+_IGNORE_CVES\s*\+?=") + CVE_TAG_IN_PATCH = re.compile(r"^CVE: *CVE-\d+-\d+$") + + def before(self): + self.pending_patches = [] + self.package_dir = Path(self.filename).parent + + def check_patch_files(self): + for patch_lineno, patch_name, patch_text in self.pending_patches: + patch_file = self.package_dir / patch_name + self.pending_patch = None + + if not patch_file.is_file(): + return ["{}:{}: patch file '{}' mentioned for ignored CVEs is missing" + .format(self.filename, patch_lineno, patch_name), + patch_text] + + if not any(map(self.CVE_TAG_IN_PATCH.match, patch_file.open())): + return ["{}: patch file '{}' is missing 'CVE:' tag" + .format(self.filename, patch_name), + patch_text] + + def check_line(self, lineno, text): + m = self.PATCH_COMMENT.match(text.rstrip()) + if m: + self.pending_patches.append((lineno, m.group(1), text)) + return + + # other comments do not break the association between the patch + # comment and the following _IGNORE_CVES assignment + if text.lstrip().startswith("#"): + return + + if self.IGNORE_CVES.search(text): + return self.check_patch_files() + + self.pending_patches = [] + + class OverriddenVariable(_CheckFunction): CONCATENATING = re.compile(r"^([A-Z0-9_]+)\s*(\+|:|)=\s*\$\(\1\)") END_CONDITIONAL = re.compile(r"^\s*({})".format("|".join(end_conditional))) diff --git a/utils/checkpackagelib/test_lib_mk.py b/utils/checkpackagelib/test_lib_mk.py index 3f97c25b2d..21effa9489 100644 --- a/utils/checkpackagelib/test_lib_mk.py +++ b/utils/checkpackagelib/test_lib_mk.py @@ -197,6 +197,63 @@ def test_Indent(testname, filename, string, expected): assert warnings == expected +MissingCVEPatch = [ + ('patches present', + {'0001-some-fix.patch': 'CVE: CVE-2000-1234', '0002-other-fix.patch': 'CVE: CVE-2000-1234'}, + '# 0001-some-fix.patch\n' + '# 0002-other-fix.patch\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + []), + ('patch missing', + {'0002-other-fix.patch': 'CVE: CVE-2000-1234'}, + '# 0001-some-fix.patch\n' + '# 0002-other-fix.patch\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + [['{}:1: patch file \'0001-some-fix.patch\' mentioned for ignored CVEs is missing', + '# 0001-some-fix.patch\n']]), + ('CVE tag missing', + {'0001-some-fix.patch': ''}, + '# 0001-some-fix.patch\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + [['{}: patch file \'0001-some-fix.patch\' is missing \'CVE:\' tag', + '# 0001-some-fix.patch\n']]), + ('patch in version subdir', + {'1.0/0001-some-fix.patch': 'CVE: CVE-2000-1234'}, + '# 1.0/0001-some-fix.patch\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + []), + ('comments between patch and ignore', + {}, + '# 0001-some-fix.patch\n' + '# this entry is not stale\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + [['{}:1: patch file \'0001-some-fix.patch\' mentioned for ignored CVEs is missing', + '# 0001-some-fix.patch\n']]), + ('comment not followed by ignore', + {}, + '# 0001-some-fix.patch\n' + 'FOO_DEPENDENCIES = host-foo\n' + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + []), + ('ignore without patch comment', + {}, + 'FOO_IGNORE_CVES += CVE-2000-1234\n', + []), + ] + + +@pytest.mark.parametrize('testname,patches,string,expected', MissingCVEPatch) +def test_MissingCVEPatch(testname, patches, string, expected, tmp_path): + filename = str(tmp_path / 'foo.mk') + for patch_name, content in patches.items(): + patch_path = tmp_path / patch_name + patch_path.parent.mkdir(parents=True, exist_ok=True) + patch_path.write_text(content) + warnings = util.check_file(m.MissingCVEPatch, filename, string) + expected = [[w[0].format(filename)] + w[1:] for w in expected] + assert warnings == expected + + OverriddenVariable = [ ('simple assignment', 'any.mk',