high Threat analysis

Linux Copy Fail CVE-2026-31431: KEV Privilege Escalation on Shared Build Hosts

CISA added Linux kernel CVE-2026-31431 to KEV on 2026-05-01. Theori's Copy Fail research ties the bug to AF_ALG AEAD in-place operation and shows why shared CI runners, Kubernetes nodes, and multi-tenant Linux hosts need kernel patch proof or AF_ALG mitigation.

#linux#kernel#cisa-kev#zero-day#ci-cd
On this page 0% read

    Executive Summary

    CISA added CVE-2026-31431 to KEV on 2026-05-01 with a due date of 2026-05-15 CISA KEV. CISA describes it as a Linux kernel incorrect resource transfer vulnerability that can allow privilege escalation CISA KEV.

    Theori’s Copy Fail research explains the root cause in the Linux crypto subsystem: an AF_ALG AEAD in-place optimization could place page-cache pages into a writable scatterlist, enabling an unprivileged write primitive against read-only file mappings under the right conditions Theori. The upstream fix is commit a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5, which removes the in-place behavior in algif_aead Linux commit.

    Key Facts

    cve: "CVE-2026-31431"
    product: "Linux kernel"
    kev_added: "2026-05-01"
    kev_due: "2026-05-15"
    cwe: "CWE-669"
    component: "AF_ALG AEAD / algif_aead"
    fix_commit: "a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5"
    introducing_commit_reference: "72548b093ee3"
    impact:
      - "local privilege escalation"
      - "shared CI runner escalation"
      - "Kubernetes node and container boundary risk where kernel exposure exists"
    temporary_mitigation:
      - "patch the distribution kernel"
      - "block AF_ALG socket creation through sandbox policy where feasible"
      - "disable algif_aead module where operationally safe"

    Source Confidence & Evidence Mapping

    • confirmed: CISA KEV lists CVE-2026-31431 as known exploited CISA KEV.
    • confirmed: Linux CVE announcement identifies the issue and points operators at stable Linux update guidance Linux CVE announcement.
    • confirmed: Theori’s Copy Fail post explains the AF_ALG, splice, scatterlist, and page-cache primitive and lists the coordinated disclosure timeline Theori.
    • confirmed: The upstream Linux fix commit removes the AEAD in-place operation path Linux commit.
    • confirmed: NVD tracks CVE-2026-31431 with the same affected product and references NVD.
    • unknown: Public sources reviewed here do not provide a reliable exploitation victim list or a universal affected-version matrix for every distribution kernel backport.

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decision
    Confirmed compromiseLocal exploit execution, suspicious AF_ALG AEAD use, unexpected setuid/root file changes, or container-to-host escalation evidence appears on a vulnerable kernel.Process, syscall, EDR, auditd, file integrity, package, and kernel version evidence.Isolate the host, preserve volatile evidence, rebuild from trusted media if root compromise is plausible.
    Presumed exposedShared Linux host, CI runner, Kubernetes node, or developer box has untrusted local code execution and lacks patched-kernel proof.Kernel package version, distro advisory, workload schedule, runner logs, and user/container access records.Patch or cordon/rebuild the host; rotate credentials available to jobs or containers on the node.
    Potentially exposedLinux assets exist but kernel build, AF_ALG exposure, or untrusted-code execution path is incomplete.Asset inventory, package manager output, kernel config, module status, seccomp profile, workload mapping.Collect kernel and workload evidence before narrowing scope.
    Not exposedPatched kernel or vendor backport is proven and no vulnerable local execution window exists.Distro advisory match, package changelog, booted kernel version, and negative workload review.Preserve closure evidence.
    UnknownKernel package, booted kernel, workload placement, or audit telemetry is unavailable.Named telemetry gap with owner and retention window.Keep shared hosts and credentials in scope until evidence is recovered.

    Timeline

    • 2026-03-23: Theori reports the vulnerability to the Linux kernel security team Theori.
    • 2026-04-01: Theori’s timeline records mainline patch commit activity Theori.
    • 2026-04-22: Linux CVE announcement for CVE-2026-31431 is published Linux CVE announcement.
    • 2026-05-01: CISA adds CVE-2026-31431 to KEV CISA KEV.
    • 2026-06-01: This Halting Problems refresh found no existing local post for CVE-2026-31431 or Copy Fail.

    Technical Analysis

    Copy Fail matters for supply-chain and CI/CD response because local privilege escalation on shared build hosts can turn a compromised project, malicious package install, or untrusted pull-request job into host-level control. The vulnerable surface involves AF_ALG AEAD and splice() interactions with page-cache-backed scatterlists Theori. A runner that executes attacker-controlled build scripts and shares a kernel with secrets, caches, sibling jobs, or Kubernetes node credentials should be treated as high-value.

    The upstream fix commit states that there is no benefit in operating in-place in algif_aead because source and destination come from different mappings, and it changes the request setup to use out-of-place operation Linux commit. For defenders, patched distribution kernel evidence is more useful than raw upstream version comparison because vendors backport security fixes.

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-linux-copy-fail-cve-2026-31431-kev-scope"))
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.cisa.gov",
      "lore.kernel.org",
      "nvd.nist.gov",
    ]
    URLS = [
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://xint.io/blog/copy-fail-linux-distributions",
      "https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5",
      "https://lore.kernel.org/linux-cve-announce/2026042214-CVE-2026-31431-3d65@gregkh/",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-31431",
    ]
    IPS = [
    ]
    HASHES = [
      "a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5",
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Downstream Abuse Audits

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-linux-copy-fail-cve-2026-31431-kev-github-audit"))
    
    SELECTORS = [
      "www.cisa.gov",
      "lore.kernel.org",
      "nvd.nist.gov",
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://xint.io/blog/copy-fail-linux-distributions",
      "https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5",
      "https://lore.kernel.org/linux-cve-announce/2026042214-CVE-2026-31431-3d65@gregkh/",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-31431",
      "a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
    
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
    
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
    
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
    
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
    
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-linux-copy-fail-cve-2026-31431-kev-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
    
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
    
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-01T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-linux-copy-fail-cve-2026-31431-kev-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Remediation and Closure

    Patch the distribution kernel and reboot into the patched kernel. Where immediate patching is not feasible, block AF_ALG socket creation through sandbox policy or disable algif_aead if workloads do not require it. Closure requires patched booted-kernel proof, host workload mapping, negative file-integrity review, and downstream credential decisions for affected jobs and pods.

    Sources

    1. CISA Known Exploited Vulnerabilities catalog JSON
    2. Linux CVE announcement for CVE-2026-31431
    3. Theori Xint: Copy Fail
    4. Linux fix commit a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
    5. NVD CVE-2026-31431

    IOC Clipboard

    9 IOCs
    Defang IOCs
    domain www.cisa.gov www[.]cisa[.]gov
    domain lore.kernel.org lore[.]kernel[.]org
    domain nvd.nist.gov nvd[.]nist[.]gov
    url https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json hxxps://www[.]cisa[.]gov/sites/default/files/feeds/known_exploited_vulnerabilities[.]json
    url https://xint.io/blog/copy-fail-linux-distributions hxxps://xint[.]io/blog/copy-fail-linux-distributions
    url https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 hxxps://github[.]com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
    url https://lore.kernel.org/linux-cve-announce/2026042214-CVE-2026-31431-3d65@gregkh/ hxxps://lore[.]kernel[.]org/linux-cve-announce/2026042214-CVE-2026-31431-3d65@gregkh/
    url https://nvd.nist.gov/vuln/detail/CVE-2026-31431 hxxps://nvd[.]nist[.]gov/vuln/detail/CVE-2026-31431
    hash a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5