Back to Skiasharp

Skia Core CVE Resolution

.agents/skills/security-audit/references/skia-cve-resolution.md

4.148.016.2 KB
Original Source

Skia Core CVE Resolution

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.

Why Skia is Different

  • Invisible to Component Governance. CG scans build-time dependencies (Docker, npm, NuGet) and third-party libraries vendored via DEPS, but the Skia source tree itself is not in any CG manifest. The NVD query is the only automated way to detect Skia CVEs.
  • We carry a fork. 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".
  • Reachability matters. A Skia CVE might affect code that isn't exposed through our C API, or code that's compiled out by our build flags (Graphite, Vulkan, Dawn). This affects prioritization.

Pipeline (run for EVERY Skia CVE)

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

Step 1: Query NVD for Skia CVEs

bash
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=Skia query 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.

Cross-reference with Chrome Releases blog

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):

  • CVEs in both NVD and Chrome Releases: Normal. Use NVD for CVSS score, Chrome Releases for the fix milestone and bug ID (may be more reliable than NVD references).
  • CVEs in Chrome Releases but NOT in NVD: Early disclosures. NVD may lag by days/weeks. Use Chrome's severity rating (Critical → ~9.8, High → ~8.8, Medium → ~6.5, Low → ~3.3).
  • CVEs in NVD but NOT in Chrome Releases: Likely vendor-bulletin CVEs (Android, Huawei). These are filed against Skia code paths in vendor forks — see Non-Chrome Skia CVEs.

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, or needs_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's cves[] array. Use each CVE's assessment field to distinguish status. Do NOT split Skia into multiple findings by milestone or category.

For each CVE returned, extract:

  1. CVE ID and description

  2. Severity — CVSS score from metrics.cvssMetricV31

  3. Chrome fix version — from configurations[].nodes[].cpeMatch[] where criteria contains chrome and versionEndExcluding exists

  4. Chrome fix milestone — major version from versionEndExcluding (e.g., 132.0.6834.83132)

  5. Bug ID — from references[], look for issues.chromium.org/issues/NNNNN:

    python
    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.

Step 2: First-Pass Classification (by milestone)

Use the verified milestone from SkMilestone.h (not cgmanifest.json):

ConditionFirst-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 SkiaRenderEngineNot 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 publishedUse 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, or already_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 as false_positive with 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).

Step 3: Fetch Upstream Branches

bash
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.

Step 4: Resolve Bug ID → Fix Commit

bash
# 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:

  1. Try the next milestone branch (m${OUR_MILESTONE+2}, etc.).
  2. Try git log --all --grep="<BUG_ID>" after fetching more branches.
  3. Search Chromium's Gerrit — the fix may be in chromium/src (Chrome's integration layer), not the standalone Skia library:
    bash
    # 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]}\")
    "
    
    If the result shows project: "chromium/src", fetch the commit details:
    bash
    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']}\")
    "
    
  4. Classify the result:
    • If modified files are under skia/ext/Chrome integration layer (see Chrome Integration Layer False Positives below)
    • If modified files are under third_party/skia/ → the fix IS in Skia, search harder in the Skia repo (it may have landed under a different bug ID)
    • If modified files are elsewhere in chromium/src → not Skia at all
  5. Only after exhausting ALL of the above (Skia branches, --all, and Chromium Gerrit) may you mark a CVE as "commit not found" — and you MUST include a detailed note explaining every search attempted.

Step 5: Branch Ancestry Verification

For each fix commit, determine where it lives:

bash
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
YESYES✅ Already fixed
YESNO🔴 VULNERABLE — upstream has backport for our milestone but our fork doesn't
NONO🔴 VULNERABLE — fix only on newer milestone, needs cherry-pick
NOYES✅ Fixed via fork-specific patch (rare; verify carefully)

Step 6: Cherry-Pick Feasibility Test

For EVERY commit classified as VULNERABLE:

bash
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.

Step 7: Reachability Assessment

Check whether the affected code is reachable from SkiaSharp:

bash
# 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
CategoryMeaning
REACHABLECode is in the C API or called by C API functions (SkRuntimeEffect, SkRegion, SkPath, SkMaskFilter, etc.)
COMPILED_NOT_EXPOSEDCode is in the library binary but not callable from SkiaSharp C API (e.g., include/private/chromium/). Still include as defense-in-depth.
NOT_REACHABLECode is behind a compile flag we don't use (Graphite, Vulkan, Dawn)

Non-Chrome Skia CVEs (Android / Vendor Bulletins)

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:

bash
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"
ResultStatus
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

Skia-Specific False Positives

PatternWhy 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 upstreamAndroid-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 errorsNVD 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.

Chrome Integration Layer

<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:

  • SkiaSharp uses the standalone Skia library (skia.googlesource.com/skia)
  • The skia/ext/ directory does NOT exist in the Skia library — it's Chrome-specific
  • Our submodule at externals/skia/ has no ext/ directory
  • These files are never compiled into SkiaSharp's native binary

However, 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"

Required Output Fields (per Skia CVE)

FieldRequiredSource
CVE IDNVD
DescriptionNVD
Bug IDNVD reference URL
Bug URLhttps://issues.chromium.org/issues/{bug_id}
Fix commit SHAgit log --grep
Fix commit titlegit log --format
Files modifiedgit 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 cleanlygit cherry-pick --no-commit test
ReachabilityC API + .gni check (REACHABLE / COMPILED_NOT_EXPOSED / NOT_REACHABLE)
CVSS scoreNVD (or Chromium severity if pending)
Fix milestoneNVD 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.