GHSA-98F3-7G2J-289Q

Vulnerability from github – Published: 2026-10-14 12:31 – Updated: 2026-10-14 12:31
VLAI
Details

The insights-runtime-extractor runs as a privileged DaemonSet (hostPID: true) that scans container processes for runtime fingerprinting. When scanning a container, the coordinator:

  1. fork()s a child process
  2. setns() into the target container's PID and mount namespaces
  3. fchdir() back to an orphaned FD pointing at the extractor's own root filesystem (where /crio.sock is hostPath-mounted)
  4. Calls only seteuid(target_uid) — changing the effective UID but leaving the real UID and saved-set-UID at 0 and retaining the permitted capability set
  5. The check_no_privileged_perms() guard inspects only the Effective capability set (so it passes), and is skipped entirely when the target runs as uid 0
  6. The VersionExecutable fingerprint invokes fpr_kind_executable with process.command_line[0] — the target's argv[0], read from /proc/<pid>/cmdline and fully controlled by the workload
  7. fpr_kind_executable calls exec.Command(executable, "--version").Output() — executing the workload's own binary inside the workload's mount namespace while ruid=0 in the host user namespace

The executed process can call setuid(0) to restore full root with CAP_SYS_ADMIN, and inherits the orphaned cwd which resolves into the extractor container's filesystem containing the host CRI-O socket.

Affected Code

1. Insufficient privilege drop (Rust — extractor coordinator)

File: extractor/src/insights_runtime_extractor.rs

// Lines 139-146 — seteuid-only drop, guard skipped for root containers
join_process_namespaces(process.pid)?;
change_dir(current_dir);
switch_user(*process.uid)?;
if *process.uid != 0 {
    perms::check_no_privileged_perms()
        .expect("Must not have privileged permissions to run fingerprints");
}
fingerprint::run_fingerprints(&config, out_dir, &process);
// Lines 182-185 — only effective UID changed; ruid/suid stay 0
fn switch_user(uid: u32) -> Result<(), ScannerError> {
    debug!("... Becoming user: {uid}");
    seteuid(uid.into()).map_err(|e| ScannerError::Errno(e))
}

2. Execution of untrusted binary (Go — fingerprint)

File: fingerprints/pkg/utils/file.go

// Lines 75-80 — executes a path taken verbatim from workload's argv[0]
func GetExecutableVersionOutput(executable string) (string, error) {
    out, err := exec.Command(executable, "--version").Output()
    if err != nil {
        return "", err
    }
    return string(out), nil
}

3. Workload-controlled path forwarded (Rust — version_executable)

File: version_executable.rs

// Lines 29-34 — workload-controlled command_line[0] forwarded to fingerprint
return Some(vec![
    fpr_kind_executable,
    out_dir.to_string(),
    String::from(&process.command_line[0]),
    String::from(&version_executable.runtime_kind_name),
]);

Attack Pattern

Attacker: Any tenant able to schedule a pod (unprivileged, any namespace) Victim: The node running the insights-runtime-extractor DaemonSet Trigger: Automatic — the DaemonSet scans all container processes on the node

Attack sequence:

  1. Attacker creates a pod whose process name matches a configured versioned-executable pattern (node, python, python3, MainThread, or contains java)
  2. The pod's main binary (at the path matching argv[0]) is a malicious executable that calls setuid(0) to restore root
  3. When the insights operator triggers a scan, the DaemonSet forks, enters the attacker's mount namespace, and executes the attacker's binary with ruid=0
  4. The attacker's code regains full root with CAP_SYS_ADMIN in the host user namespace
  5. The inherited cwd contains the host CRI-O socket → full node compromise
Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-104795"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-250"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-10-14T11:16:59Z",
    "severity": "HIGH"
  },
  "details": "The insights-runtime-extractor runs as a privileged DaemonSet (`hostPID: true`)\nthat scans container processes for runtime fingerprinting. When scanning a\ncontainer, the coordinator:\n\n1. **fork()s** a child process\n2. **setns()** into the target container\u0027s PID and mount namespaces\n3. **fchdir()** back to an orphaned FD pointing at the extractor\u0027s own root\n   filesystem (where `/crio.sock` is hostPath-mounted)\n4. Calls **only `seteuid(target_uid)`** \u2014 changing the effective UID but leaving\n   the **real UID and saved-set-UID at 0** and **retaining the permitted\n   capability set**\n5. The `check_no_privileged_perms()` guard inspects only the Effective capability\n   set (so it passes), and is **skipped entirely when the target runs as uid 0**\n6. The `VersionExecutable` fingerprint invokes `fpr_kind_executable` with\n   `process.command_line[0]` \u2014 the target\u0027s argv[0], read from\n   `/proc/\u003cpid\u003e/cmdline` and **fully controlled by the workload**\n7. `fpr_kind_executable` calls `exec.Command(executable, \"--version\").Output()`\n   \u2014 executing the **workload\u0027s own binary** inside the workload\u0027s mount\n   namespace while **ruid=0 in the host user namespace**\n\nThe executed process can call `setuid(0)` to restore full root with\n`CAP_SYS_ADMIN`, and inherits the orphaned cwd which resolves into the extractor\ncontainer\u0027s filesystem containing the host CRI-O socket.\n\n## Affected Code\n\n### 1. Insufficient privilege drop (Rust \u2014 extractor coordinator)\n\n**File:** `extractor/src/insights_runtime_extractor.rs`\n\n```rust\n// Lines 139-146 \u2014 seteuid-only drop, guard skipped for root containers\njoin_process_namespaces(process.pid)?;\nchange_dir(current_dir);\nswitch_user(*process.uid)?;\nif *process.uid != 0 {\n    perms::check_no_privileged_perms()\n        .expect(\"Must not have privileged permissions to run fingerprints\");\n}\nfingerprint::run_fingerprints(\u0026config, out_dir, \u0026process);\n```\n\n```rust\n// Lines 182-185 \u2014 only effective UID changed; ruid/suid stay 0\nfn switch_user(uid: u32) -\u003e Result\u003c(), ScannerError\u003e {\n    debug!(\"... Becoming user: {uid}\");\n    seteuid(uid.into()).map_err(|e| ScannerError::Errno(e))\n}\n```\n\n### 2. Execution of untrusted binary (Go \u2014 fingerprint)\n\n**File:** `fingerprints/pkg/utils/file.go`\n\n```go\n// Lines 75-80 \u2014 executes a path taken verbatim from workload\u0027s argv[0]\nfunc GetExecutableVersionOutput(executable string) (string, error) {\n    out, err := exec.Command(executable, \"--version\").Output()\n    if err != nil {\n        return \"\", err\n    }\n    return string(out), nil\n}\n```\n\n### 3. Workload-controlled path forwarded (Rust \u2014 version_executable)\n\n**File:** `version_executable.rs`\n\n```rust\n// Lines 29-34 \u2014 workload-controlled command_line[0] forwarded to fingerprint\nreturn Some(vec![\n    fpr_kind_executable,\n    out_dir.to_string(),\n    String::from(\u0026process.command_line[0]),\n    String::from(\u0026version_executable.runtime_kind_name),\n]);\n```\n\n## Attack Pattern\n\n**Attacker:** Any tenant able to schedule a pod (unprivileged, any namespace)\n**Victim:** The node running the insights-runtime-extractor DaemonSet\n**Trigger:** Automatic \u2014 the DaemonSet scans all container processes on the node\n\n**Attack sequence:**\n\n1. Attacker creates a pod whose process name matches a configured\n   versioned-executable pattern (`node`, `python`, `python3`, `MainThread`,\n   or contains `java`)\n2. The pod\u0027s main binary (at the path matching argv[0]) is a malicious\n   executable that calls `setuid(0)` to restore root\n3. When the insights operator triggers a scan, the DaemonSet forks, enters the\n   attacker\u0027s mount namespace, and executes the attacker\u0027s binary with ruid=0\n4. The attacker\u0027s code regains full root with `CAP_SYS_ADMIN` in the host user\n   namespace\n5. The inherited cwd contains the host CRI-O socket \u2192 full node compromise",
  "id": "GHSA-98f3-7g2j-289q",
  "modified": "2026-10-14T12:31:33Z",
  "published": "2026-10-14T12:31:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104795"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/CVE-2026-104795"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2543917"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.