.agents/skills/security-audit/references/skia-cve-resolution.md
Skia is the product, not just a dependency. If a CVE exists in libpng or freetype, we simply bump past it. For Skia itself, we need fine-grained resolution: find the actual fix commit, verify which branches contain it, test cherry-pick feasibility, and assess whether the vulnerable code is reachable through the SkiaSharp C API.
Every Skia CVE must be resolved to a fix commit. Classification by milestone alone is only the first pass — it is NEVER a final answer. A CVE without a verified fix commit is an INCOMPLETE finding.
mono/skia diverges from upstream google/skia with the SkiaSharp
C API and other patches, so "upstream fixed it" doesn't necessarily mean "we have the fix".NVD CVE
→ Bug URL (issues.chromium.org/issues/NNNNN)
→ Bug ID
→ Fetch upstream chrome/mNNN branches
→ git log --grep="<bug_id>"
→ Fix commit SHA
→ Branch ancestry check (our milestone? our tree?)
→ Cherry-pick test (--no-commit)
→ Reachability assessment (C API / build flags)
→ Structured finding
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=Skia&resultsPerPage=200"
No API key is required, but the rate limit is 5 requests per 30 seconds without one.
🛑 Process ALL CVEs returned by the query. Do not filter by keywords in the description (e.g., "print file", "extension"). The
keywordSearch=Skiaquery already filters to Skia-related CVEs. Sub-filtering by description keywords will miss CVEs like CVE-2026-6364 whose description mentions "print file" but is actually about SKP lattice validation.
🛑 Scope: Recent CVEs (last 2 years). From the full NVD results, process every CVE with a 2024, 2025, or 2026 ID (or whatever the current and prior 2 years are). Older CVEs are historical and already addressed. This typically yields 15-25 CVEs. If you find fewer than 10 recent Skia CVEs, something is wrong — re-check your query and pagination.
After the NVD query, compare results against the Chrome Releases data (from Step 1.5 of the
main workflow, cached in output/ai/chrome-releases-cache.json):
The Chrome Releases blog provides the bug ID (issues.chromium.org/issues/NNNNN) directly
in the CVE listing. This is the same bug ID needed for Step 4 (commit resolution). Using it
from Chrome Releases can save the NVD reference URL parsing step when available.
🛑 Every processed CVE must appear in the final JSON report — as
affected,already_fixed,false_positive, orneeds_review. Do NOT silently drop CVEs that are "likely fixed" by milestone ordering. They must be verified and reported.
🛑 All Skia CVEs go in ONE finding object. In the final report JSON, there must be exactly ONE finding with
"dependency": "skia". Put ALL CVEs (affected, already_fixed, false_positive) in that single finding'scves[]array. Use each CVE'sassessmentfield to distinguish status. Do NOT split Skia into multiple findings by milestone or category.
For each CVE returned, extract:
CVE ID and description
Severity — CVSS score from metrics.cvssMetricV31
Chrome fix version — from configurations[].nodes[].cpeMatch[] where criteria
contains chrome and versionEndExcluding exists
Chrome fix milestone — major version from versionEndExcluding (e.g., 132.0.6834.83 → 132)
Bug ID — from references[], look for issues.chromium.org/issues/NNNNN:
bug_id = None
for ref in cve['references']:
if 'issues.chromium.org/issues/' in ref['url']:
bug_id = ref['url'].rstrip('/').split('/')[-1]
The bug ID is the critical link between the NVD CVE and the actual fix commit in the Skia git repo. Every Chrome-reported Skia CVE has one. If a CVE has no bug URL, flag it for manual investigation but do NOT skip it.
Use the verified milestone from SkMilestone.h (not cgmanifest.json):
| Condition | First-pass classification |
|---|---|
| Fix milestone > our milestone | 🔴 Potentially affected — proceed to commit resolution |
| Fix milestone ≤ our milestone | 🟡 Likely fixed — verify with commit ancestry then classify as ⚪ false positive or 🔴 affected |
No Chrome version, mentions SkiaRenderEngine | ⚪ Not applicable — Android render engine, not Skia library |
| No Chrome version, reported via Android/Huawei/vendor bulletin | ⚠️ Code-path verification needed (see below) |
| No Chrome version, other | ⚠️ Manual review — check fork for affected code |
| CVSS score not yet published | Use Chromium severity (High → treat as HIGH ~8.8) |
🛑 ALL CVEs must appear in the final report. Do NOT drop CVEs from the report because they are "likely fixed." Every CVE — regardless of classification — must be carried through commit resolution and included in the JSON output (as
affected,false_positive, oralready_fixed). Silently dropping CVEs is the #1 cause of incomplete audits.
🛑 EVERY CVE classified as 🔴 or 🟡 must proceed through commit resolution below. For 🟡 (fix milestone ≤ ours): verify the fix commit IS in our tree with
git merge-base --is-ancestor. If confirmed, classify asfalse_positivewith evidence. If NOT in our tree (fork diverged before the fix), reclassify as 🔴 affected.
💡 Milestone ordering hint: If a CVE says "fixed in Chrome N" and N ≤ our milestone, the fix SHOULD be in our tree (because m147 includes all of m146). If you can't find the commit in Skia's git, it likely means the fix is in Chrome's integration layer (
chromium/src), not in the Skia library itself. See Step 4 fallback #3 (Chromium Gerrit search).
cd externals/skia
git remote add upstream https://github.com/google/skia.git 2>/dev/null || true
# Full fetch — simple and reliable. Do NOT use --depth or --deepen.
git fetch upstream chrome/m${OUR_MILESTONE}
git fetch upstream chrome/m${OUR_MILESTONE+1}
Fetching is read-only and does not modify any tracked files. Full fetches are necessary because fix commits may be deep in branch history, not just at the tip as cherry-picks.
# Search the fix milestone branch for the bug ID
git log upstream/chrome/m${FIX_MILESTONE} --oneline --grep="<BUG_ID>"
# Bug IDs in commit messages appear as any of:
# Bug: NNNNNNNNN
# Bug: b/NNNNNNNNN
# Bug: chromium:NNNNNNNNN
# A plain --grep="NNNNNNNNN" matches all three.
If grep returns empty:
m${OUR_MILESTONE+2}, etc.).git log --all --grep="<BUG_ID>" after fetching more branches.chromium/src (Chrome's integration
layer), not the standalone Skia library:
# Query Chromium's code review system for the bug ID
curl -s "https://chromium-review.googlesource.com/changes/?q=bug:<BUG_ID>&n=5&o=CURRENT_REVISION" \
| tail -n +2 | python3 -c "
import json, sys
changes = json.load(sys.stdin)
for c in changes:
rev = list(c.get('revisions', {}).keys())[0] if c.get('revisions') else 'unknown'
print(f\" {c['project']} CL#{c['_number']} {c['subject']} commit={rev[:12]}\")
"
project: "chromium/src", fetch the commit details:
curl -s "https://chromium.googlesource.com/chromium/src/+/<COMMIT_SHA>?format=JSON" \
| tail -n +2 | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f\"Subject: {data['message'].splitlines()[0]}\")
for f in data.get('tree_diff', []):
print(f\" {f['new_path']}\")
"
skia/ext/ → Chrome integration layer (see
Chrome Integration Layer False Positives below)third_party/skia/ → the fix IS in Skia, search
harder in the Skia repo (it may have landed under a different bug ID)chromium/src → not Skia at all--all, and Chromium Gerrit)
may you mark a CVE as "commit not found" — and you MUST include a detailed note
explaining every search attempted.For each fix commit, determine where it lives:
COMMIT_SHA=<sha from previous step>
# Is it on our milestone's upstream branch? (best case — Google backported)
git merge-base --is-ancestor "$COMMIT_SHA" upstream/chrome/m${OUR_MILESTONE} \
&& echo "ON_OUR_MILESTONE" || echo "NOT_ON_OUR_MILESTONE"
# Is it in our actual fork tree?
git merge-base --is-ancestor "$COMMIT_SHA" HEAD \
&& echo "IN_OUR_TREE" || echo "NOT_IN_OUR_TREE"
On upstream/chrome/m${OUR} | In our tree (HEAD) | Status |
|---|---|---|
| YES | YES | ✅ Already fixed |
| YES | NO | 🔴 VULNERABLE — upstream has backport for our milestone but our fork doesn't |
| NO | NO | 🔴 VULNERABLE — fix only on newer milestone, needs cherry-pick |
| NO | YES | ✅ Fixed via fork-specific patch (rare; verify carefully) |
For EVERY commit classified as VULNERABLE:
git cherry-pick --no-commit "$COMMIT_SHA"
if [ $? -eq 0 ]; then
echo "APPLIES_CLEANLY"
git checkout -- . && git clean -fd --quiet
else
echo "CONFLICTS"
git cherry-pick --abort
fi
Record cherryPicksCleanly: true | false in the report.
Check whether the affected code is reachable from SkiaSharp:
# Files modified by the fix
git diff-tree --no-commit-id --name-only -r "$COMMIT_SHA"
# Check the C API for references to affected functions/classes
grep -r "<relevant_symbol>" src/c/ include/c/
# Check that the file is compiled into our library
grep "<filename>" gn/*.gni BUILD.gn
| Category | Meaning |
|---|---|
| REACHABLE | Code is in the C API or called by C API functions (SkRuntimeEffect, SkRegion, SkPath, SkMaskFilter, etc.) |
| COMPILED_NOT_EXPOSED | Code is in the library binary but not callable from SkiaSharp C API (e.g., include/private/chromium/). Still include as defense-in-depth. |
| NOT_REACHABLE | Code is behind a compile flag we don't use (Graphite, Vulkan, Dawn) |
CVEs reported via Android Security Bulletins, Huawei HarmonyOS bulletins, or other vendor
bulletins reference Skia code that may also exist in upstream google/skia and therefore
in our fork. Vendor forks diverged from the same upstream, so shared code paths are common.
Do NOT dismiss a CVE just because it was reported through a vendor bulletin. Verify:
cd externals/skia
# Check if the vulnerable file exists
find . -name "TheVulnerableFile.cpp"
# Check if the vulnerable function exists
git grep "vulnerable_function_name"
# If a fix commit is referenced (e.g., from AOSP), check ancestry against HEAD
git merge-base --is-ancestor "<fix_commit>" HEAD && echo "FIXED" || echo "VULNERABLE"
| Result | Status |
|---|---|
| Vulnerable file/function does NOT exist in our fork | ⚪ False positive — vendor-specific code |
| Vulnerable code EXISTS + fix commit is ancestor of HEAD | ✅ Already fixed |
| Vulnerable code EXISTS + NOT fixed | 🔴 Needs attention |
| Pattern | Why it's a false positive |
|---|---|
SkiaRenderEngine.cpp (Android) | Android OS rendering infrastructure, not the Skia library itself. Always a false positive. |
AOSP platform/external/skia paths not in upstream | Android-specific code in AOSP's fork that doesn't exist in google/skia. Verify by file/function existence (above). |
| Chrome-only rendering paths (HTML Canvas, SVG-in-browser) | May not be reachable through the SkiaSharp C API. Verify with reachability check. |
| NVD version range errors | NVD ranges are sometimes wrong — verify with commit ancestry. If our tree contains the fix commit, classify as ⚪ False positive (NVD version range incorrect) and cite the commit. |
chromium/src/skia/ext/ (Chrome integration layer) | See below. |
<a id="chrome-integration-layer"></a>
Chrome has its own Skia extension code under chromium/src/skia/ext/ (image_operations,
convolver, benchmarking utilities). This is NOT part of the standalone Skia library —
it's Chrome-specific wrapper code that uses Skia types.
How to identify: The fix commit is in chromium/src and modifies files under skia/ext/.
Why it's a false positive for SkiaSharp:
skia.googlesource.com/skia)skia/ext/ directory does NOT exist in the Skia library — it's Chrome-specificexternals/skia/ has no ext/ directoryHowever, you SHOULD still inspect the fix to determine if the underlying Skia code
being called has the same vulnerability. For example, if Chrome's skia/ext/image_operations.cc
adds an overflow check before calling SkBitmap::tryAllocPixels(), ask: "Does the same
overflow exist when SkiaSharp calls tryAllocPixels directly?"
Assessment template for Chrome integration layer CVEs:
assessment: "false_positive"
fixCommitNote: "Fix is in chromium/src/skia/ext/ (Chrome integration layer, CL#NNNNN).
This code does not exist in the standalone Skia library. Inspected the fix — it adds
overflow checks for [specific scenario]. Verified that SkiaSharp's equivalent code path
[is/is not] vulnerable to the same overflow because [reason]."
reachability: "NOT_REACHABLE"
reachabilityNote: "chromium/src/skia/ext/ is Chrome-specific code not compiled into SkiaSharp"
| Field | Required | Source |
|---|---|---|
| CVE ID | ✅ | NVD |
| Description | ✅ | NVD |
| Bug ID | ✅ | NVD reference URL |
| Bug URL | ✅ | https://issues.chromium.org/issues/{bug_id} |
| Fix commit SHA | ✅ | git log --grep |
| Fix commit title | ✅ | git log --format |
| Files modified | ✅ | git diff-tree --name-only |
On upstream/chrome/m${OUR} | ✅ | git merge-base --is-ancestor |
| In our tree (HEAD) | ✅ | git merge-base --is-ancestor |
| Cherry-picks cleanly | ✅ | git cherry-pick --no-commit test |
| Reachability | ✅ | C API + .gni check (REACHABLE / COMPILED_NOT_EXPOSED / NOT_REACHABLE) |
| CVSS score | ✅ | NVD (or Chromium severity if pending) |
| Fix milestone | ✅ | NVD versionEndExcluding |
🛑 Never report a Skia CVE as just "potentially affected" without resolving it to a commit. The bug ID → commit → branch verification pipeline is MANDATORY for every single CVE.