【译】使用rust来写一个容器(七)

  1. 1. 系统调用与资源限制
    1. 1.1. 系统调用限制
      1. 1.1.1. 什么是系统调用(syscall)?
      2. 1.1.2. Seccomp 和系统调用限制
        1. 1.1.2.1. 要对哪些系统调用进行限制?
      3. 1.1.3. 应用seccomp
        1. 1.1.3.1. 无条件系统调用限制(Unconditionnal syscalls restriction)
        2. 1.1.3.2. 条件系统调用限制(Conditionnal syscalls restriction)
      4. 1.1.4. 测试
        1. 1.1.4.1. Patch for this step
    2. 1.2. 资源限制
      1. 1.2.1. Cgroups
        1. 1.2.1.1. 限制CPU使用时长
      2. 1.2.2. Rlimit
      3. 1.2.3. 限制资源
      4. 1.2.4. 清除限制
      5. 1.2.5. 测试
        1. 1.2.5.1. Patch for this step

本文章为对 Litchi Pi《Writing a container in Rust》的翻译转载,不享受任何著作权利,不用于任何商业目的,不以任何许可证进行授权,不对任何转载行为尤其是商业转载行为负责。一切权利均由原作者 Litchi Pi 保有。个人翻译能力有限,如有疑问可查看原文。

系统调用与资源限制

系统调用限制

什么是系统调用(syscall)?

当应用程序因错误而崩溃时,必须以不影响底层操作系统的方式来解决问题。
想象一下,如果你的的俄罗斯方块游戏在保存高分时崩溃了。 由于它没有结束对磁盘的操作,因此可能会损坏磁盘,或者会对其他程序使用磁盘造成麻烦。

这一原则的一个有趣的例子是通过非常著名的Windows 98 现场失败进行现场演示,这也是预录制视频取代现场演示的原因.

为了避免这种情况,Linux 将软件分为两个“区域”:内核区域(Kernel-land)和用户区域(User-land)。

内核区域(Kernel-land)具有特权,这意味着它可以完全掌控整个机器(除了一些例外情况,例如 Arm TrustZone 的处理器级上下文分离),它可以读取和写入所有内存,并使用驱动程序与连接的外设进行交互。

用户区域(User-land)是无特权的,这意味着即使root帐户也不能直接完全控制机器,但这种权限允许运行所有系统调用。

系统调用(syscall)是用户区域(User-land)的应用程序执行需要内核来完成的操作的方式。

syscalls是一种特殊的汇编机器代码,每个系统调用都有一个“索引”,这个索引被传递到一个寄存器中,当执行syscall命令时,它会切换到负责处理对应系统调用的内核代码。

下面是使用write系统调用写入磁盘的应用程序的表示。

当在Python等其他语言中使用write时,通常会调用C的write函数,因为该函数在编译时会直接翻译为相应的系统调用。

驱动程序作为模块嵌入到内核中,并在内部管理其内部状态和操作。 通过这种形式,当发生错误并且另一个应用程序想要执行写入操作时,它可以重置其状态,处理底层物理设备所需的特殊操作(我正在看着你,eMMC!)

有关 Linux 内核驱动程序的更多信息,请参阅本文(说真的,为了保护您的眼睛,请注意文章的颜色)。

此处提供了可以使用 Linux 内核调用的所有系统调用的完整列表

Seccomp 和系统调用限制

由于系统调用允许用户控制系统,因此我们需要限制可能允许容器内的处理损害我们的底层操作系统的系统调用。

让我们了解一下Seccomp

正如seccomp的维基百科页面所给出的:

seccomp(secure computing mode,即安全计算模式的缩写)是 Linux 内核中的计算机安全设施。
seccomp 允许进程单向转换到“安全”状态,在这种状态下,除了对已打开的文件描述符执行 exit()、sigreturn()、read() 和 write() 之外,它不能进行任何系统调用。 如果它尝试进行任何其他系统调用,内核将使用SIGKILLSIGSYS终止该进程。 从这个意义上说,它并不是虚拟化系统的资源,而是将进程与系统资源完全隔离。

通过 prctl(2)系统调用使用PR_SET_SECCOMP参数或者(自 Linux 内核 3.17 起)通过 seccomp(2) 系统调用可启用seccomp模式。

译者注

prctl是基本的进程管理函数,最原始的沙箱规则就是通过prctl函数来实现的,它可以决定可调用的系统调用函数。当第一个参数是PR_SET_SECCOMP,第二个参数为1的时候(严格模式),允许的系统调用只有read,write,exit和sigereturn。
在Linux 3.5内核版本中, 引入了seccomp第二种匹配模式:SECCOMP_MODE_FILTER。 该模式使用BPF程序做过滤规则匹配。

我们很容易看出 seccomp 是 Docker 等容器的支柱之一,它将进程隔离到只能读取和写入文件系统或退出的状态。

在默认情况下,这种安全计算模式具有很大的限制性,因为它拒绝了所有限制外的系统调用尝试。为了容器的良好功能,我们可能需要对其进行配置并添加例外。
为此,我们可以为 seccomp 设置一个定义了特殊规则、允许某些系统调用或触发特殊操作的配置文件。
对于我们的容器来说,我们要做的就是配置 seccomp 以默认允许所有系统调用,然后使用配置文件来拒绝某些系统调用。

要对哪些系统调用进行限制?

在本教程中,我们不会仔细研究我们将拒绝/允许的每个系统调用,因为原始教程的“系统调用”部分给出了一些利用示例的深入描述。
此外,正如原始教程中所指出的,你可以通过docker文档moby 的 seccomp 配置文件来获取系统调用限制相关的配置资源。

以下是我们将在容器中拒绝的系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Kernel keyring
keyctl
add_key
request_key

// NUMA (memory management)
mbind
migrate_pages
move_pages
set_mempolicy

// Allow userland to handle memory faults in the kernel
userfaultfd

// Trace / profile syscalls
perf_event_open

有关我们限制的一些额外资源:
Kernel keyring
NUMA
Userland memory handling
Syscalls tracing

应用seccomp

我们通过syscallz包来对子进程的seccomp进行限制,此外这个操作也依赖libc, 以下是Cargo.toml新增内容:

1
2
3
4
[dependencies]
# ...
syscallz = "0.16.1"
libc = "0.2.102"

接下来我们新建一个src/syscalls.rs文件,像这样创建一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use syscallz::{Context, Action};

pub fn setsyscalls() -> Result<(), Errcode> {
log::debug!("Refusing / Filtering unwanted syscalls");

// Unconditionnal syscall deny

// Conditionnal syscall deny

// Initialize seccomp profile with all syscalls allowed by default
if let Ok(mut ctx) = Context::init_with_action(Action::Allow) {

// Configure profile here

if let Err(_) = ctx.load(){
return Err(Errcode::SyscallsError(0));
}

Ok(())
} else {
Err(Errcode::SyscallsError(1))
}
}

接下来我们引入这个模块,然后在子配置函数中调用此函数。
修改src/main.rs:

1
2
3
// ...
mod syscalls;

修改src/errors.rs:

1
2
3
4
pub enum Errcode {
// ...
SyscallsError(u8),
}

接下来是src/child.rs

1
2
3
4
5
6
7
use crate::syscalls::setsyscalls;

pub fn setup_container_configurations(config: &ContainerOpts) -> Result<(), Errcode> {
// ...
setsyscalls()?;
Ok(())
}

无条件系统调用限制(Unconditionnal syscalls restriction)

让我们先来拒绝我们不希望子进程执行的系统调用。
为此,我们创建函数refuse_syscall来完全拒绝在子进程中调用该系统调用的任何尝试。

1
2
3
4
5
6
7
8
const EPERM: u16 = 1;

fn refuse_syscall(ctx: &mut Context, sc: &Syscall) -> Result<(), Errcode>{
match ctx.set_action_for_syscall(Action::Errno(EPERM), *sc){
Ok(_) => Ok(()),
Err(_) => Err(Errcode::SyscallsError(2)),
}
}

接下来,我们列出要拒绝的系统调用,并遍历它们以填充配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use crate::syscallz::Syscall;

pub fn setsyscalls() -> Result<(), Errcode> {
// ...
// Unconditionnal syscall deny
let syscalls_refused = [
Syscall::keyctl,
Syscall::add_key,
Syscall::request_key,
Syscall::mbind,
Syscall::migrate_pages,
Syscall::move_pages,
Syscall::set_mempolicy,
Syscall::userfaultfd,
Syscall::perf_event_open,
];

if let Ok(mut ctx) = Context::init_with_action(Action::Allow){
// ...

for sc in syscalls_refused.iter() {
refuse_syscall(&mut ctx, sc)?;
}

// ...
}
}

条件系统调用限制(Conditionnal syscalls restriction)

当满足特定条件时,限制系统调用。
为此,我们创建一个规则,该规则接受一个值并返回是否应设置权限。 由于我们有此功能的基本用法,因此我们只需测试变量是否等于预期值。
让我们创建refuse_if_comp函数来实现这一点:

1
2
3
4
5
6
7
8
9
use syscallz::{Comparator, Cmp};

fn refuse_if_comp(ctx: &mut Context, ind: u32, sc: &Syscall, biteq: u64)-> Result<(), Errcode>{
match ctx.set_rule_for_syscall(Action::Errno(EPERM), *sc,
&[Comparator::new(ind, Cmp::MaskedEq, biteq, Some(biteq))]){
Ok(_) => Ok(()),
Err(_) => Err(Errcode::SyscallsError(3)),
}
}

这个Comparator将获取传递给系统调用的参数号ind,并使用掩码biteq与值biteq进行比较。
这相当于测试biteq位是否已设置。
让我们添加要为系统调用设置的所有规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use libc::TIOCSTI;
use nix::sys::stat::Mode;
use nix::sched::CloneFlags;

pub fn setsyscalls() -> Result<(), Errcode> {
// ...

let s_isuid: u64 = Mode::S_ISUID.bits().into();
let s_isgid: u64 = Mode::S_ISGID.bits().into();
let clone_new_user: u64 = CloneFlags::CLONE_NEWUSER.bits() as u64;

// Conditionnal syscall deny
let syscalls_refuse_ifcomp = [
(Syscall::chmod, 1, s_isuid),
(Syscall::chmod, 1, s_isgid),

(Syscall::fchmod, 1, s_isuid),
(Syscall::fchmod, 1, s_isgid),

(Syscall::fchmodat, 2, s_isuid),
(Syscall::fchmodat, 2, s_isgid),

(Syscall::unshare, 0, clone_new_user),
(Syscall::clone, 0, clone_new_user),

(Syscall::ioctl, 1, TIOCSTI),
];

if let Ok(mut ctx) = Context::init_with_action(Action::Allow){
// ...
for (sc, ind, biteq) in syscalls_refuse_ifcomp.iter(){
refuse_if_comp(&mut ctx, *ind, sc, *biteq)?;
}
// ...
}
}

测试

现在一切准备就绪,让我们简单测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[2022-03-09T09:08:27Z INFO  crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
[2022-03-09T09:08:27Z DEBUG crabcan::container] Linux release: 5.13.0-30-generic
[2022-03-09T09:08:27Z DEBUG crabcan::container] Container sockets: (3, 4)
[2022-03-09T09:08:27Z DEBUG crabcan::hostname] Container hostname is now soft-world-116
[2022-03-09T09:08:27Z DEBUG crabcan::mounts] Setting mount points ...
[2022-03-09T09:08:27Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.Qo04pP4PBG9U
[2022-03-09T09:08:27Z DEBUG crabcan::mounts] Pivoting root
[2022-03-09T09:08:27Z DEBUG crabcan::mounts] Unmounting old root
[2022-03-09T09:08:27Z DEBUG crabcan::namespaces] Setting up user namespace with UID 0
[2022-03-09T09:08:27Z DEBUG crabcan::namespaces] Child UID/GID map done, sending signal to child to continue...
[2022-03-09T09:08:27Z DEBUG crabcan::container] Creation finished
[2022-03-09T09:08:27Z DEBUG crabcan::container] Container child PID: Some(Pid(130688))
[2022-03-09T09:08:27Z DEBUG crabcan::container] Waiting for child (pid 130688) to finish
[2022-03-09T09:08:27Z INFO crabcan::namespaces] User namespaces set up
[2022-03-09T09:08:27Z DEBUG crabcan::namespaces] Switching to uid 0 / gid 0...
[2022-03-09T09:08:27Z DEBUG crabcan::capabilities] Clearing unwanted capabilities ...
[2022-03-09T09:08:27Z DEBUG crabcan::syscalls] Refusing / Filtering unwanted syscalls
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 1024, datum_b: 1024 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=fchmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=fchmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 1024, datum_b: 1024 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=fchmodat comparators=[Comparator { arg: 2, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=fchmodat comparators=[Comparator { arg: 2, op: MaskedEq, datum_a: 1024, datum_b: 1024 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=unshare comparators=[Comparator { arg: 0, op: MaskedEq, datum_a: 268435456, datum_b: 268435456 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=clone comparators=[Comparator { arg: 0, op: MaskedEq, datum_a: 268435456, datum_b: 268435456 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=ioctl comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 21522, datum_b: 21522 }]
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=keyctl
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=add_key
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=request_key
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=mbind
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=migrate_pages
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=move_pages
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=set_mempolicy
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=userfaultfd
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=perf_event_open
[2022-03-09T09:08:27Z DEBUG syscallz] seccomp: loading policy
[2022-03-09T09:08:27Z INFO crabcan::child] Container set up successfully
[2022-03-09T09:08:27Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash"]
[2022-03-09T09:08:27Z DEBUG crabcan::container] Finished, cleaning & exit
[2022-03-09T09:08:27Z DEBUG crabcan::container] Cleaning container
[2022-03-09T09:08:27Z DEBUG crabcan::errors] Exit without any error, returning 0

它会生成一些日志记录,因为syscallz包和我们的我们的项目一样设置了log
它很好地表明我们的系统调用现在已被过滤,我们可以进入下一步!

Patch for this step

这一步的代码可以在github litchipi/crabcan branch “step13”中找到.
前一步到这一步的原始补丁可以在此处找到

资源限制

Cgroups

Cgroups 是 Linux v2.6.4 中引入的一种机制,允许为一组进程“分配”资源。
对于给定的进程组,系统“看起来”只拥有 X 个给定资源。

资源数量不能高于系统最初拥有的数量,它不是虚拟化功能,而是限制功能。

在Linux系统中,您可以使用/sys/fs/cgroup/来设置进程限制,方法如下:

1
2
#     100 Mib
echo 100000000 > /sys/fs/cgroup/memory/<groupname>/memory.limit_in_bytes

由于此功能已被重新设计,因此两个版本在 Linux 内核中共存。
对于我们用户来说,主要区别在于v2将给定组的所有配置分组在同一目录下。

有关它们的更多信息,您可以查看 LWN 上的这一系列精彩文章

此功能在同一服务器上的应用程序容器化中大量使用,并且能够向客户出售服务器上的一组特定性能,然后在客户购买性能提升时进行升级。

此功能对于容器非常重要,以至于最近的一个漏洞让攻击者可以逃逸出容器并直接感染主机系统。 您必须记住,当我们向所包含的应用程序授予完全权限时,如果该应用程序成功逃逸出沙盒环境,它可能会在主机系统上保留其权限。

译者注

Cgroup与容器的关系可参考以下文章:
一篇搞懂容器技术的基石: cgroup

限制CPU使用时长

为了限制CPU的使用,cgroup使用权重来确定进程将获得多少CPU使用时间。
默认情况下,cgroup 将授予进程权重 1024,但该权重可以从1到2^64的范围内进行调整。
与其他程序相比,权重越大,CPU份额就越多。

如果 3 个进程的权重都为 25000,则它们的 CPU 时间与全部为 1024 时的 CPU 时间相同。较大的值范围允许对该值进行微调。

更多关于CPU共享的信息可以查看redhat的这篇文章

Rlimit

Rlimit 是一个用于限制单个进程的系统。
它的重点更集中于该进程可以做什么,而不是它消耗的实时系统资源。

有关 rlimit 的详细信息,您可以查看这篇文章,我从中提取了所有可用 rlimit 的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
RLIMIT_CPU         // CPU time limit given in seconds                                                   
// 最大允许的CPU使用时间,秒为单位。
RLIMIT_FSIZE // the maximum size of files that a process may create
// 进程可建立的文件的最大长度。
RLIMIT_DATA // the maximum size of the process's data segment
// 进程数据段的最大值。
RLIMIT_STACK // the maximum size of the process stack in bytes
// 最大的进程堆栈,以字节为单位
RLIMIT_CORE // the maximum size of a core file.
// 内核转存文件的最大长度。
RLIMIT_RSS // the number of bytes that can be allocated for a process in RAM
// 进程驻留内存的页数的大小限制
RLIMIT_NPROC // the maximum number of processes that can be created by a user
// 当前进程所属的真实id对应的用户所能创建的最大进程数
RLIMIT_NOFILE // the maximum number of a file descriptor that can be opened by a process
// 一个进程能打开文件个数的最大值
RLIMIT_MEMLOCK // the maximum number of bytes of memory that may be locked into RAM by mlock.
// 进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_AS // the maximum size of virtual memory in bytes.
// 进程的最大虚内存空间,字节为单位。
RLIMIT_LOCKS // the maximum number flock and locking related fcntl calls
// 进程可建立的文件锁数量最大值。
RLIMIT_SIGPENDING // maximum number of signals that may be queued for a user of the calling process
// 表示进程信号等待队列最大大小
RLIMIT_MSGQUE // the number of bytes that can be allocated for POSIX message queues
// 进程可为POSIX消息队列分配的最大字节数
RLIMIT_NICE // the maximum nice value that can be set by a process
// 进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_RTPRIO // maximum real-time priority value
// 进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_RTTIME // maximum number of microseconds that a process may be scheduled under real-time scheduling policy without making blocking system call
// 实时进程timer最大超时时间

我们将使用 rlimit 来限制进程可以打开的文件描述符的数量,因为,如原始教程中所述:

文件描述符数量与 pid 数量一样,是针对每个用户的,因此我们希望防止容器内进程占用所有文件描述符数量。

所以我们需要设置RLIMIT_NOFILE

从理论上讲,rlimit 和 cgroups 的限制可能“重叠”(先达到上限的规则会成为实际限制),但实际上它们的应用领域不同,这种情况几乎不会发生。

注意,由于rlimit使用系统调用,因此进程可以重新配置这些设置,这就是我们将CAP_SYS_RESOURCE添加到系统调用黑名单中的原因。

限制资源

有一个叫做cgroups_rs的包可以简化与cgroups定义相关的所有内容,但请记住,由于Unix中一切皆文件(Linux 遵循 Unix 的哲学),即使我们没有这个包,我们要完成这项工作也只需要写入正确的文件

我们还将使用rlimit包来包装对我们需要的系统调用的调用。

在新文件src/resources.rs中,我们创建一个函数来限制容器内的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use rlimit::{setrlimit, Resource};
use cgroups_rs::cgroup_builder::CgroupBuilder;

// KiB MiB Gib
const KMEM_LIMIT: i64 = 1024 * 1024 * 1024;
const MEM_LIMIT: i64 = KMEM_LIMIT;
const MAX_PID: MaxValue = MaxValue::Value(64);
const NOFILE_RLIMIT: u64 = 64;

pub fn restrict_resources(hostname: &String) -> Result<(), Errcode>{
log::debug!("Restricting resources for hostname {}", hostname);

// Cgroups
let cgs = CgroupBuilder::new(hostname)

// Allocate less CPU time than other processes
.cpu().shares(256).done()

// Limiting the memory usage to 1 GiB
// The user can limit it to less than this, never increase above 1Gib
.memory().kernel_memory_limit(KMEM_LIMIT).memory_hard_limit(MEM_LIMIT).done()

// This process can only create a maximum of 64 child processes
.pid().maximum_number_of_processes(MAX_PID).done()

// Give an access priority to block IO lower than the system
.blkio().weight(50).done()

.build(Box::new(V2::new()));

// We apply the cgroups rules to the child process we just created
let pid : u64 = pid.as_raw().try_into().unwrap();
if let Err(_) = cgs.add_task(CgroupPid::from(pid)) {
return Err(Errcode::ResourcesError(0));
};


// Rlimit
// Can create only 64 file descriptors
if let Err(_) = setrlimit(Resource::NOFILE, NOFILE_RLIMIT, NOFILE_RLIMIT){
return Err(Errcode::ResourcesError(0));
}

Ok(())
}

我们可以在容器的create函数中使用这个新函数,因为它只需要主机名来应用限制。

1
2
3
4
5
6
7
8
9
10
11
use crate::resources::restrict_resources;

impl Container {
// ...

pub fn create(&mut self) -> Result<(), Errcode> {
let pid = generate_child_process(self.config.clone())?;
restrict_resources(&self.config.hostname, pid)?;
// ...
}
}

接下来在Cargo.toml中添加依赖

1
2
3
4
[dependencies]
# ...
cgroups-rs = "0.2.6"
rlimit = "0.6.2"

同样的,在src/main.rs中添加这个模块

1
2
// ...
mod resources;

src/error.rs中添加错误变体

1
2
3
4
pub enum Errcode {
// ...
ResourcesError(u8),
}

清除限制

子进程退出后,我们需要清除所有添加的cgroup限制。
这非常简单,因为cgroups v2将所有内容集中在/sys/fs/cgroup/<groupname>/下的目录中,因此我们只需删除它即可。

src/resources.rs中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub fn clean_cgroups(hostname: &String) -> Result<(), Errcode>{
log::debug!("Cleaning cgroups");
match canonicalize(format!("/sys/fs/cgroup/{}/", hostname)){
Ok(d) => {
if let Err(_) = remove_dir(d) {
return Err(Errcode::ResourcesError(2));
}
},
Err(e) => {
log::error!("Error while canonicalize path: {}", e);
return Err(Errcode::ResourcesError(3));
}
}
Ok(())
}

该函数将在文件src/container.rsclean_exit函数中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use crate::resources::clean_cgroups;

impl Container {
// ...

pub fn clean_exit(&mut self) -> Result<(), Errcode> {
// ...

if let Err(e) = clean_cgroups(&self.config.hostname){
log::error!("Cgroups cleaning failed: {}", e);
return Err(e);
}

Ok(())
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[2022-03-09T13:58:37Z INFO  crabcan] Args { debug: true, command: "/bin/bash", uid: 0, mount_dir: "./mountdir/" }
[2022-03-09T13:58:37Z DEBUG crabcan::container] Linux release: 5.13.0-30-generic
[2022-03-09T13:58:37Z DEBUG crabcan::container] Container sockets: (3, 4)
[2022-03-09T13:58:37Z DEBUG crabcan::resources] Restricting resources for hostname small-girl-247
[2022-03-09T13:58:37Z DEBUG crabcan::hostname] Container hostname is now small-girl-247
[2022-03-09T13:58:37Z DEBUG crabcan::mounts] Setting mount points ...
[2022-03-09T13:58:37Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.LH9HSKzfsmN7
[2022-03-09T13:58:37Z DEBUG crabcan::mounts] Pivoting root
[2022-03-09T13:58:37Z DEBUG crabcan::mounts] Unmounting old root
[2022-03-09T13:58:37Z DEBUG crabcan::namespaces] Setting up user namespace with UID 0
[2022-03-09T13:58:37Z DEBUG crabcan::namespaces] Child UID/GID map done, sending signal to child to continue...
[2022-03-09T13:58:37Z DEBUG crabcan::container] Creation finished
[2022-03-09T13:58:37Z DEBUG crabcan::container] Container child PID: Some(Pid(162889))
[2022-03-09T13:58:37Z DEBUG crabcan::container] Waiting for child (pid 162889) to finish
[2022-03-09T13:58:37Z INFO crabcan::namespaces] User namespaces set up
[2022-03-09T13:58:37Z DEBUG crabcan::namespaces] Switching to uid 0 / gid 0...
[2022-03-09T13:58:37Z DEBUG crabcan::capabilities] Clearing unwanted capabilities ...
[2022-03-09T13:58:37Z DEBUG crabcan::syscalls] Refusing / Filtering unwanted syscalls
[2022-03-09T13:58:37Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
...
[2022-03-09T13:58:37Z DEBUG syscallz] seccomp: loading policy
[2022-03-09T13:58:37Z INFO crabcan::child] Container set up successfully
[2022-03-09T13:58:37Z INFO crabcan::child] Starting container with command /bin/bash and args ["/bin/bash"]
[2022-03-09T13:58:37Z DEBUG crabcan::container] Finished, cleaning & exit
[2022-03-09T13:58:37Z DEBUG crabcan::container] Cleaning container
[2022-03-09T13:58:37Z DEBUG crabcan::resources] Cleaning cgroups
[2022-03-09T13:58:37Z DEBUG crabcan::errors] Exit without any error, returning 0

Patch for this step

这一步的代码可以在github litchipi/crabcan branch “step14”中找到.
前一步到这一步的原始补丁可以在此处找到