1
0
mirror of https://github.com/containers/youki synced 2024-11-22 17:02:00 +01:00

Init a selinux project (#2800)

* selinux_init

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* use_unimplemented

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* add_explanation

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* update

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* use_path_instead_of_str

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* use_thiserror

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

* use_struct

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>

---------

Signed-off-by: Hiroyuki Moriya <41197469+Gekko0114@users.noreply.github.com>
This commit is contained in:
Hiroyuki Moriya 2024-06-21 20:22:38 +09:00 committed by GitHub
parent af804e4c99
commit 6dd0d7f031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 665 additions and 1 deletions

@ -1,7 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["crates/*", "tests/contest/*", "tools/*"] members = ["crates/*", "tests/contest/*", "tools/*"]
exclude = ["experiment/seccomp"] exclude = ["experiment/seccomp", "experiment/selinux"]
[profile.release] [profile.release]
lto = true lto = true

102
experiment/selinux/Cargo.lock generated Normal file

@ -0,0 +1,102 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "proc-macro2"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "selinux"
version = "0.1.0"
dependencies = [
"nix",
"thiserror",
]
[[package]]
name = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

@ -0,0 +1,16 @@
[package]
name = "selinux"
version = "0.1.0"
description = "Library for selinux"
license-file = "../../LICENSE"
repository = "https://github.com/containers/youki"
homepage = "https://containers.github.io/youki"
readme = "README.md"
authors = ["youki team"]
edition = "2021"
autoexamples = true
keywords = ["youki", "container", "selinux"]
[dependencies]
nix = { version = "0.29.0", features = ["process", "fs"] }
thiserror = "1.0.61"

@ -0,0 +1,7 @@
This is an experimental project in order to create selinux library in Rust.
Ref: https://github.com/containers/youki/issues/2718.
Reimplementation of (selinux)[https://github.com/opencontainers/selinux] in Rust.
Also selinux depends on xattr, but nix doesn't cover xattr function.
Therefore, this PR will implement xattr in Rust.
Please import and use this project.

@ -0,0 +1,2 @@
pub mod selinux;
pub mod xattr;

@ -0,0 +1,469 @@
use crate::xattr::*;
use nix::unistd::gettid;
use nix::sys::statfs;
use nix::errno::Errno;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use std::os::fd::{AsFd, AsRawFd};
use std::sync::atomic::{AtomicBool, Ordering};
const XATTR_NAME_SELINUX: &str = "security.selinux";
const ERR_EMPTY_PATH: &str = "empty path";
#[derive(Debug, thiserror::Error)]
pub enum SELinuxError {
#[error("Failed to set file label for SELinux: {0}")]
SetFileLabel(String),
#[error("Failed to lset file label for SELinux: {0}")]
LSetFileLabel(String),
#[error("Failed to get file label for SELinux: {0}")]
FileLabel(String),
#[error("Failed to get lfile label for SELinux: {0}")]
LFileLabel(String),
#[error("Failed to call is_proc_handle for SELinux: {0}")]
IsProcHandle(String),
#[error("Failed to call read_con_fd for SELinux: {0}")]
ReadConFd(String),
#[error("Failed to call read_con for SELinux: {0}")]
ReadCon(String),
}
pub struct SELinux {
have_thread_self: AtomicBool,
init_done: AtomicBool,
}
impl Default for SELinux {
fn default() -> Self {
SELinux::new()
}
}
impl SELinux {
pub fn new() -> Self {
SELinux {
have_thread_self: AtomicBool::new(false),
init_done: AtomicBool::new(false),
}
}
// function similar with setDisabled in go-selinux repo.
// set_disabled disables SELinux support for the package.
pub fn set_disabled() {
unimplemented!("not implemented yet")
}
// function similar with getEnabled in go-selinux repo.
// get_enabled returns whether SELinux is enabled or not.
pub fn get_enabled() -> bool {
unimplemented!("not implemented yet")
}
// function similar with classIndex in go-selinux repo.
// classIndex returns the int index for an object class in the loaded policy,
// or -1 and an error.
pub fn class_index(class: &str) -> Result<i64, String> {
unimplemented!("not implemented yet")
}
// function similar with setFileLabel in go-selinux repo.
// set_file_label sets the SELinux label for this path, following symlinks, or returns an error.
pub fn set_file_label(fpath: &Path, label: &str) -> Result<(), SELinuxError> {
if !fpath.exists() {
return Err(SELinuxError::SetFileLabel(ERR_EMPTY_PATH.to_string()));
}
loop {
match set_xattr(fpath, XATTR_NAME_SELINUX, label.as_bytes(), 0) {
Ok(_) => break,
// TODO: This line will be fixed after implementing set_xattr.
Err(EINTR) => continue,
Err(e) => {
return Err(SELinuxError::SetFileLabel(
format!("set_xattr failed: {}", e),
));
}
}
}
Ok(())
}
// function similar with lSetFileLabel in go-selinux repo.
// lset_file_label sets the SELinux label for this path, not following symlinks,
// or returns an error.
pub fn lset_file_label(fpath: &Path, label: &str) -> Result<(), SELinuxError> {
if !fpath.exists() {
return Err(SELinuxError::LSetFileLabel(ERR_EMPTY_PATH.to_string()));
}
loop {
match lset_xattr(fpath, XATTR_NAME_SELINUX, label.as_bytes(), 0) {
Ok(_) => break,
// TODO: This line will be fixed after implementing lset_xattr.
Err(EINTR) => continue,
Err(e) => {
return Err(SELinuxError::LSetFileLabel(format!("lset_xattr failed: {}", e)));
}
}
}
Ok(())
}
// function similar with fileLabel in go-selinux repo.
// fileLabel returns the SELinux label for this path, following symlinks,
// or returns an error.
pub fn file_label(fpath: &Path) -> Result<String, SELinuxError> {
if !fpath.exists() {
return Err(SELinuxError::FileLabel(ERR_EMPTY_PATH.to_string()));
}
get_xattr(fpath, XATTR_NAME_SELINUX)
.map_err(|e| SELinuxError::FileLabel(e.to_string()))
}
// function similar with lFileLabel in go-selinux repo.
// lfile_label returns the SELinux label for this path, not following symlinks,
// or returns an error.
pub fn lfile_label(fpath: &Path) -> Result<String, SELinuxError> {
if !fpath.exists() {
return Err(SELinuxError::LFileLabel(ERR_EMPTY_PATH.to_string()));
}
lget_xattr(fpath, XATTR_NAME_SELINUX)
.map_err(|e| SELinuxError::LFileLabel(e.to_string()))
}
// function similar with setFSCreateLabel in go-selinux repo.
// set_fscreate_label sets the default label the kernel which the kernel is using
// for file system objects.
pub fn set_fscreate_label(&self, label: &str) -> Result<(), SELinuxError> {
return Self::write_con(self.attr_path("fscreate").as_path(), label);
}
// function similar with fsCreateLabel in go-selinux repo.
// fscreate_label returns the default label the kernel which the kernel is using
// for file system objects created by this task. "" indicates default.
pub fn fscreate_label(&self) -> Result<String, SELinuxError> {
return Self::read_con(self.attr_path("fscreate").as_path());
}
// function similar with currentLabel in go-selinux repo.
// current_label returns the SELinux label of the current process thread, or an error.
pub fn current_label(&self) -> Result<String, SELinuxError> {
return Self::read_con(self.attr_path("current").as_path());
}
// function similar with pidLabel in go-selinux repo.
// pid_label returns the SELinux label of the given pid, or an error.
pub fn pid_label(pid: i64) -> Result<String, SELinuxError> {
let file_name = &format!("/proc/{}/attr/current", pid);
let label = Path::new(file_name);
Self::read_con(label)
}
// function similar with execLabel in go-selinux repo.
// exec_label returns the SELinux label that the kernel will use for any programs
// that are executed by the current process thread, or an error.
pub fn exec_label(&self) -> Result<String, SELinuxError> {
return Self::read_con(self.attr_path("exec").as_path());
}
// function similar with SetExecLabel in go-selinux repo.
// set_exec_label sets the SELinux label that the kernel will use for any programs
// that are executed by the current process thread, or an error.
pub fn set_exec_label(label: &str) {
unimplemented!("not implemented yet")
}
// function similar with SetTaskLabel in go-selinux repo.
// set_task_label sets the SELinux label for the current thread, or an error.
// This requires the dyntransition permission.
pub fn set_task_label(label: &str) {
unimplemented!("not implemented yet")
}
// function similar with SetSocketLabel in go-selinux repo.
// set_socket_label takes a process label and tells the kernel to assign the
// label to the next socket that gets created.
pub fn set_socket_label(label: &str) {
unimplemented!("not implemented yet")
}
// function similar with SocketLabel in go-selinux repo.
// socket_label retrieves the current socket label setting.
pub fn socket_label() {
unimplemented!("not implemented yet")
}
// function similar with peerLabel in go-selinux repo.
// peer_label retrieves the label of the client on the other side of a socket.
pub fn peer_label() {
unimplemented!("not implemented yet")
}
// function similar with setKeyLabel in go-selinux repo.
// set_key_label takes a process label and tells the kernel to assign the
// label to the next kernel keyring that gets created.
pub fn set_key_label(label: &str) -> Result<(), SELinuxError> {
match Self::write_con(Path::new("/proc/self/attr/keycreate"), label) {
Ok(v) => Ok(v),
// TODO: This line will be fixed after implementing write_con.
Err(e) => Err(e),
}
}
// function similar with KeyLabel in go-selinux repo.
// key_label retrieves the current kernel keyring label setting
pub fn key_label() {
unimplemented!("not implemented yet")
}
// function similar with clearLabels in go-selinux repo.
// clear_labels clears all reserved labels.
pub fn clear_labels() {
unimplemented!("not implemented yet")
}
// function similar with reserveLabel in go-selinux repo.
// reserve_label reserves the MLS/MCS level component of the specified label
pub fn reserve_label(label: &str) {
unimplemented!("not implemented yet")
}
// function similar with roFileLabel in go-selinux repo.
// ro_file_label returns the specified SELinux readonly file label
pub fn ro_file_label() {
unimplemented!("not implemented yet")
}
// function similar with kvmContainerLabels in go-selinux repo.
// kvm_container_labels returns the default processLabel and mountLabel to be used
// for kvm containers by the calling process.
pub fn kvm_container_labels() {
unimplemented!("not implemented yet")
}
// function similar with initContainerLabels in go-selinux repo.
// init_container_labels returns the default processLabel and file labels to be
// used for containers running an init system like systemd by the calling process.
pub fn init_container_labels() {
unimplemented!("not implemented yet")
}
// function similar with containerLabels in go-selinux repo.
// container_labels returns an allocated processLabel and fileLabel to be used for
// container labeling by the calling process.
pub fn container_labels() {
unimplemented!("not implemented yet")
}
// function similar with PrivContainerMountLabel in go-selinux repo.
// priv_container_mount_label returns mount label for privileged containers.
pub fn priv_container_mount_label() {
unimplemented!("not implemented yet")
}
// function similar with FormatMountLabel in go-selinux repo.
// format_mount_label returns a string to be used by the mount command.
// Using the SELinux `context` mount option.
// Changing labels of files on mount points with this option can never be changed.
// format_mount_label returns a string to be used by the mount command.
// The format of this string will be used to alter the labeling of the mountpoint.
// The string returned is suitable to be used as the options field of the mount command.
// If you need to have additional mount point options, you can pass them in as
// the first parameter. The second parameter is the label that you wish to apply
// to all content in the mount point.
pub fn format_mount_label(src: &str, mount_label: &str) -> String {
Self::format_mount_label_by_type(src, mount_label, "context")
}
// function similar with FormatMountLabelByType in go-selinux repo.
// format_mount_label_by_type returns a string to be used by the mount command.
// Allow caller to specify the mount options. For example using the SELinux
// `fscontext` mount option would allow certain container processes to change
// labels of files created on the mount points, where as `context` option does not.
pub fn format_mount_label_by_type(src: &str, mount_label: &str, context_type: &str) -> String {
let mut formatted_src = src.to_owned();
if !mount_label.is_empty() {
if formatted_src.is_empty() {
formatted_src = format!("{}=\"{}\"", context_type, mount_label);
} else {
formatted_src = format!("{},{}=\"{}\"", formatted_src, context_type, mount_label);
}
}
formatted_src
}
// function similar with writeCon in go-selinux repo.
pub fn write_con(fpath: &Path, val: &str) -> Result<(), SELinuxError> {
unimplemented!("not implemented yet");
}
// function similar with isProcHandle in go-selinux repo.
pub fn is_proc_handle(file: &File) -> Result<(), SELinuxError> {
loop {
match statfs::fstatfs(file.as_fd()) {
Ok(stat) if stat.filesystem_type() == statfs::PROC_SUPER_MAGIC => break,
Ok(_) => {
return Err(SELinuxError::IsProcHandle(format!("file {} is not on procfs", file.as_raw_fd())
));
},
Err(Errno::EINTR) => continue,
Err(err) => {
return Err(SELinuxError::IsProcHandle(format!("fstatfs failed: {}", err)))
}
}
}
Ok(())
}
// function similar with readConFd in go-selinux repo.
pub fn read_con_fd(file: &mut File) -> Result<String, SELinuxError> {
let mut data = String::new();
file.read_to_string(&mut data)
.map_err(|e| SELinuxError::ReadConFd(e.to_string()))?;
// Remove null bytes on the end of a file.
let trimmed_data = data.trim_end_matches(char::from(0));
Ok(trimmed_data.to_string())
}
// function similar with readCon in go-selinux repo.
pub fn read_con(fpath: &Path) -> Result<String, SELinuxError> {
if fpath.as_os_str().is_empty() {
return Err(SELinuxError::ReadCon(ERR_EMPTY_PATH.to_string()));
}
let mut in_file = File::open(fpath)
.map_err(|e| SELinuxError::ReadCon(format!("failed to open file: {}", e)))?;
Self::is_proc_handle(&in_file)?;
Self::read_con_fd(&mut in_file)
}
// function similar with attrPath in go-selinux repo.
// attr_path determines the correct file path for accessing SELinux
// attributes of a process or thread in a Linux environment.
pub fn attr_path(&self, attr: &str) -> PathBuf {
// Linux >= 3.17 provides this
const THREAD_SELF_PREFIX: &str = "/proc/thread-self/attr";
// Avoiding code conflicts and ensuring thread-safe execution once only.
if !self.init_done.load(Ordering::SeqCst) {
let path = PathBuf::from(THREAD_SELF_PREFIX);
let is_dir = path.is_dir();
self.have_thread_self.store(is_dir, Ordering::SeqCst);
self.init_done.store(true, Ordering::SeqCst);
}
if self.have_thread_self.load(Ordering::SeqCst) {
return PathBuf::from(&format!("{}/{}", THREAD_SELF_PREFIX, attr));
}
PathBuf::from(&format!("/proc/self/task/{}/attr/{}", gettid(), attr))
}
}
#[cfg(test)]
mod tests {
use crate::selinux::*;
use std::fs::{self, File};
use std::path::Path;
use std::io::Write;
fn create_temp_file(content: &[u8], file_name: &str) {
let path = Path::new(file_name);
let mut file = File::create(&path).expect("Failed to create file");
file.write_all(content).expect("Failed to write to file");
file.sync_all().expect("Failed to sync file");
}
#[test]
fn test_format_mount_label() {
let src_array = ["", "src", "src"];
let mount_label_array = ["foobar", "foobar", ""];
let expected_array = ["context=\"foobar\"", "src,context=\"foobar\"", "src"];
for (i, src) in src_array.iter().enumerate() {
let mount_label = mount_label_array[i];
let expected = expected_array[i];
assert_eq!(
SELinux::format_mount_label(src, mount_label),
expected
);
}
}
#[test]
fn test_format_mount_label_by_type() {
let src_array = ["", "src", "src"];
let mount_label_array = ["foobar", "foobar", ""];
let context_array = ["fscontext", "fscontext", "rootcontext"];
let expected_array = ["fscontext=\"foobar\"", "src,fscontext=\"foobar\"", "src"];
for (i, src) in src_array.iter().enumerate() {
let mount_label = mount_label_array[i];
let context = context_array[i];
let expected = expected_array[i];
assert_eq!(
SELinux::format_mount_label_by_type(src, mount_label, context),
expected
);
}
}
#[test]
fn test_read_con_fd() {
let content_array: Vec<&[u8]> = vec![b"Hello, world\0", b"Hello, world\0\0\0", b"Hello,\0world"];
let expected_array = ["Hello, world", "Hello, world", "Hello,\0world"];
let file_name = "test.txt";
for (i, content) in content_array.iter().enumerate() {
let expected = expected_array[i];
create_temp_file(content, file_name);
// Need to open again to get read permission.
let mut file = File::open(file_name).expect("Failed to open file");
let result = SELinux::read_con_fd(&mut file).expect("Failed to read file");
assert_eq!(result, expected);
fs::remove_file(file_name).expect("Failed to remove test file");
}
}
#[test]
fn test_attr_path() {
let selinux = SELinux::new();
// Test with "/proc/thread-self/attr" path (Linux >= 3.17)
let attr = "bar";
let expected_name = &format!("/proc/thread-self/attr/{}", attr);
let expected_path = Path::new(expected_name);
let actual_path = selinux.attr_path(attr);
assert_eq!(expected_path, actual_path);
// Test with not having "/proc/thread-self/attr" path by setting HAVE_THREAD_SELF as false
selinux.init_done.store(true, Ordering::SeqCst);
selinux.have_thread_self.store(false, Ordering::SeqCst);
let thread_id = gettid();
let expected_name = &format!("/proc/self/task/{}/attr/{}", thread_id, attr);
let expected_path = Path::new(expected_name);
let actual_path = selinux.attr_path(attr);
assert_eq!(expected_path, actual_path);
}
#[test]
fn test_is_proc_handle() {
let filename_array = ["/proc/self/status", "/tmp/testfile"];
let expected_array = [true, false];
for (i, filename) in filename_array.iter().enumerate() {
let expected_ok = expected_array[i];
let path = Path::new(filename);
let file = match File::open(path) {
Ok(file) => file,
Err(_) => {
create_temp_file(b"", filename);
File::open(path).expect("failed to open file")
}
};
let result = SELinux::is_proc_handle(&file);
if expected_ok {
assert!(result.is_ok(), "Expected Ok, but got Err: {:?}", result);
} else {
assert!(result.is_err(), "Expected Err, but got Ok");
}
}
}
}

@ -0,0 +1,3 @@
mod xattr;
pub use xattr::*;

@ -0,0 +1,65 @@
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum XattrError {
#[error("Failed to set_xattr: {0}")]
SetXattr(String),
#[error("Failed to lset_xattr: {0}")]
LSetXattr(String),
#[error("Failed to get_xattr: {0}")]
GetXattr(String),
#[error("Failed to call lget_xattr: {0}")]
LGetXattr(String),
}
// function similar with setxattr in golang.org/x/sys/unix repo.
// set_xattr sets extended attributes on a file specified by its path.
pub fn set_xattr(fpath: &Path, attr: &str, data: &[u8], flags: i64) -> Result<(), XattrError> {
unimplemented!("not implemented yet")
}
// function similar with lsetxattr in golang.org/x/sys/unix repo.
// lset_xattr sets extended attributes on a symbolic link.
pub fn lset_xattr(fpath: &Path, attr: &str, data: &[u8], flags: i64) -> Result<(), XattrError> {
unimplemented!("not implemented yet")
}
// function similar with getattr in go-selinux repo.
// get_xattr returns the value of an extended attribute attr set for path.
pub fn get_xattr(fpath: &Path, attr: &str) -> Result<String, XattrError> {
unimplemented!("not implemented yet")
/*
match label {
Ok(mut v) => {
if (!v.is_empty()) && (v.chars().last() == Some('\x00')) {
v = (&v[0..v.len() - 1]).to_string();
}
return Ok(v);
},
Err(e) => return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("get_xattr failed: {}", e),
))
}
*/
}
// function similar with lgetxattr in go-selinux repo.
// lget_xattr returns the value of an extended attribute attr set for path.
pub fn lget_xattr(fpath: &Path, attr: &str) -> Result<String, XattrError> {
unimplemented!("not implemented yet")
/*
match label {
Ok(mut v) => {
if (!v.is_empty()) && (v.chars().last() == Some('\x00')) {
v = (&v[0..v.len() - 1]).to_string();
}
return Ok(v);
},
Err(e) => return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("lget_xattr failed: {}", e),
))
}
*/
}