mirror of
https://github.com/kata-containers/kata-containers.git
synced 2026-04-12 23:04:33 +00:00
Compare commits
1 Commits
main
...
burgerdev/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b059f60fc |
@@ -159,6 +159,7 @@ netns-rs = "0.1.0"
|
|||||||
# Note: nix needs to stay sync'd with libs versions
|
# Note: nix needs to stay sync'd with libs versions
|
||||||
nix = "0.26.4"
|
nix = "0.26.4"
|
||||||
oci-spec = { version = "0.8.1", features = ["runtime"] }
|
oci-spec = { version = "0.8.1", features = ["runtime"] }
|
||||||
|
pathrs = "0.2.4"
|
||||||
opentelemetry = { version = "0.17.0", features = ["rt-tokio"] }
|
opentelemetry = { version = "0.17.0", features = ["rt-tokio"] }
|
||||||
procfs = "0.12.0"
|
procfs = "0.12.0"
|
||||||
prometheus = { version = "0.14.0", features = ["process"] }
|
prometheus = { version = "0.14.0", features = ["process"] }
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ safe-path.workspace = true
|
|||||||
# to be modified at runtime.
|
# to be modified at runtime.
|
||||||
logging.workspace = true
|
logging.workspace = true
|
||||||
vsock-exporter.workspace = true
|
vsock-exporter.workspace = true
|
||||||
|
pathrs.workspace = true
|
||||||
|
|
||||||
# Initdata
|
# Initdata
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ libseccomp = { version = "0.3.0", optional = true }
|
|||||||
zbus = "3.12.0"
|
zbus = "3.12.0"
|
||||||
bit-vec = "0.8.0"
|
bit-vec = "0.8.0"
|
||||||
xattr = "0.2.3"
|
xattr = "0.2.3"
|
||||||
|
pathrs.workspace = true
|
||||||
|
|
||||||
# Local dependencies
|
# Local dependencies
|
||||||
protocols.workspace = true
|
protocols.workspace = true
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ use nix::NixPath;
|
|||||||
use oci::{LinuxDevice, Mount, Process, Spec};
|
use oci::{LinuxDevice, Mount, Process, Spec};
|
||||||
use oci_spec::runtime as oci;
|
use oci_spec::runtime as oci;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs;
|
||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
use std::os::unix;
|
use std::os::unix;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
@@ -47,6 +48,7 @@ pub struct Info {
|
|||||||
const MOUNTINFO_FORMAT: &str = "{d} {d} {d}:{d} {} {} {} {}";
|
const MOUNTINFO_FORMAT: &str = "{d} {d} {d}:{d} {} {} {} {}";
|
||||||
const MOUNTINFO_PATH: &str = "/proc/self/mountinfo";
|
const MOUNTINFO_PATH: &str = "/proc/self/mountinfo";
|
||||||
const PROC_PATH: &str = "/proc";
|
const PROC_PATH: &str = "/proc";
|
||||||
|
const SHARED_DIRECTORY: &str = "/run/kata-containers/shared/containers";
|
||||||
|
|
||||||
const ERR_FAILED_PARSE_MOUNTINFO: &str = "failed to parse mountinfo file";
|
const ERR_FAILED_PARSE_MOUNTINFO: &str = "failed to parse mountinfo file";
|
||||||
const ERR_FAILED_PARSE_MOUNTINFO_FINAL_FIELDS: &str =
|
const ERR_FAILED_PARSE_MOUNTINFO_FINAL_FIELDS: &str =
|
||||||
@@ -233,10 +235,11 @@ pub fn init_rootfs(
|
|||||||
|
|
||||||
// From https://github.com/opencontainers/runtime-spec/blob/main/config.md#mounts
|
// From https://github.com/opencontainers/runtime-spec/blob/main/config.md#mounts
|
||||||
// type (string, OPTIONAL) The type of the filesystem to be mounted.
|
// type (string, OPTIONAL) The type of the filesystem to be mounted.
|
||||||
// bind may be only specified in the oci spec options -> flags update r#type
|
// For bind mounts, this can be empty or any string whatsoever. For consistency, we set it
|
||||||
|
// to 'bind'.
|
||||||
let m = &{
|
let m = &{
|
||||||
let mut mbind = m.clone();
|
let mut mbind = m.clone();
|
||||||
if is_none_mount_type(mbind.typ()) && flags & MsFlags::MS_BIND == MsFlags::MS_BIND {
|
if flags.contains(MsFlags::MS_BIND) {
|
||||||
mbind.set_typ(Some("bind".to_string()));
|
mbind.set_typ(Some("bind".to_string()));
|
||||||
}
|
}
|
||||||
mbind
|
mbind
|
||||||
@@ -397,13 +400,6 @@ fn mount_cgroups_v2(cfd_log: RawFd, m: &Mount, rootfs: &str, flags: MsFlags) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_none_mount_type(typ: &Option<String>) -> bool {
|
|
||||||
match typ {
|
|
||||||
Some(t) => t == "none",
|
|
||||||
None => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mount_cgroups(
|
fn mount_cgroups(
|
||||||
cfd_log: RawFd,
|
cfd_log: RawFd,
|
||||||
m: &Mount,
|
m: &Mount,
|
||||||
@@ -763,48 +759,20 @@ fn mount_from(
|
|||||||
let mut d = String::from(data);
|
let mut d = String::from(data);
|
||||||
let mount_dest = m.destination().display().to_string();
|
let mount_dest = m.destination().display().to_string();
|
||||||
let mount_typ = m.typ().as_ref().unwrap();
|
let mount_typ = m.typ().as_ref().unwrap();
|
||||||
|
|
||||||
|
if mount_typ == "bind" {
|
||||||
|
// Bind mounts need special treatment for security, handle them separately.
|
||||||
|
return bind_mount_from(m, rootfs, flags)
|
||||||
|
.inspect_err(|e| log_child!(cfd_log, "bind_mount_from failed: {:?}", e));
|
||||||
|
}
|
||||||
|
|
||||||
let dest = scoped_join(rootfs, mount_dest)?
|
let dest = scoped_join(rootfs, mount_dest)?
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))?
|
.ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let mount_source = m.source().as_ref().unwrap().display().to_string();
|
let mount_source = m.source().as_ref().unwrap().display().to_string();
|
||||||
let src = if mount_typ == "bind" {
|
let src = {
|
||||||
let src = fs::canonicalize(&mount_source)?;
|
|
||||||
let dir = if src.is_dir() {
|
|
||||||
Path::new(&dest)
|
|
||||||
} else {
|
|
||||||
Path::new(&dest).parent().unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
fs::create_dir_all(dir).inspect_err(|e| {
|
|
||||||
log_child!(
|
|
||||||
cfd_log,
|
|
||||||
"create dir {}: {}",
|
|
||||||
dir.to_str().unwrap(),
|
|
||||||
e.to_string()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// make sure file exists so we can bind over it
|
|
||||||
if !src.is_dir() {
|
|
||||||
let _ = OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.write(true)
|
|
||||||
.open(&dest)
|
|
||||||
.map_err(|e| {
|
|
||||||
log_child!(
|
|
||||||
cfd_log,
|
|
||||||
"open/create dest error. {}: {:?}",
|
|
||||||
dest.as_str(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
src.to_str().unwrap().to_string()
|
|
||||||
} else {
|
|
||||||
let _ = fs::create_dir_all(&dest);
|
let _ = fs::create_dir_all(&dest);
|
||||||
if mount_typ == "cgroup2" {
|
if mount_typ == "cgroup2" {
|
||||||
"cgroup2".to_string()
|
"cgroup2".to_string()
|
||||||
@@ -864,25 +832,126 @@ fn mount_from(
|
|||||||
if !label.is_empty() && selinux::is_enabled()? && use_xattr {
|
if !label.is_empty() && selinux::is_enabled()? && use_xattr {
|
||||||
xattr::set(dest.as_str(), "security.selinux", label.as_bytes())?;
|
xattr::set(dest.as_str(), "security.selinux", label.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
if flags.contains(MsFlags::MS_BIND)
|
fn bind_mount_from(m: &Mount, rootfs: &str, flags: MsFlags) -> Result<()> {
|
||||||
&& flags.intersects(
|
let mount_source_fd = {
|
||||||
!(MsFlags::MS_REC
|
let shared_dir = PathBuf::from(SHARED_DIRECTORY);
|
||||||
| MsFlags::MS_REMOUNT
|
let unsafe_mount_source = m
|
||||||
| MsFlags::MS_BIND
|
.source()
|
||||||
| MsFlags::MS_PRIVATE
|
.as_ref()
|
||||||
| MsFlags::MS_SHARED
|
.ok_or_else(|| anyhow::anyhow!("Missing source for bind mount"))?;
|
||||||
| MsFlags::MS_SLAVE),
|
// Policy checks ensured earlier that shared mount sources start with the `sfprefix`.
|
||||||
|
// Therefore, it's safe to derive the root for scoping the mount source from that prefix.
|
||||||
|
let (root_path, inner_path) = match unsafe_mount_source.strip_prefix(&shared_dir) {
|
||||||
|
Ok(inner) => (shared_dir, inner), // needs scoping
|
||||||
|
Err(_) => (PathBuf::from("/"), unsafe_mount_source.as_path()), // does not need scoping, i.e. scoped to root
|
||||||
|
};
|
||||||
|
let root = pathrs::Root::open(&root_path)
|
||||||
|
.context(format!("opening mount_source_root {:?} failed", root_path))?;
|
||||||
|
root.open_subpath(inner_path, pathrs::flags::OpenFlags::O_PATH)
|
||||||
|
.context(format!(
|
||||||
|
"opening {:?} in {:?} failed",
|
||||||
|
inner_path, root_path
|
||||||
|
))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let container_root = pathrs::Root::open(rootfs)
|
||||||
|
.context(format!("opening mount_source_root {:?} failed", rootfs))?;
|
||||||
|
|
||||||
|
// .metadata queries attrs with statx + AT_EMPTY_PATH, i.e. directly from the opened fd.
|
||||||
|
let meta = mount_source_fd.metadata().context("statx failed")?;
|
||||||
|
let mount_destination_fd = if meta.is_dir() {
|
||||||
|
container_root
|
||||||
|
.mkdir_all(m.destination(), &std::fs::Permissions::from_mode(0o755))
|
||||||
|
.context(format!(
|
||||||
|
"mkdir_all for {:?} in {} failed",
|
||||||
|
m.destination(),
|
||||||
|
rootfs
|
||||||
|
))?
|
||||||
|
.reopen(pathrs::flags::OpenFlags::O_DIRECTORY)?
|
||||||
|
} else if meta.is_symlink() {
|
||||||
|
anyhow::bail!("won't bind mount from symlink {:?}: {:?}", m.destination(), meta)
|
||||||
|
} else {
|
||||||
|
// All other bind mounts (files, devices, sockets) should target a file.
|
||||||
|
if let Some(parent) = m.destination().parent() {
|
||||||
|
let _ = container_root
|
||||||
|
.mkdir_all(parent, &std::fs::Permissions::from_mode(0o755))
|
||||||
|
.context(format!("mkdir_all for {:?} in {} failed", parent, rootfs))?;
|
||||||
|
}
|
||||||
|
container_root
|
||||||
|
.create_file(
|
||||||
|
m.destination(),
|
||||||
|
pathrs::flags::OpenFlags::O_TRUNC,
|
||||||
|
&std::fs::Permissions::from_mode(0o755),
|
||||||
|
)
|
||||||
|
.context(format!(
|
||||||
|
"create_file for {:?} in {} failed",
|
||||||
|
m.destination(),
|
||||||
|
rootfs
|
||||||
|
))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_tree_flags = if flags.intersects(MsFlags::MS_REC) {
|
||||||
|
libc::AT_RECURSIVE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let empty_path = std::ffi::CString::new("")?;
|
||||||
|
|
||||||
|
let tree_dfd = unsafe {
|
||||||
|
libc::syscall(
|
||||||
|
libc::SYS_open_tree,
|
||||||
|
mount_source_fd.as_raw_fd(),
|
||||||
|
empty_path.as_ptr(),
|
||||||
|
libc::OPEN_TREE_CLONE
|
||||||
|
| libc::OPEN_TREE_CLOEXEC
|
||||||
|
| libc::AT_EMPTY_PATH as u32
|
||||||
|
| open_tree_flags as u32,
|
||||||
)
|
)
|
||||||
{
|
};
|
||||||
|
|
||||||
|
if tree_dfd < 0 {
|
||||||
|
return Err(std::io::Error::last_os_error().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
libc::syscall(
|
||||||
|
libc::SYS_move_mount,
|
||||||
|
tree_dfd,
|
||||||
|
empty_path.as_ptr(),
|
||||||
|
mount_destination_fd.as_raw_fd(),
|
||||||
|
empty_path.as_ptr(),
|
||||||
|
0x01 /* MOVE_MOUNT_F_EMPTY_PATH */ | 0x02, /* MOVE_MOUNT_T_EMPTY_PATH */
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if ret < 0 {
|
||||||
|
return Err(std::io::Error::last_os_error().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.intersects(
|
||||||
|
!(MsFlags::MS_REC
|
||||||
|
| MsFlags::MS_REMOUNT
|
||||||
|
| MsFlags::MS_BIND
|
||||||
|
| MsFlags::MS_PRIVATE
|
||||||
|
| MsFlags::MS_SHARED
|
||||||
|
| MsFlags::MS_SLAVE),
|
||||||
|
) {
|
||||||
|
// TODO(burgerdev): we really should be using mount_setattr here, but the necessary API is not in nix.
|
||||||
|
|
||||||
|
// We successfully resolved the destination within the rootfs above. If nothing else messed
|
||||||
|
// with the filesystem in between, using the path unchecked should be safe.
|
||||||
|
let dest = scoped_join(rootfs, m.destination())?;
|
||||||
mount(
|
mount(
|
||||||
Some(dest.as_str()),
|
Some(&dest),
|
||||||
dest.as_str(),
|
&dest,
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
flags | MsFlags::MS_REMOUNT,
|
flags | MsFlags::MS_REMOUNT,
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
)
|
)
|
||||||
.inspect_err(|e| log_child!(cfd_log, "remout {}: {:?}", dest.as_str(), e))?;
|
.context("remount failed")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use pathrs::flags::OpenFlags;
|
||||||
use rustjail::{pipestream::PipeStream, process::StreamType};
|
use rustjail::{pipestream::PipeStream, process::StreamType};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -13,6 +14,7 @@ use std::convert::TryFrom;
|
|||||||
use std::ffi::{CString, OsStr};
|
use std::ffi::{CString, OsStr};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
#[cfg(target_arch = "s390x")]
|
#[cfg(target_arch = "s390x")]
|
||||||
@@ -99,7 +101,7 @@ use std::os::unix::prelude::PermissionsExt;
|
|||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use nix::unistd::{Gid, Uid};
|
use nix::unistd::{Gid, Uid};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::os::unix::fs::FileExt;
|
use std::os::unix::fs::FileExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -132,6 +134,18 @@ const ERR_NO_SANDBOX_PIDNS: &str = "Sandbox does not have sandbox_pidns";
|
|||||||
// not available.
|
// not available.
|
||||||
const IPTABLES_RESTORE_WAIT_SEC: u64 = 5;
|
const IPTABLES_RESTORE_WAIT_SEC: u64 = 5;
|
||||||
|
|
||||||
|
/// This mask is applied to parent directories implicitly created for CopyFile requests.
|
||||||
|
const IMPLICIT_DIRECTORY_PERMISSION_MASK: u32 = 0o777;
|
||||||
|
|
||||||
|
/// This mask is applied to directories explicitly created for CopyFile requests.
|
||||||
|
/// setgid and sticky bit are valid for such directories, whereas setuid is not.
|
||||||
|
const EXPLICIT_DIRECTORY_PERMISSION_MASK: u32 = 0o3777;
|
||||||
|
|
||||||
|
/// This mask is applied to files created for CopyFile requests.
|
||||||
|
/// This constant is used for consistency with *_DIRECTORY_PERMISSION_MASK.
|
||||||
|
const FILE_PERMISSION_MASK: u32 = 0o7777;
|
||||||
|
|
||||||
|
|
||||||
// Convenience function to obtain the scope logger.
|
// Convenience function to obtain the scope logger.
|
||||||
fn sl() -> slog::Logger {
|
fn sl() -> slog::Logger {
|
||||||
slog_scope::logger()
|
slog_scope::logger()
|
||||||
@@ -1521,7 +1535,9 @@ impl agent_ttrpc::AgentService for AgentService {
|
|||||||
trace_rpc_call!(ctx, "copy_file", req);
|
trace_rpc_call!(ctx, "copy_file", req);
|
||||||
is_allowed(&req).await?;
|
is_allowed(&req).await?;
|
||||||
|
|
||||||
do_copy_file(&req).map_ttrpc_err(same)?;
|
// Potentially untrustworthy data from the host needs to go into the shared dir.
|
||||||
|
let root_path = PathBuf::from(KATA_GUEST_SHARE_DIR);
|
||||||
|
do_copy_file(&req, &root_path).map_ttrpc_err(same)?;
|
||||||
|
|
||||||
Ok(Empty::new())
|
Ok(Empty::new())
|
||||||
}
|
}
|
||||||
@@ -2035,125 +2051,116 @@ fn do_set_guest_date_time(sec: i64, usec: i64) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_copy_file(req: &CopyFileRequest) -> Result<()> {
|
/// do_copy_file creates a file, directory or symlink beneath the provided directory.
|
||||||
let path = PathBuf::from(req.path.as_str());
|
///
|
||||||
|
/// The function guarantees that no content is written outside of the directory. However, a symlink
|
||||||
|
/// created by this function might point outside the shared directory. Other users of that
|
||||||
|
/// directory need to consider whether they trust the host, or handle the directory with the same
|
||||||
|
/// care as do_copy_file.
|
||||||
|
///
|
||||||
|
/// Parent directories are created, if they don't exist already. For these implicit operations, the
|
||||||
|
/// permissions are set with req.dir_mode. The actual target is created with permissions from
|
||||||
|
/// req.file_mode, even if it's a directory. For symlinks, req.file_mode is
|
||||||
|
///
|
||||||
|
/// If this function returns an error, the filesystem may be in an unexpected state. This is not
|
||||||
|
/// significant for the caller, since errors are almost certainly not retriable. The runtime should
|
||||||
|
/// abandon this VM instead.
|
||||||
|
fn do_copy_file(req: &CopyFileRequest, shared_dir: &PathBuf) -> Result<()> {
|
||||||
|
let insecure_full_path = PathBuf::from(req.path.as_str());
|
||||||
|
let path = insecure_full_path
|
||||||
|
.strip_prefix(&shared_dir)
|
||||||
|
.context(format!(
|
||||||
|
"removing {:?} prefix from {}",
|
||||||
|
shared_dir, req.path
|
||||||
|
))?;
|
||||||
|
|
||||||
if !path.starts_with(CONTAINER_BASE) {
|
// The shared directory might not exist yet, but we need to create it in order to open the root.
|
||||||
return Err(anyhow!(
|
std::fs::create_dir_all(shared_dir)?;
|
||||||
"Path {:?} does not start with {}",
|
let root = pathrs::Root::open(shared_dir)?;
|
||||||
path,
|
|
||||||
CONTAINER_BASE
|
// Remove anything that might already exist at the target location.
|
||||||
));
|
// This is safe even for a symlink leaf, remove_all removes the named inode in its parent dir.
|
||||||
}
|
root.remove_all(path).or_else(|e| match e.kind() {
|
||||||
|
pathrs::error::ErrorKind::OsError(Some(errno)) if errno == libc::ENOENT => Ok(()),
|
||||||
|
_ => Err(e),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Create parent directories if missing
|
// Create parent directories if missing
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
if !parent.exists() {
|
let dir = root
|
||||||
let dir = parent.to_path_buf();
|
.mkdir_all(
|
||||||
// Attempt to create directory, ignore AlreadyExists errors
|
parent,
|
||||||
if let Err(e) = fs::create_dir_all(&dir) {
|
&std::fs::Permissions::from_mode(req.dir_mode & IMPLICIT_DIRECTORY_PERMISSION_MASK),
|
||||||
if e.kind() != std::io::ErrorKind::AlreadyExists {
|
)
|
||||||
return Err(e.into());
|
.context("mkdir_all parent")?
|
||||||
}
|
.reopen(OpenFlags::O_DIRECTORY)
|
||||||
}
|
.context("reopen parent")?;
|
||||||
|
|
||||||
// Set directory permissions and ownership
|
// TODO(burgerdev): why are we only applying this to the immediate parent?
|
||||||
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(req.dir_mode))?;
|
unistd::fchown(
|
||||||
unistd::chown(
|
dir.as_raw_fd(),
|
||||||
&dir,
|
Some(Uid::from_raw(req.uid as u32)),
|
||||||
Some(Uid::from_raw(req.uid as u32)),
|
Some(Gid::from_raw(req.gid as u32)),
|
||||||
Some(Gid::from_raw(req.gid as u32)),
|
)
|
||||||
)?;
|
.context("fchown parent")?
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sflag = stat::SFlag::from_bits_truncate(req.file_mode);
|
let sflag = stat::SFlag::from_bits_truncate(req.file_mode);
|
||||||
|
|
||||||
if sflag.contains(stat::SFlag::S_IFDIR) {
|
if sflag.contains(stat::SFlag::S_IFDIR) {
|
||||||
// Remove existing non-directory file if present
|
// mkdir_all does not support the setuid/setgid/sticky bits, so we first create the
|
||||||
if path.exists() && !path.is_dir() {
|
// directory with the stricter mask and then change permissions with the correct mask.
|
||||||
fs::remove_file(&path)?;
|
let dir = root
|
||||||
}
|
.mkdir_all(
|
||||||
|
&path,
|
||||||
|
&std::fs::Permissions::from_mode(req.file_mode & IMPLICIT_DIRECTORY_PERMISSION_MASK),
|
||||||
|
)
|
||||||
|
.context("mkdir_all dir")?
|
||||||
|
.reopen(OpenFlags::O_DIRECTORY)
|
||||||
|
.context("reopen dir")?;
|
||||||
|
dir.set_permissions(std::fs::Permissions::from_mode(req.file_mode & EXPLICIT_DIRECTORY_PERMISSION_MASK))?;
|
||||||
|
|
||||||
fs::create_dir(&path).or_else(|e| {
|
unistd::fchown(
|
||||||
if e.kind() != std::io::ErrorKind::AlreadyExists {
|
dir.as_raw_fd(),
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(req.file_mode))?;
|
|
||||||
|
|
||||||
unistd::chown(
|
|
||||||
&path,
|
|
||||||
Some(Uid::from_raw(req.uid as u32)),
|
Some(Uid::from_raw(req.uid as u32)),
|
||||||
Some(Gid::from_raw(req.gid as u32)),
|
Some(Gid::from_raw(req.gid as u32)),
|
||||||
)?;
|
)
|
||||||
|
.context("fchown dir")?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle symlink creation
|
// Handle symlink creation
|
||||||
if sflag.contains(stat::SFlag::S_IFLNK) {
|
if sflag.contains(stat::SFlag::S_IFLNK) {
|
||||||
// Clean up existing path (whether symlink, dir, or file)
|
|
||||||
if path.exists() || path.is_symlink() {
|
|
||||||
// Use appropriate removal method based on path type
|
|
||||||
if path.is_symlink() {
|
|
||||||
unistd::unlink(&path)?;
|
|
||||||
} else if path.is_dir() {
|
|
||||||
fs::remove_dir_all(&path)?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(&path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new symbolic link
|
// Create new symbolic link
|
||||||
let src = PathBuf::from(OsStr::from_bytes(&req.data));
|
let src = PathBuf::from(OsStr::from_bytes(&req.data));
|
||||||
unistd::symlinkat(&src, None, &path)?;
|
root.create(path, &pathrs::InodeType::Symlink(src))
|
||||||
|
.context("create symlink")?;
|
||||||
// Set symlink ownership (permissions not supported for symlinks)
|
// Symlinks don't have permissions on Linux!
|
||||||
let path_str = CString::new(path.as_os_str().as_bytes())?;
|
|
||||||
|
|
||||||
let ret = unsafe { libc::lchown(path_str.as_ptr(), req.uid as u32, req.gid as u32) };
|
|
||||||
Errno::result(ret).map(drop)?;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tmpfile = path.clone();
|
// Write file content.
|
||||||
tmpfile.set_extension("tmp");
|
let flags = if req.offset == 0 {
|
||||||
|
OpenFlags::O_RDWR | OpenFlags::O_CREAT | OpenFlags::O_TRUNC
|
||||||
|
} else {
|
||||||
|
OpenFlags::O_RDWR | OpenFlags::O_CREAT
|
||||||
|
};
|
||||||
|
let file = root
|
||||||
|
.create_file(path, flags, &std::fs::Permissions::from_mode(req.file_mode & FILE_PERMISSION_MASK))
|
||||||
|
.context("create_file")?;
|
||||||
|
file.write_all_at(req.data.as_slice(), req.offset as u64)
|
||||||
|
.context("write_all_at")?;
|
||||||
|
// Things like umask can change the permissions after create, make sure that they stay
|
||||||
|
file.set_permissions(std::fs::Permissions::from_mode(req.file_mode & FILE_PERMISSION_MASK))
|
||||||
|
.context("set_permissions")?;
|
||||||
|
|
||||||
let file = OpenOptions::new()
|
unistd::fchown(
|
||||||
.write(true)
|
file.as_raw_fd(),
|
||||||
.create(true)
|
|
||||||
.truncate(req.offset == 0) // Only truncate when offset is 0
|
|
||||||
.open(&tmpfile)?;
|
|
||||||
|
|
||||||
file.write_all_at(req.data.as_slice(), req.offset as u64)?;
|
|
||||||
let st = stat::stat(&tmpfile)?;
|
|
||||||
|
|
||||||
if st.st_size != req.file_size {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
file.set_permissions(std::fs::Permissions::from_mode(req.file_mode))?;
|
|
||||||
|
|
||||||
unistd::chown(
|
|
||||||
&tmpfile,
|
|
||||||
Some(Uid::from_raw(req.uid as u32)),
|
Some(Uid::from_raw(req.uid as u32)),
|
||||||
Some(Gid::from_raw(req.gid as u32)),
|
Some(Gid::from_raw(req.gid as u32)),
|
||||||
)?;
|
)
|
||||||
|
.context("fchown")?;
|
||||||
// Remove existing target path before rename
|
|
||||||
if path.exists() || path.is_symlink() {
|
|
||||||
if path.is_dir() {
|
|
||||||
fs::remove_dir_all(&path)?;
|
|
||||||
} else {
|
|
||||||
fs::remove_file(&path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::rename(tmpfile, path)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2444,6 +2451,7 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{namespace::Namespace, protocols::agent_ttrpc_async::AgentService as _};
|
use crate::{namespace::Namespace, protocols::agent_ttrpc_async::AgentService as _};
|
||||||
|
use anyhow::ensure;
|
||||||
use nix::mount;
|
use nix::mount;
|
||||||
use nix::sched::{unshare, CloneFlags};
|
use nix::sched::{unshare, CloneFlags};
|
||||||
use oci::{
|
use oci::{
|
||||||
@@ -3465,4 +3473,274 @@ COMMIT
|
|||||||
assert_eq!(d.result, result, "{msg}");
|
assert_eq!(d.result, result, "{msg}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_do_copy_file() {
|
||||||
|
let temp_dir = tempdir().expect("creating temp dir failed");
|
||||||
|
// We start one directory deeper such that we catch problems when the shared directory does
|
||||||
|
// not exist yet.
|
||||||
|
let base = temp_dir.path().join("shared");
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
name: String,
|
||||||
|
request: CopyFileRequest,
|
||||||
|
assertions: Box<dyn Fn(&Path) -> Result<()>>,
|
||||||
|
should_fail: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = [
|
||||||
|
TestCase {
|
||||||
|
name: "Create a top-level file".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("f").to_string_lossy().into(),
|
||||||
|
file_mode: 0o644 | libc::S_IFREG,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let f = base.join("f");
|
||||||
|
let f_stat = fs::metadata(&f).context("stat ./f failed")?;
|
||||||
|
ensure!(f_stat.is_file());
|
||||||
|
ensure!(0o644 == f_stat.permissions().mode() & 0o777);
|
||||||
|
let content = std::fs::read_to_string(&f).context("read ./f failed")?;
|
||||||
|
ensure!(content.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Replace a top-level file".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("f").to_string_lossy().into(),
|
||||||
|
file_mode: 0o600 | libc::S_IFREG,
|
||||||
|
data: b"Hello!".to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let f = base.join("f");
|
||||||
|
let f_stat = fs::metadata(&f).context("stat ./f failed")?;
|
||||||
|
ensure!(f_stat.is_file());
|
||||||
|
ensure!(0o600 == f_stat.permissions().mode() & 0o777);
|
||||||
|
let content = std::fs::read_to_string(&f).context("read ./f failed")?;
|
||||||
|
ensure!("Hello!" == content);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a file and its parent directory".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/b").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o755 | libc::S_IFDIR,
|
||||||
|
file_mode: 0o644 | libc::S_IFREG,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let a_stat = fs::metadata(base.join("a")).context("stat ./a failed")?;
|
||||||
|
ensure!(a_stat.is_dir());
|
||||||
|
ensure!(0o755 == a_stat.permissions().mode() & 0o777);
|
||||||
|
let b_stat = fs::metadata(base.join("a/b")).context("stat ./a/b failed")?;
|
||||||
|
ensure!(b_stat.is_file());
|
||||||
|
ensure!(0o644 == b_stat.permissions().mode() & 0o777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a file within an existing directory".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/c").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that existing directories are not touched - we expect this to stay 0o755.
|
||||||
|
file_mode: 0o621 | libc::S_IFREG,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let a_stat = fs::metadata(base.join("a")).context("stat ./a failed")?;
|
||||||
|
ensure!(a_stat.is_dir());
|
||||||
|
ensure!(0o755 == a_stat.permissions().mode() & 0o777);
|
||||||
|
let c_stat = fs::metadata(base.join("a/c")).context("stat ./a/c failed")?;
|
||||||
|
ensure!(c_stat.is_file());
|
||||||
|
ensure!(0o621 == c_stat.permissions().mode() & 0o777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a directory".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/d").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that the permissions are taken from file_mode.
|
||||||
|
file_mode: 0o755 | libc::S_IFDIR,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let a_stat = fs::metadata(base.join("a")).context("stat ./a failed")?;
|
||||||
|
ensure!(a_stat.is_dir());
|
||||||
|
ensure!(0o755 == a_stat.permissions().mode() & 0o777);
|
||||||
|
let d_stat = fs::metadata(base.join("a/d")).context("stat ./a/d failed")?;
|
||||||
|
ensure!(d_stat.is_dir());
|
||||||
|
ensure!(0o755 == d_stat.permissions().mode() & 0o777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a dir onto an existing file".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/b").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that the permissions are taken from file_mode.
|
||||||
|
file_mode: 0o755 | libc::S_IFDIR,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let b_stat = fs::metadata(base.join("a/b")).context("stat ./a/b failed")?;
|
||||||
|
ensure!(b_stat.is_dir());
|
||||||
|
ensure!(0o755 == b_stat.permissions().mode() & 0o777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a file onto an existing dir".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/b").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o755 | libc::S_IFDIR,
|
||||||
|
file_mode: 0o644 | libc::S_IFREG,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let b_stat = fs::metadata(base.join("a/b")).context("stat ./a/b failed")?;
|
||||||
|
ensure!(b_stat.is_file());
|
||||||
|
ensure!(0o644 == b_stat.permissions().mode() & 0o777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a symlink".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/link").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that the permissions are taken from file_mode.
|
||||||
|
file_mode: 0o755 | libc::S_IFLNK,
|
||||||
|
data: b"/etc/passwd".to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let a_stat = fs::metadata(base.join("a")).context("stat ./a failed")?;
|
||||||
|
ensure!(a_stat.is_dir());
|
||||||
|
ensure!(0o755 == a_stat.permissions().mode() & 0o777);
|
||||||
|
let link = base.join("a/link");
|
||||||
|
let link_stat = nix::sys::stat::lstat(&link).context("stat ./a/link failed")?;
|
||||||
|
// Linux symlinks have no permissions!
|
||||||
|
ensure!(0o777 | libc::S_IFLNK == link_stat.st_mode);
|
||||||
|
let target = fs::read_link(&link).context("read_link ./a/link failed")?;
|
||||||
|
ensure!(target.to_string_lossy() == "/etc/passwd");
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a directory with setgid and sticky bit".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("x/y").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o3755 | libc::S_IFDIR,
|
||||||
|
file_mode: 0o3770 | libc::S_IFDIR,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
// Implicitly created directories should not get a sticky bit.
|
||||||
|
let x_stat = fs::metadata(base.join("x")).context("stat ./x failed")?;
|
||||||
|
ensure!(x_stat.is_dir());
|
||||||
|
ensure!(0o755 == x_stat.permissions().mode() & 0o7777);
|
||||||
|
// Explicitly created directories should.
|
||||||
|
let y_stat = fs::metadata(base.join("x/y")).context("stat ./x/y failed")?;
|
||||||
|
ensure!(y_stat.is_dir());
|
||||||
|
ensure!(0o3770 == y_stat.permissions().mode() & 0o7777);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// =================================
|
||||||
|
// Below are some adversarial tests.
|
||||||
|
// =================================
|
||||||
|
TestCase {
|
||||||
|
name: "Malicious intermediate directory is a symlink".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base
|
||||||
|
.join("a/link/this-could-just-be-shadow-but-I-am-not-risking-it")
|
||||||
|
.to_string_lossy()
|
||||||
|
.into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that the permissions are taken from file_mode.
|
||||||
|
file_mode: 0o755 | libc::S_IFLNK,
|
||||||
|
data: b"root:password:19000:0:99999:7:::\n".to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: true,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
let link_stat = nix::sys::stat::lstat(&base.join("a/link"))
|
||||||
|
.context("stat ./a/link failed")?;
|
||||||
|
ensure!(0o777 | libc::S_IFLNK == link_stat.st_mode);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a symlink onto an existing symlink".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/link").to_string_lossy().into(),
|
||||||
|
dir_mode: 0o700 | libc::S_IFDIR, // Test that the permissions are taken from file_mode.
|
||||||
|
file_mode: 0o755 | libc::S_IFLNK,
|
||||||
|
data: b"/etc".to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
// The symlink should be created at the same place (not followed), with the new content.
|
||||||
|
let a_stat = fs::metadata(base.join("a")).context("stat ./a failed")?;
|
||||||
|
ensure!(a_stat.is_dir());
|
||||||
|
ensure!(0o755 == a_stat.permissions().mode() & 0o777);
|
||||||
|
let link = base.join("a/link");
|
||||||
|
let link_stat = nix::sys::stat::lstat(&link).context("stat ./a/link failed")?;
|
||||||
|
// Linux symlinks have no permissions!
|
||||||
|
ensure!(0o777 | libc::S_IFLNK == link_stat.st_mode);
|
||||||
|
let target = fs::read_link(&link).context("read_link ./a/link failed")?;
|
||||||
|
ensure!(target.to_string_lossy() == "/etc");
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
name: "Create a file onto an existing symlink".into(),
|
||||||
|
request: CopyFileRequest {
|
||||||
|
path: base.join("a/link").to_string_lossy().into(),
|
||||||
|
file_mode: 0o600 | libc::S_IFREG,
|
||||||
|
data: b"Hello!".to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
should_fail: false,
|
||||||
|
assertions: Box::new(|base| -> Result<()> {
|
||||||
|
// The symlink itself should be replaced with the file, not followed.
|
||||||
|
let link = base.join("a/link");
|
||||||
|
let link_stat = nix::sys::stat::lstat(&link).context("stat ./a/link failed")?;
|
||||||
|
ensure!(0o600 | libc::S_IFREG == link_stat.st_mode);
|
||||||
|
let content = std::fs::read_to_string(&link).context("read ./a/link failed")?;
|
||||||
|
ensure!("Hello!" == content);
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let uid = unistd::getuid().as_raw() as i32;
|
||||||
|
let gid = unistd::getgid().as_raw() as i32;
|
||||||
|
|
||||||
|
for mut tc in tests {
|
||||||
|
println!("Running test case: {}", tc.name);
|
||||||
|
// Since we're in a unit test, using root ownership causes issues with cleaning the temp dir.
|
||||||
|
tc.request.uid = uid;
|
||||||
|
tc.request.gid = gid;
|
||||||
|
|
||||||
|
let res = do_copy_file(&tc.request, (&base).into());
|
||||||
|
if tc.should_fail != res.is_err() {
|
||||||
|
panic!("{}: unexpected do_copy_file result: {:?}", tc.name, res)
|
||||||
|
}
|
||||||
|
(tc.assertions)(&base).context(tc.name).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user