See how kata containers compares to other vendors in security performance
Kata Containers is an open source project focusing on a standard implementation of lightweight Virtual Machines (VMs) that perform like containers. Prior to version 4.0.0, kata-runtime is vulnerable to host code execution via an unvalidated configuration path annotation. The runtime accepts an arbitrary io.katacontainers.configpath pod annotation and loads the referenced host TOML file without restriction. As a result, a pod user who can place a file at a host-visible path can supply a configuration that selects an attacker-controlled hypervisor or virtio-fs daemon binary, executing code as root on the host. This issue is fixed in version 4.0.0.
Summary
In the runtime-rs standalone virtio-fs path, verified here with QEMU (and verified with Cloud Hypervisor too), Kata Containers runs host virtiofsd as root with:
--sandbox none --seccomp none
If an attacker has root-equivalent execution inside the Kata guest VM, they can send raw FUSE requests directly to the host virtiofsd. With the tested runtime-rs virtio-fs configuration, a raw FUSESYMLINK request whose new symlink name is an absolute host path is honored outside the virtio-fs shared directory.
This lets guest root create host-root owned symlinks in sensitive host paths. The PoC created here will create symlinks in the host /etc/cron.d directory, causing host cron to execute a guest-controlled payload as host root.
Impact: guest root can execute code as host root.
Affected configuration
The verified host used:
/opt/kata/share/defaults/kata-containers/runtime-rs/configuration-qemu-runtime-rs.toml
rootless = false sharedfs = "virtio-fs" virtiofsdaemon = "/opt/kata/libexec/virtiofsd" hypervisorname = "qemu" debugconsoleenabled = false
Pinned upstream references, using Kata Containers main commit 2ffd1538a296cff93a357bfba0dfca747480a1f8:
- runtime-rs standalone virtio-fs adds --sandbox none --seccomp none to the virtiofsd command line. - runtime-rs QEMU leaves rootless mode disabled by default: rootless = false. - The QEMU runtime-rs config template generates an installed config that uses standalone virtio-fs and points runtime-rs at the host virtiofsd binary: sharedfs and virtiofsdaemon. - The runtime-rs Makefile resolves those placeholders to virtio-fs and $(LIBEXECDIR)/virtiofsd. - runtime-rs selects the same standalone virtio-fs implementation whenever sharedfs = "virtio-fs": ShareVirtioFsStandalone.
Details
The guest kernel normally owns the virtio-fs client. A normal guest process will use filesystem syscalls, and the guest kernel will validate the paths, and only then does the kernel send FUSE messages to the host backend.
An attacker with root-equivalent access inside the guest can bypass that guest virtio-fs client. They can access the virtio-fs PCI device, mmap the virtio PCI BAR, recover guest physical addresses from /proc/self/pagemap, and build their own virtqueue from userspace. That queue can submit attacker-built FUSE messages directly to host virtiofsd.
The relevant primitive is FUSESYMLINK. An attacker can send a request whose body contains:
new symlink name: /etc/cron.d/kata-go-escape-cron-<pid> symlink target: /proc/<pid>/root/run/kata-containers/shared/sandboxes/<sid>/ro/passthrough/<sid>/rootfs/tmp/kata-go-escape-payload
The new symlink name is an absolute host path. virtiofsd should reject that request or force it to resolve below the configured --shared-dir. In the tested runtime-rs path, host-root unsandboxed virtiofsd accepts the absolute name, creating a real host symlink under /etc/cron.d.
The attacker can make the symlink target resolve through /proc/<pid>/root/... for a live Kata runtime process whose mount namespace can see the guest-created payload. One matching runtime PID is enough.
When the host cron reads /etc/cron.d, it follows the root-owned symlink, loads the guest-created crontab payload, and executes it as host root.
PoC
shell sudo timeout --foreground --kill-after=10s 600s ctr run --rm \ --runtime /opt/kata/runtime-rs/bin/containerd-shim-kata-v2 \ --runtime-config-path /opt/kata/share/defaults/kata-containers/runtime-rs/configuration-qemu-runtime-rs.toml \ --privileged \ --privileged-without-host-devices \ docker.io/library/kata-go-escape:local \ "$runid"
The container is privileged only to model the post-escape condition where the attacker already has guest-root capabilities. It is not the vulnerability by itself.
Inside the guest, the PoC:
1. Writes a cron payload to guest /tmp/kata-go-escape-payload. 2. Finds the virtio-fs PCI device in guest /sys. 3. Takes over a virtio-fs queue from userspace. 4. Sends FUSEINIT. 5. Discovers the current runtime-rs sandbox under passthrough/. 6. Looks up passthrough/<sid>/rootfs/tmp/kata-go-escape-payload. 7. Sends raw FUSESYMLINK requests where the new symlink names are absolute host paths under /etc/cron.d. 8. Keeps the guest alive while host cron scans.
Example log lines:
[guest] virtio-fs PCI device: /sys/devices/pci0000:00/0000:00:05.0 [res] sandboxid=kata-go-escape-test-1778522686-1539 [res] lookuppatherror=0 path=passthrough/kata-go-escape-test-1778522686-1539/rootfs/tmp/kata-go-escape-payload nodeid=21 [spray] pid=1 err=-2 createdcandidates=1
err=-2 is expected for the symlink spray. virtiofsd can return ENOENT after the side effect because its follow-up lookup is still relative to the export root. The host symlink creation has already happened.
Impact
The PoC proves guest-root to host-root command execution.
Verified host proof:
/run/kata-go-escape.proof
uid=0(root) gid=0(root) groups=0(root) Mon May 11 18:05:01 UTC 2026
The proof file is written in host /run by host cron. It is not written by the guest process and not written by virtiofsd.
An attacker who reaches guest root can therefore cross the Kata isolation boundary and execute commands as host root on affected runtime-rs virtio-fs deployments.
Summary An oversight in the CopyFile policy (and perhaps the CopyFile handler) allows untrusted hosts to write to arbitrary locations inside the guest workload image. This can be used to overwrite binaries inside the guest and exfiltrate data from containers; even those running inside CVMs.
Details Here is the policy that covers CopyFile requests. CopyFileRequest if { print("CopyFileRequest: input.path =", input.path) checkdirectorytraversal(input.path) some regex1 in policydata.requestdefaults.CopyFileRequest regex2 := replace(regex1, "$(sfprefix)", policydata.common.sfprefix) regex3 := replace(regex2, "$(cpath)", policydata.common.cpath) regex4 := replace(regex3, "$(bundle-id)", "[a-z0-9]{64}") print("CopyFileRequest: regex4 =", regex4) regex.match(regex4, input.path) print("CopyFileRequest: true") } This checks that files are being copied to policydata.common.cpath, which is typically set to /run/kata-containers/shared/containers. In other words, you're allowed to copy files to anywhere inside the shared dir.
For reference, here is the CopyFile message. Note that none of the other fields are check in the policy. message CopyFileRequest { // Path is the destination file in the guest. It must be absolute, // canonical and below /run. string path = 1; // FileSize is the expected file size, for security reasons write operations // are made in a temporary file, once it has the expected size, it's moved // to the destination path. int64 filesize = 2; // FileMode is the file mode. uint32 filemode = 3; // DirMode is the mode for the parent directories of destination path. uint32 dirmode = 4; // Uid is the numeric user id. int32 uid = 5; // Gid is the numeric group id. int32 gid = 6; // Offset for the next write operation. int64 offset = 7; // Data to write in the destination file. bytes data = 8; }
In addition to copying files directly, the Kata Agent supports creating symlinks via the CopyFile API. In this case the path is the symlink name and the data field contains the symlink target. Given that the policy only checks the path, an attacker can craft a CopyFile request that results in a symlink going from any location into the shared dir.
PoC The above primitive can be used to to write arbitrary data into container images (pulled in the guest or otherwise). A couple steps are required.
First, identify some target path in the guest image. This could be a binary that will be called by the workload. You could also experiment with overwriting other stuff.
Create a symlink from this binary to the shared dir. The path/link name should be in the shared dir and the data/target should point to the path of the target file inside the container image in the guest fs.
Create a second CopyFile request to copy your data from the host into the symlink you just created in the shared dir. This will then be propagated into the image. You may want to restart the container to ensure that your new binary is invoked.
Impact Anyone who is using the upstream genpolicy implementation and expects it to prevent host access to container images is vulnerable This includes Confidential Containers workloads where the trust model explicitly forbids this type of access. If you have your own policy implementation you may or may not be vulnerable. If you do not care about protecting the image from the host (e.g. you are using unprotected host pull), you are not vulnerable.
This was discovered by @calonso-nv.
A flaw was found in Kata Containers. In configurations utilizing genpolicy for Confidential Containers guest protection, a malicious host operator can exploit insufficient validation of CreateContainer mount and storage rules. This allows them to mount arbitrary container-rootfs paths over sensitive host locations or provision arbitrary content, potentially exposing confidential information or enabling the acceptance of attacker-controlled input.
Kata Containers is an open source project focusing on a standard implementation of lightweight Virtual Machines (VMs) that perform like containers. In versions prior to 3.27.0, an issue in Kata with Cloud Hypervisor allows a user of the container to modify the file system used by the Guest micro VM ultimately achieving arbitrary code execution as root in said VM. The current understanding is this doesn’t impact the security of the Host or of other containers / VMs running on that Host (note that arm64 QEMU lacks NVDIMM read-only support: It is believed that until the upstream QEMU gains this capability, a guest write could reach the image file). Version 3.27.0 patches the issue.
Kata Containers coco-tdx malicious host can circumvent initdata verification
Kata Containers is an open source implementation of lightweight Virtual Machines (VMs) that perform like containers. In versions prior to 4.0.0, the kata-agent is vulnerable to an authorization bypass in confidential-guest memory management. In Confidential Containers (CoCo) deployments, the kata-agent enforces an OPA/Rego-based AgentPolicy that must authorize every ttRPC API call, forming the security boundary that prevents an untrusted host from directing the confidential guest. Two ttRPC methods introduced with the mem-agent feature are missing this authorization check, so an untrusted host can invoke them unconditionally regardless of the guest's policy configuration. When mem-agent is enabled (off by default), this lets the host tamper with in-guest memory management by forcing swap, aggressive eviction, or compaction, resulting in attacker-controlled availability and performance degradation of the confidential workload entirely outside the agent-policy boundary. The impact does not include memory disclosure or code execution, and severity is bounded by the precondition that mem-agent must be explicitly enabled. This issue is fixed in version 4.0.0.
Hi,
Zvonko kindly brought the below to linux-distros last month (thank you, Zvonko!), but seems to have failed to bring it to oss-security on the public disclosure date as required. I only discovered this now while processing the distros list statistics for April.
Gentoo and Amazon failed to stay on top of this, even though that was their contributing-back task. We already figured this out with Gentoo in context of another issue, but I am yet to hear from Amazon.
The "private" GitHub advisory referenced below is now public at that URL. It says the issue's Severity is Critical without giving a vector/score. However, NIST NVD lists this for CVE-2026-41326: Kata Containers is an open source project focusing on a standard implementation of lightweight Virtual Machines (VMs) that perform like containers. From v3.4.0 to v3.28.0, an oversight in the CopyFile policy (and perhaps the CopyFile handler) allows untrusted hosts to write to arbitrary locations inside the guest workload image. This can be used to overwrite binaries inside the guest and exfiltrate data from containers; even those running inside CVMs. This vulnerability is fixed in v3.29.0.
CNA: GitHub, Inc. CVSS-B 8.2 HIGH Vector: CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:H/SI:N/SA:N Alexander
----- Forwarded message from Zvonko Kaiser <zkaiser () nvidia com> -----
From: Zvonko Kaiser <zkaiser () nvidia com> To: "linux-distros () vs openwall org" <linux-distros () vs openwall org> Subject: [vs-plain] Vulnerability in Kata Containers (CVE Requested) Date: Thu, 16 Apr 2026 13:42:19 +0000
This is an advance warning of a vulnerability discovered in Kata Containers, to give you, as downstream stakeholders, a chance to coordinate the release of fixes and reduce the vulnerability window. Please treat the following information as confidential until the proposed public disclosure date.
An oversight in the CopyFile policy (and perhaps the CopyFile handler) allows untrusted hosts to write to arbitrary locations inside the guest workload image. This can be used to overwrite binaries inside the guest and exfiltrate data from containers; even those running inside CVMs.
Proposed patch: See attached patches. Unless a flaw is discovered in them, these patches will be merged to their corresponding branches on the public disclosure date.
CVE: Requested
Proposed public disclosure date/time: 2026-04-22, 1800 UTC Please do not make the issue public (or release public patches) before this coordinated embargo date.
Original private report: https://github.com/kata-containers/kata-containers/security/advisories/GHSA-q49m-57vm-c8cc
For access to read and comment on the security report, please reply to me with your GitHub username and I will subscribe you.
Zvonko Kaiser Kata Containers Vulnerability Management Team From 30d06769469a652e7c33dee2cf1400bfb691fbcf Mon Sep 17 00:00:00 2001 From: Markus Rudy <mr@edgeless.systems> Date: Tue, 14 Apr 2026 16:51:15 +0200 Subject: [PATCH 1/3] agent: add required features for standalone build
Building the kata-agent-policy crate only succeeded when its parents (agent and genpolicy) pulled in the required features. This commit adds the required features to the crate itself, such that it can be built standalone and IDEs don't show errors while browsing it.
Signed-off-by: Markus Rudy <mr@edgeless.systems> --- src/agent/policy/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/agent/policy/Cargo.toml b/src/agent/policy/Cargo.toml index 3562008b91..b17a9ff764 100644 --- a/src/agent/policy/Cargo.toml +++ b/src/agent/policy/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true [dependencies] # Async runtime -tokio.workspace = true +tokio = { workspace = true, features = ["fs", "io-util"] } anyhow.workspace = true From b89ad10f76447380c5601dad24ea5d663babb4c0 Mon Sep 17 00:00:00 2001 From: Markus Rudy <mr@edgeless.systems> Date: Wed, 15 Apr 2026 09:29:24 +0200 Subject: [PATCH 2/3] agent: fix naming for symlinks in CopyFile
The agent referred to the data field of an incoming CopyFileRequest as the 'src'. This is misleading, because 'source' is not mentioned in the specification (where links are just a path with attached bytes), and because the documentation for the ln utility calls the path LINKNAME and the data TARGET. This commit fixes the glitch and calls the first argument to symlinkat the target.
Signed-off-by: Markus Rudy <mr@edgeless.systems> --- src/agent/src/rpc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/agent/src/rpc.rs b/src/agent/src/rpc.rs index b85e948e49..d90e57d6df 100644 --- a/src/agent/src/rpc.rs +++ b/src/agent/src/rpc.rs @@ -2108,8 +2108,8 @@ fn docopyfile(req: &CopyFileRequest) -> Result<()> { } // Create new symbolic link - let src = PathBuf::from(OsStr::frombytes(&req.data)); - unistd::symlinkat(&src, None, &path)?; + let symlinktarget = PathBuf::from(OsStr::frombytes(&req.data)); + unistd::symlinkat(&symlinktarget, None, &path)?; // Set symlink ownership (permissions not supported for symlinks) let pathstr = CString::new(path.asosstr().asbytes())?; From 5e931ac8301fcf46724614fa5f1f3836b2ae54d9 Mon Sep 17 00:00:00 2001 From: Markus Rudy <mr@edgeless.systems> Date: Thu, 9 Apr 2026 16:02:14 +0200 Subject: [PATCH 3/3] genpolicy: restrict symlinks in CopyFile
Allowing arbitrary symlinks in the shared directory is unsafe for confidential VM use cases. In order to make CopyFile safe both for the VM as well for the consuming containers, we implement the following rules for symlinks (in addition to the existing rules for other files):
1. Symlinks may not be placed directly into the shared directory. 2. Symlinks must not point 'upwards', i.e. contain .. as a path element. 3. Symlinks must be relative.
These rules ensure that all writes initiated by CopyFile are restricted to the shared directory (protecting the VM), and that symlinks can't point outside their mount points (protecting the container).
These new restrictions mean that we can't support arbitrary mount sources (which might not follow these rules), but the usual k8s suspects (ConfigMap, Secret, ServiceAccountToken) should still pass.
In order to aid writing the policy, we convert the CopyFileRequest to a structure that does not contain binary data, but well-defined strings and types.
Signed-off-by: Markus Rudy <mr@edgeless.systems> --- Cargo.lock | 2 + src/agent/policy/Cargo.toml | 7 + src/agent/policy/src/policy.rs | 222 +++++++++++++++++- src/agent/src/policy.rs | 9 +- src/agent/src/rpc.rs | 17 +- src/tools/genpolicy/rules.rego | 50 +++- src/tools/genpolicy/tests/policy/main.rs | 8 +- .../policy/testdata/copyfile/testcases.json | 97 ++++++++ 8 files changed, 398 insertions(+), 14 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock index a4d5beb947..e9d3044eeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3469,6 +3469,8 @@ version = "0.1.0" dependencies = [ "anyhow", "json-patch 2.0.0", + "libc", + "protocols", "regorus", "serde", "serdejson", diff --git a/src/agent/policy/Cargo.toml b/src/agent/policy/Cargo.toml index b17a9ff764..42632978f1 100644 --- a/src/agent/policy/Cargo.toml +++ b/src/agent/policy/Cargo.toml @@ -25,8 +25,15 @@ regorus = { version = "0.2.8", default-features = false, features = [ ] } json-patch = "2.0.0" +# POSIX +libc.workspace = true + # Note: this crate sets the slog 'max' features which allows the log level # to be modified at runtime. slog.workspace = true slog-scope.workspace = true + +# Internal dependencies + +protocols = { workspace = true, features = ["with-serde"] } diff --git a/src/agent/policy/src/policy.rs b/src/agent/policy/src/policy.rs index 4e4a9debb1..0aa2c1081c 100644 --- a/src/agent/policy/src/policy.rs +++ b/src/agent/policy/src/policy.rs @@ -6,7 +6,10 @@ //! Policy evaluation for the kata-agent. -use anyhow::{bail, Result}; +use std::{ffi::OsStr, os::unix::ffi::OsStrExt as }; + +use anyhow::{bail, Error, Result}; +use protocols::agent::CopyFileRequest; use slog::{debug, error, info, warn}; use tokio::io::AsyncWriteExt; @@ -241,3 +244,220 @@ impl AgentPolicy { Ok(()) } } + +/// FileType represents the SIFMT part of the POSIX file mode such that it's easier to check in +/// Rego. +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Default, PartialEq)] +pub enum FileType { + #[default] + Unknown, + Regular, + Directory, + Symlink, +} + +impl From<u32> for FileType { + fn from(rawmode: u32) -> Self { + match rawmode & libc::SIFMT { + libc::SIFREG => Self::Regular, + libc::SIFDIR => Self::Directory, + libc::SIFLNK => Self::Symlink, + => Self::Unknown, + } + } +} + +/// PolicyCopyFileRequest is a pre-processed variant of the CopyFileRequest that avoids byte +/// manipulation in Rego rules. +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Default, PartialEq)] +#[serde(default)] +pub struct PolicyCopyFileRequest { + pub path: String, + pub filetype: FileType, + pub symlinktarget: Option<String>, + + // Below fields are copied from the original request. They are not used by the genpolicy rules, + // but might be relevant for alternative rule sets. The data field is intentionally omitted to + // reduce serde overhead and protect the rules engine. + + pub filesize: i64, + pub filemode: u32, + pub dirmode: u32, + pub uid: i32, + pub gid: i32, + pub offset: i64, +} + +impl std::convert::TryFrom<&CopyFileRequest> for PolicyCopyFileRequest { + type Error = Error; + + fn tryfrom(req: &CopyFileRequest) -> Result<Self> { + let filetype = req.filemode.into(); + let symlinktarget: Option<String> = match filetype { + FileType::Symlink => { + if let Some(s) = OsStr::frombytes(&req.data).tostr() { + Some(s.toowned()) + } else { + bail!("invalid symlink content") + } + } + => None, + }; + + Ok(PolicyCopyFileRequest { + path: req.path.clone(), + filetype, + symlinktarget, + filesize: req.filesize, + filemode: req.filemode, + dirmode: req.dirmode, + uid: req.uid, + gid: req.gid, + offset: req.offset, + }) + } +} + +#[cfg(test)] +mod tests { + use super::; + use std::convert::TryInto; + + use protocols::agent::CopyFileRequest; + + struct TestCase { + name: String, + input: CopyFileRequest, + output: Option<PolicyCopyFileRequest>, + } + + #[test] + fn testcopyfiletranslation() { + let testcases = [ + TestCase { + name: "regular".toowned(), + input: CopyFileRequest { + filemode: libc::SIFREG, + path: "/foo/bar".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFREG, + filetype: FileType::Regular, + path: "/foo/bar".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "directory".toowned(), + input: CopyFileRequest { + filemode: libc::SIFDIR, + path: "/foo".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFDIR, + filetype: FileType::Directory, + path: "/foo".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "socket".toowned(), + input: CopyFileRequest { + filemode: libc::SIFSOCK, + path: "/foo/sock".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFSOCK, + filetype: FileType::Unknown, + path: "/foo/sock".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "mixed".toowned(), + input: CopyFileRequest { + filemode: libc::SIFDIR | libc::SIFREG, + path: "/foo/dunno".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFDIR | libc::SIFREG, + filetype: FileType::Unknown, + path: "/foo/dunno".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "all".toowned(), + input: CopyFileRequest { + filemode: libc::SIFMT, + path: "/wat".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFMT, + filetype: FileType::Unknown, + path: "/wat".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "none".toowned(), + input: CopyFileRequest { + filemode: 0, + path: "/0".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: 0, + filetype: FileType::Unknown, + path: "/0".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "link/valid".toowned(), + input: CopyFileRequest { + data: b"..data/foo".tovec(), + filemode: libc::SIFLNK, + path: "/foo/lnk".toowned(), + ..Default::default() + }, + output: Some(PolicyCopyFileRequest { + filemode: libc::SIFLNK, + filetype: FileType::Symlink, + symlinktarget: Some("..data/foo".toowned()), + path: "/foo/lnk".toowned(), + ..Default::default() + }), + }, + TestCase { + name: "link/invalid".toowned(), + input: CopyFileRequest { + filemode: libc::SIFLNK, + data: vec![0x00, 0xFF, 0xFF, 0x00], + ..Default::default() + }, + output: None, + }, + ]; + + for testcase in testcases { + let outputres: Result<PolicyCopyFileRequest> = (&testcase.input).tryinto(); + if let Some(expected) = testcase.output { + let output = outputres.expect(&format!("test case {}", &testcase.name)); + asserteq!(expected, output, "test case {}", &testcase.name) + } else { + assert!( + outputres.iserr(), + "test case {}\nunexpected success: {:?}", + &testcase.name, + outputres + ) + } + } + } +} diff --git a/src/agent/src/policy.rs b/src/agent/src/policy.rs index 1b8f391c0b..fddef06ed2 100644 --- a/src/agent/src/policy.rs +++ b/src/agent/src/policy.rs @@ -29,9 +29,16 @@ async fn allowrequest(policy: &mut AgentPolicy, ep: &str, request: &str) -> ttr } pub async fn isallowed(req: &(impl MessageDyn + serde::Serialize)) -> ttrpc::Result<()> { + isallowedwithentrypoint(req.descriptordyn().name(), &req).await +} + +pub async fn isallowedwithentrypoint( + ep: &str, + req: &impl serde::Serialize, +) -> ttrpc::Result<()> { let request = serdejson::tostring(req).unwrap(); let mut policy = AGENTPOLICY.lock().await; - allowrequest(&mut policy, req.descriptordyn().name(), &request).await + allowrequest(&mut policy, ep, &request).await } pub async fn dosetpolicy(req: &protocols::agent::SetPolicyRequest) -> ttrpc::Result<()> { diff --git a/src/agent/src/rpc.rs b/src/agent/src/rpc.rs index d90e57d6df..f4fca88247 100644 --- a/src/agent/src/rpc.rs +++ b/src/agent/src/rpc.rs @@ -4,12 +4,16 @@ // use asynctrait::asynctrait; +#[cfg(feature = "agent-policy")] +use kataagentpolicy::policy::PolicyCopyFileRequest; use rustjail::{pipestream::PipeStream, process::StreamType}; use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf}; use tokio::sync::Mutex; use tokio::time::{timeout, Duration}; use std::convert::TryFrom; +#[cfg(feature = "agent-policy")] +use std::convert::TryInto as ; use std::ffi::{CString, OsStr}; use std::fmt::Debug; use std::io; @@ -28,6 +32,8 @@ use anyhow::{anyhow, Context, Result}; use cgroups::freezer::FreezerState; use oci::{Hooks, LinuxNamespace, Spec}; use ocispec::runtime as oci; +#[cfg(feature = "agent-policy")] +use protobuf::MessageDyn; use protobuf::MessageField; use protocols::agent::{ AddSwapPathRequest, AddSwapRequest, AgentDetails, CopyFileRequest, GetIPTablesRequest, @@ -85,7 +91,7 @@ use crate::tracerpccall; use crate::tracer::extractcarrierfromttrpc; #[cfg(feature = "agent-policy")] -use crate::policy::{dosetpolicy, isallowed}; +use crate::policy::{dosetpolicy, isallowed, isallowedwithentrypoint}; use opentelemetry::global; use tracing::span; @@ -1519,6 +1525,15 @@ impl agentttrpc::AgentService for AgentService { req: protocols::agent::CopyFileRequest, ) -> ttrpc::Result<Empty> { tracerpccall!(ctx, "copyfile", req); + #[cfg(feature = "agent-policy")] + { + let reqforpolicy: PolicyCopyFileRequest = (&req) + .tryinto() + .context("parsing CopyFileRequest for policy") + .mapttrpcerr(same)?; + isallowedwithentrypoint(req.descriptordyn().name(), &reqforpolicy).await?; + } + #[cfg(not(feature = "agent-policy"))] isallowed(&req).await?; docopyfile(&req).mapttrpcerr(same)?; diff --git a/src/tools/genpolicy/rules.rego b/src/tools/genpolicy/rules.rego index 1f2b8c4ffc..96c2d37513 100644 --- a/src/tools/genpolicy/rules.rego +++ b/src/tools/genpolicy/rules.rego @@ -1542,19 +1542,55 @@ allowsandboxstorage(pstorages, istorage) if { } CopyFileRequest if { - print("CopyFileRequest: input.path =", input.path) + print("CopyFileRequest: input =", input) - checkdirectorytraversal(input.path) + allowcopyfile + + print("CopyFileRequest: true") +} + +allowcopyfile if { + print("allowcopyfile regular") + + input.filetype == "Regular" + allowcopyfilepath(input.path, "") + + print("allowcopyfile regular: true") +} + +allowcopyfile if { + print("allowcopyfile directory") + + input.filetype == "Directory" + allowcopyfilepath(input.path, "") + + print("allowcopyfile directory: true") +} + +allowcopyfile if { + print("allowcopyfile symlink") + + input.filetype == "Symlink" + # Symlinks are not allowed on the top-level of the shared directory, from which we mount. + allowcopyfilepath(input.path, "./.+") + # Symlinks must be normalized. + checkdirectorytraversal(input.symlinktarget) + # Symlinks must be relative. + not startswith(input.symlinktarget, "/") + + print("allowcopyfile symlink: true") +} + +allowcopyfilepath(path, regexsuffix) if { + checkdirectorytraversal(path) some regex1 in policydata.requestdefaults.CopyFileRequest regex2 := replace(regex1, "$(sfprefix)", policydata.common.sfprefix) regex3 := replace(regex2, "$(cpath)", policydata.common.cpath) regex4 := replace(regex3, "$(bundle-id)", "[a-z0-9]{64}") - print("CopyFileRequest: regex4 =", regex4) - - regex.match(regex4, input.path) - - print("CopyFileRequest: true") + regex5 := concat("", [regex4, regexsuffix]) + print("allowcopyfilepath: regex5 =", regex5) + regex.match(regex5, path) } CreateSandboxRequest if { diff --git a/src/tools/genpolicy/tests/policy/main.rs b/src/tools/genpolicy/tests/policy/main.rs index c0b312850d..2e24fcb156 100644 --- a/src/tools/genpolicy/tests/policy/main.rs +++ b/src/tools/genpolicy/tests/policy/main.rs @@ -12,12 +12,12 @@ mod tests { use std::str; use protocols::agent::{ - AddARPNeighborsRequest, CopyFileRequest, CreateContainerRequest, CreateSandboxRequest, - ExecProcessRequest, RemoveContainerRequest, UpdateInterfaceRequest, UpdateRoutesRequest, + AddARPNeighborsRequest, CreateContainerRequest, CreateSandboxRequest, ExecProcessRequest, + RemoveContainerRequest, UpdateInterfaceRequest, UpdateRoutesRequest, }; use serde::{Deserialize, Serialize}; - use kataagentpolicy::policy::AgentPolicy; + use kataagentpolicy::policy::{AgentPolicy, PolicyCopyFileRequest}; // Translate each test case in testcases.json // to one request type. @@ -25,7 +25,7 @@ mod tests { #[serde(tag = "kind", content = "request")] #[allow(clippy::enumvariantnames)] // The tags need to match the entrypoint logged by the agent. enum TestRequest { - CopyFileRequest(CopyFileRequest), + CopyFileRequest(PolicyCopyFileRequest), CreateContainerRequest(CreateContainerRequest), CreateSandboxRequest(CreateSandboxRequest), ExecProcessRequest(ExecProcessRequest), diff --git a/src/tools/genpolicy/tests/policy/testdata/copyfile/testcases.json b/src/tools/genpolicy/tests/policy/testdata/copyfile/testcases.json index 17e52f2e1b..ed4d4d1fbd 100644 --- a/src/tools/genpolicy/tests/policy/testdata/copyfile/testcases.json +++ b/src/tools/genpolicy/tests/policy/testdata/copyfile/testcases.json @@ -4,6 +4,7 @@ "description": "copy initiated by k8s mount", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-resolv.conf" } }, @@ -12,6 +13,7 @@ "description": "a dirname can have trailing dots", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo../bar" } }, @@ -20,6 +22,7 @@ "description": "attempt to copy outside of container root", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/etc/ssl/cert.pem" } }, @@ -28,6 +31,7 @@ "description": "attempt to write into container root", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc/rootfs/bin/sh" } }, @@ -36,6 +40,7 @@ "description": "attempt to write into container root - guest pull", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/run/kata-containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc/rootfs/bin/sh" } }, @@ -44,6 +49,7 @@ "description": "attempted directory traversal", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/../../../../../etc/ssl/cert.pem" } }, @@ -52,6 +58,7 @@ "description": "attempted directory traversal - parent directory", "kind": "CopyFileRequest", "request": { + "filetype": "Directory", "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/.." } }, @@ -60,6 +67,7 @@ "description": "relative path", "kind": "CopyFileRequest", "request": { + "filetype": "Regular", "path": "etc/ssl/cert.pem" } }, @@ -68,7 +76,96 @@ "description": "relative path - parent directory", "kind": "CopyFileRequest", "request": { + "filetype": "Directory", "path": ".." } + }, + { + "allowed": false, + "description": "unsupported file type", + "kind": "CopyFileRequest", + "request": { + "filetype": "Unknown", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/bar" + } + }, + { + "allowed": true, + "description": "directory in top-level shared directory", + "kind": "CopyFileRequest", + "request": { + "filetype": "Directory", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo" + } + }, + { + "allowed": false, + "description": "symlink in top-level shared directory", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "abc", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo" + } + }, + { + "allowed": true, + "description": "symlink beneath top-level shared directory", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "abc", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } + }, + { + "allowed": false, + "description": "symlink pointing up - leading", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "../abc", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } + }, + { + "allowed": false, + "description": "symlink pointing up - middle", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "a/../../b", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } + }, + { + "allowed": false, + "description": "symlink pointing up - trailing", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "foo/..", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } + }, + { + "allowed": false, + "description": "symlink pointing up - ..", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "..", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } + }, + { + "allowed": false, + "description": "symlink with absolute target", + "kind": "CopyFileRequest", + "request": { + "symlinktarget": "/abc", + "filetype": "Symlink", + "path": "/run/kata-containers/shared/containers/81e5f43bc8599c5661e66f959ac28df5bfb30da23c5d583f2dcc6f9e0c5186dc-ce23cfeb91e75aaa-foo/lnk" + } } ]
----- End forwarded message -----
This vulnerability was fixed in Kata Containers 3.31.0:
https://github.com/kata-containers/kata-containers/releases/tag/3.31.0 https://github.com/kata-containers/kata-containers/commit/555b7738fe2f699af6d57712cf75330f012b4e3b
Description:
In the runtime-rs standalone virtio-fs path, Kata Containers runs virtiofsd as root with --sandbox none --seccomp none.
If an attacker has root-equivalent execution inside the Kata guest VM, they can send raw FUSE requests directly to the host virtiofsd.
Then, a raw FUSESYMLINK request whose new symlink name is an absolute host path is honored outside the virtio-fs shared directory.
This lets guest root create host-root-owned symlinks in sensitive host paths.
CVE: CVE-2026-47243 GHSA: GHSA-2gv2-cffp-j227
Original report: https://github.com/kata-containers/kata-containers/security/advisories/GHSA-2gv2-cffp-j227
--- Aurelien Bombo Kata Containers Vulnerability Management Team
This vulnerability has been fixed in Kata Containers. The fix will be included
in the upcoming 4.1.0 release, which is expected to be available shortly:
genpolicy: don't match image pull storages to mounts <https://github.com/kata-containers/kata-containers/commit/fe8eeefcd0bec13c0 37ceb8f0889e48b75db17ab> . kata-containers/kata-containers@fe8eeef
Description:
A flaw was found in Kata Containers. In configurations utilizing genpolicy for
Confidential Containers guest protection, a malicious host operator can
exploit insufficient validation of CreateContainer mount and storage rules.
This allows them to mount arbitrary container-rootfs paths over sensitive
host locations or provision arbitrary content, potentially exposing
confidential information or enabling the acceptance of attacker-controlled
input.
CVE: CVE-2026-77176
GHSA: GHSA-fmg6-v47x-52wr
Original report:
generated policy allows mounting attacker-chosen guest paths to specific locations <https://github.com/kata-containers/kata-containers/security/advisories/GHSA -fmg6-v47x-52wr> . Advisory . kata-containers/kata-containers
---
Manuel Huber, on behalf of the
Kata Containers Vulnerability Management Team
Hi,
Fabiano had kindly brought this to linux-distros on July 11 and a public disclosure date of 2026-07-20 was set, but unfortunately we failed to bring this to oss-security on that date and overlooked this fact until I went to process the (linux-)distros list statistics for July.
On Sun, Aug 23, 2026 at 06:07:28PM +0000, Fabiano Fidencio wrote: The CVE-2026-50540 has been made public on 2026-07-20, and is part of the Kata Containers 4.0.0 Thank you Fabiano for posting this. We need to include actual detail, which I'll try to add by quoting 3 messages from the linux-distros thread below my signature. It could have been better to post this as one up-to-date disclosure, but these old messages is all I have.
Alexander
On Sat, Jul 11, 2026 at 09:18:15AM +0000, Fabiano Fidencio wrote: This is an advance warning of a vulnerability discovered in Kata Containers, to give you, as downstream stakeholders, a chance to coordinate the release of fixes and reduce the vulnerability window. Please treat the following information as confidential until the proposed public disclosure date.
A vulnerability has been reported in the Kata Containers configuration loading path in both the Rust and Go runtimes.
Both runtimes accept the sandbox configuration file path from the pod annotation io.katacontainers.configpath. This path is then passed to the TOML configuration loader without sufficient validation or restriction.
An authenticated pod user who is able to set pod annotations may point io.katacontainers.configpath at an arbitrary TOML configuration file available on the host. Since the Kata configuration controls privileged runtime settings, including hypervisor and virtio-fs daemon binary paths, a malicious configuration may cause attacker-controlled binaries to be executed as root on the host when the sandbox starts.
This has been classified as improper input validation, CWE-20.
Affected versions: Kata Containers versions up to and including 3.32.0 are reported as affected.
Affected components:
kata-runtime-rs (Rust runtime) kata-runtime (Go runtime) Impact: Successful exploitation may result in host-level code execution as root. The highest risk is in multi-tenant Kubernetes environments where users can create pods with arbitrary annotations and can place or reference a configuration file at a host-accessible path.
Attack requirements: The attacker must be able to create a pod with the io.katacontainers.configpath annotation and must be able to reference a TOML configuration file from a path visible to the host runtime.
Suggested mitigation: Reject or ignore the io.katacontainers.configpath annotation by default, or restrict accepted configuration paths to an explicit set of trusted directories. As a temporary workaround, cluster operators may use an admission webhook or policy engine to reject pods that set the io.katacontainers.configpath annotation.
Proposed patch: See attached patches. Unless a flaw is discovered in them, these patches will be merged to their corresponding branches on the public disclosure date.
CVE: CVE-2026-50540
Proposed public disclosure date/time: 2026-07-20, 1200UTC Please do not make the issue public (or release public patches) before this coordinated embargo date.
Original private report: https://github.com/kata-containers/kata-containers/security/advisories/GHSA-mp2j-xm59-qfgw For access to read and comment on the security report, please reply to me with your GitHub username and I will subscribe you.
-- Fabiano Fid??ncio, Kata Containers Vulnerability Management Team On Wed, Jul 22, 2026 at 01:08:31PM -0300, Marco Benatto wrote: Hello Fabiano,
I see https://github.com/kata-containers/kata-containers/security/advisories/GHSA-mp2j-xm59-qfgw is public right now however it only mentions the rust crate and doesn't include the golang one.
I see the repo has https://github.com/kata-containers/kata-containers/commit/03cc670076099530f4e1e9cb22849afdafb20f65 for the runtime which I think may be the golang fix.
GIven that, may we assume those who only ships the golang runtime are allowed to make it public on our side as well, since it's grouped all under the same CVE?
Thanks,
Marco Benatto Red Hat Product Security On Wed, Jul 22, 2026 at 04:23:21PM +0000, Fabiano Fidencio wrote: Marco,
Yes, your assumption is correct. The CVE affected both Golang and rust runtime, and the fixes applied are for both of them.
Best Regards -- Fabiano Fidencio