利用 FreeBSD Capsicum 框架实现程序沙箱化

当今时代,数据泄露的平均成本高达四百余万美元,令人惊讶的是,只有 51% 的公司计划增加安全投资^1^。这一数字很可能归因于实施健全安全解决方案所需的高昂成本。

许多安全框架都非常复杂。Capsicum 的独特之处在于其简洁性。开发者能相对容易地将 Capsicum 集成到程序中,从而在保护应用的同时降低高昂的实施成本。

本文展示了 Capsicum 的功能,并撰写了将该框架集成到新旧程序中的完整指南。

Capsicum

Capsicum 是款轻量级的安全框架,提供了用于限制程序能力的原语。更具体地说,Capsicum 能让开发者将其程序隔离到安全沙箱中。该框架基于最小特权原则设计,程序仅能访问其运行所需的资源。

能力沙箱

在支持 Capsicum 的系统上,程序能使用 cap_enter(2) 进入沙箱:

#include <sys/capsicum.h>
#include <stdio.h>

int
main(void)
{
    /* 进入 Capsicum 的能力模式。 */
    cap_enter();
    printf("能力模式下的 Hello world\n");
    return (0);
}

注意

为简洁起见,本文档中的代码片段省略了错误处理。实际使用时,应在适当位置加入错误处理。

不用链接额外的库;对 Capsicum 的支持已内置于标准 C 库(libc)。

Capsicum 能让开发者通过能力模式将程序沙箱化。在程序调用 cap_enter(2) 进入能力模式后,程序将无法自行获取新的资源。例如,使用 open(2) 打开文件会触发能力违规,造成函数调用失败,并将 errno 设置为 ECAPMODE译者注:特定错误码):这在能力模式下不被允许。

一种对程序进行 Capsicum 化的方法是在进入能力模式之前就打开好全部资源。程序事先拥有的资源能在沙箱中继续使用。

若程序需要访问某子目录域中数量未知的资源,则可以使用 openat(2)mkdirat(2)bindat(2)*at() 系统调用。这些函数需要额外的描述符参数,用作相对引用点来打开新资源。

int dirfd;

dirfd = open("/home/jfree", O_RDONLY | O_DIRECTORY);
cap_enter();

/* 打开 "/home/jfree/foo"。 */
if (openat(dirfd, "foo", O_RDONLY) < 0)
    printf("这不会发生\n");

在这个例子中,dirfd 是在进入能力模式之前获取的。若程序进入能力模式,dirfd 就能作为相对参考点访问 home/jfree/foo。无法访问相对引用所提供子目录域之外的资源。

int dirfd;

dirfd = open("/home/jfree", O_RDONLY | O_DIRECTORY);
cap_enter();

/* 打开 "/home/beastie"。 */
if (openat(dirfd, "../beastie", O_RDONLY) < 0)
    printf("这将会发生\n");

openat(2) 调用会失败,因为 ../beastie 路径指向的目录不在 dirfd 提供的目录层级之内。

进程间通信同样受到限制。进入能力模式的进程无法向其他进程发送信号,命名共享内存对象亦被禁止。某些系统接口完全不可用,如 reboot(2)kldload(2)。不过,这些限制都有相应的解决方法。

沙箱化的分区

预先打开资源对那些资源需求可预测的程序非常有效,但有些程序需要按需访问资源。在这种情况下,开发者可选择仅对程序的特定部分进行沙箱化。

如果无法在沙箱中运行完整的程序,那么可以将其隔离分区(compartmentalization)。隔离分区的做法是把一款程序拆分成多个分区,每个分区承担自身的基本任务。通过隔离分区架构,开发者可以将受信任的代码保留在沙箱之外,而将不安全或危险的代码隔离到沙箱中的分区。如果危险代码中发现了安全漏洞,将被隔离。

Capsicum 为程序的特定部分提供了直观的沙箱接口。程序能在任意时刻派生一个新的子进程,并在能力模式下执行危险代码。诸如管道和套接字这样的进程间通信原语可用于数据交换,而不会触发能力违规。

pid_t pid;
int pipefd[2], result;

pipe(pipefd);
/*
 * 创建一个子进程并将其隔离在能力沙箱中,
 * 在其中执行危险代码。
 */
pid = fork();
if (pid == 0) {
    close(pipefd[0]);
    cap_enter();
    result = dangerous_function();
    write(pipefd[1], &result, sizeof(result));
    exit(0);
}
close(pipefd[1]);
/* 从沙箱子进程获取结果。 */
result = read(pipefd[0], &result, sizeof(result));
printf("Result: %d\n", result);
/* 在父进程中继续正常执行。 */

父进程可在沙箱之外运行,而子进程在隔离环境中执行危险代码。若程序已经实现了隔离分区,开发者可以从沙箱化每个分区开始。大多数分区可能需要针对能力模式进行一些重构,但开发者可以自行选择哪些部分需要沙箱化。若正确实施,相较于对整个程序进行沙箱化,这种方式工作量更少,但安全性有大幅提升。

使用 libcasper(3) 请求资源

有些程序在设计时并未考虑隔离分区。开发者可以重新架构这些软件,但这通常需要大量时间和资源。幸运的是,库 libcasper(3) 为那些隔离分区无效的复杂程序提供了帮助。开发者可以利用 libcasper(3) 提供的接口在能力沙箱中获取新资源。

使用 Casper 服务

在程序进入能力模式之前,可以与某 casper 服务建立通信通道。Casper 服务是运行在能力沙箱之外的进程,与调用进程并行运行。建立的通信通道可用于向 casper 服务请求新资源。

注意

必须在进入能力模式之前就打开 libcasper(3) 通道,否则 casper 进程会继承父进程的沙箱。

cap_channel_t *cap_casper, *cap_net;
struct addrinfo *res;
int s;

/* 获取访问 libcasper(3) 服务的能力。 */
cap_casper = cap_init();

/*
 * 使用 cap_casper 能力与 "system.net" casper 服务
 * 建立通信通道。
 */
cap_net = cap_service_open(cap_casper, "system.net");

/*
 * 不再需要打开更多 casper 服务。
 * 关闭 cap_casper 能力。
 */
cap_close(cap_casper);

/*
 * 使用 cap_net(3) 库提供的 getaddrinfo() 变体。
 */
cap_getaddrinfo(cap_net, "freebsd.org", "80", NULL, &res);

s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

/*
 * 使用 cap_net(3) 库提供的 connect() 变体。
 */
cap_connect(cap_net, s, res->ai_addr, res->ai_addrlen);

cap_net(3) 利用 libcasper(3) 提供了支持能力模式的 libc 网络函数,否则这些函数会因 ECAPMODE 而失败。通过 libcasper(3) 接口的函数通常以 cap_ 前缀命名,以表明它们能在能力模式下成功执行。类似于 cap_net(3) 的其他 casper 服务库也存在,并提供以 cap_ 为前缀的 libc 函数。可在 libcasper(3) 手册页中找到完整列表。

如果程序支持在没有 libcasper(3) 的情况下构建,开发者依然可以使用 cap_ 函数,而无需用 #ifdef WITH_CASPER 包装。大多数 casper 服务将其 cap_ 函数定义为宏,当 WITH_CASPER 未定义时会替换为非 cap_ 的形式。

创建 Casper 服务

Casper 服务库很好地隐藏了接口 libcasper(3),使程序开发者无需直接与其交互。但有时程序需要访问现有 casper 服务库未提供的资源,这种情况下开发者可以自行创建 libcasper(3) 服务。

所有 libcasper(3) 服务都基于 CREATE_SERVICE(3) 宏:

CREATE_SERVICE(name, limit_func, command_func, flags);

所有参数都很重要,其中 command_func 函数指针尤为值得关注,因为它是服务的主例程。当通过 cap_service_open(3) 打开服务时,服务会等待命令。待接收到命令,就会传递给 command_funccmd 参数。

/*
 * cap_net(3) casper 服务库使用的命令函数。
 */
static int
net_command(const char *cmd, const nvlist_t *limits, nvlist_t *nvlin,
    nvlist_t *nvlout)
{
    if (strcmp(cmd, "bind") == 0)
        return (net_bind(limits, nvlin, nvlout));
    else if (strcmp(cmd, "connect") == 0)
        return (net_connect(limits, nvlin, nvlout));
    else if (strcmp(cmd, "gethostbyname") == 0)
        return (net_gethostbyname(limits, nvlin, nvlout));
    else if (strcmp(cmd, "gethostbyaddr") == 0)
        return (net_gethostbyaddr(limits, nvlin, nvlout));
    else if (strcmp(cmd, "getnameinfo") == 0)
        return (net_getnameinfo(limits, nvlin, nvlout));
    else if (strcmp(cmd, "getaddrinfo") == 0)
        return (net_getaddrinfo(limits, nvlin, nvlout));

    return (EINVAL);
}

CREATE_SERVICE("system.net", net_limit, net_command, 0);

大多数 command_func 会将传入的 cmd 字符串与一组字面量比较,若匹配则调用对应函数。Casper 服务运行在能力沙箱之外,因此在 command_func 内调用的所有函数都会以环境权限(ambient authority)执行,这意味着它们可以随意获取新资源。

可以使用函数在程序和 casper 服务之间 cap_xfer_nvlist(3) 交换资源。命令字符串和相关资源必须先封装进 nvlist(9),再传输给服务。Nvlist 可存储数字、字符串、二进制、描述符以及其他 nvlists。每个资源都必须配有一个标识名,用于检索。

/*
 * 向与 @chan 绑定的 casper 服务发送 "bind" 命令。
 */
static int
cap_bind(cap_channel_t *chan, int sockfd, const struct sockaddr *addr,
    socklen_t addrlen)
{
    nvlist_t *nvl = nvlist_create(0);
    int error;

    nvlist_add_string(nvl, "cmd", "bind");
    nvlist_add_descriptor(nvl, "sockfd", sockfd);
    nvlist_add_binary(nvl, "addr", addr, addrlen);

    nvl = cap_xfer_nvlist(chan, nvl);
    if (nvl == NULL)
        return (-1);

    error = nvlist_get_number(nvl, "error");
    if (error != 0) {
        nvlist_destroy(nvl);
        errno = error;
        return (-1);
    }

    error = dup2(sockfd, nvlist_get_descriptor(nvl, "sockfd"));
    nvlist_destroy(nvl);

    return (error == -1 ? -1 : 0);
}

cap_xfer_nvlist(3) 函数会将 nvl nvlist 发送到与 chan 绑定的 casper 服务。待 nvlist 到达 casper 服务,命令字符串会被提取并传入服务的 command_func

回顾 net_command() 中的片段:

if (strcmp(cmd, "bind") == 0)
        return (net_bind(limits, nvlin, nvlout));

cap_bind() 发送了 "bind" 命令字符串,因此该 strcmp() 条件成立并调用 net_bind()

/*
 * net_bind() 函数的简化版。
 * 负责从 @nvlin 提取参数,调用 bind(2),
 * 并将返回值加入 @nvlout。
 */
static int
net_bind(const nvlist_t *limits __unused, nvlist_t *nvlin, nvlist_t *nvlout)
{
    int sockfd;
    const void *addr;
    size_t len;

    addr = nvlist_get_binary(nvlin, "addr", &len);
    sockfd = nvlist_take_descriptor(nvlin, "sockfd");
    if (bind(sockfd, saddr, len) < 0) {
        int serrno = errno;
        close(sockfd);
        return (serrno);
    }
    nvlist_move_descriptor(nvlout, "sockfd", sockfd);

    return (0);
}

bind(2) 成功时,套接字必须被传回能力沙箱。nvlist_move_descriptor(9) 函数会将套接字描述符移入 nvlout nvlist,并接管该描述符的所有权。

回顾 cap_bind() 中的片段:

nvl = cap_xfer_nvlist(chan, nvl);
    if (nvl == NULL)
        err(1, "Failed transfer bind() nvlist");

cap_xfer_nvlist(9) 的返回值是 nvlout nvlist,它持有已绑定的套接字描述符。可以通过 nvlist_get_descriptor(nvl, "sockfd") 从返回的 nvlist 中获取该描述符。

限制 Casper 服务

Casper 服务提供的接口通常过于宽泛。在使用 cap_net(3) 时,你可能仅需要服务提供的部分函数。大多数服务库会定义自己的限制机制,使开发者可以禁用程序不需要的函数。

/*
 * 使用 cap_net(3) 的限制机制,仅允许解析 freebsd.org
 * 在 80 端口上的地址。
 *
 * 假设 cap_net(3) 服务已经被打开并在 @cap_net 上监听。
 */
cap_net_limit_t *limit;
int familylimit;

/* 仅允许名称解析 (cap_getaddrinfo(3))。 */
limit = cap_net_limit_init(cap_net, CAPNET_NAME2ADDR);

/* 将名称解析限制为 "freebsd.org" 的 80 端口。 */
cap_net_limit_name2addr(limit, "freebsd.org", "80");

/* 将名称解析限制为 IPv4 地址。 */
familylimit = AF_INET;
cap_net_limit_name2addr_family(limit, &familylimit, 1);

/* 将限制应用到 cap_net。 */
cap_net_limit(limit);

每个服务库都提供不同的限制机制。由于使用模式过多,建议开发者查阅各服务库的手册以获取详细信息和示例。

为 Casper 服务创建限制

开发者可以在 CREATE_SERVICE(3) 中指定 limit_func 函数指针以限制服务接口。当程序调用 cap_limit_set(3) 时,提供的 limits 会被重定向到 casper 服务的 limit_func 中,并相应应用。该机制的灵活性允许服务对其接口进行精细限制。

大多数服务库会为 cap_limit_set(3) 提供包装函数,并使用自定义的 _limit_t 类型。该类型由服务库定义,用于跟踪服务特定的限制。

随着限制的细化,实现可能很快变得复杂。像 cap_net(3) 这样的服务提供了对接口的广泛控制,因此其限制函数需要处理所有边界情况。快速查看 cap_net(3) 的限制函数可以发现,为不同的“限制模式”实现了单独的验证函数,以确保限制被正确应用和执行。

/*
 * cap_net(3) 中 net_limit() 函数的片段。
 * 为清晰起见,部分代码被省略。
 * 上下文见 "lib/libcasper/services/cap_net/cap_net.c"。
 */
while ((name = nvlist_next(newlimits, NULL, &cookie)) != NULL) {
        /* ... */
        if (strcmp(name, LIMIT_NV_BIND) == 0) {
                hasbind = true;
                if (!verify_bind_newlimts(oldlimits,
                    cnvlist_get_nvlist(cookie))) {
                        return (ENOTCAPABLE);
                }
        } else if (strcmp(name, LIMIT_NV_CONNECT) == 0) {
                hasconnect = true;
                if (!verify_connect_newlimits(oldlimits,
                    cnvlist_get_nvlist(cookie))) {
                        return (ENOTCAPABLE);
                }
        } else if (strcmp(name, LIMIT_NV_ADDR2NAME) == 0) {
                hasaddr2name = true;
                if (!verify_addr2name_newlimits(oldlimits,
                    cnvlist_get_nvlist(cookie))) {
                        return (ENOTCAPABLE);
                }
        } else if (strcmp(name, LIMIT_NV_NAME2ADDR) == 0) {
                hasname2addr = true;
                if (!verify_name2addr_newlimits(oldlimits,
                    cnvlist_get_nvlist(cookie))) {
                        return (ENOTCAPABLE);
                }
        }
}

限制函数依赖于其所限制的服务,因此没有统一的编写模式。想要为 casper 服务创建限制的开发者应参考 FreeBSD 源码中 lib/libcasper/services 的系统服务库示例。

回顾:Casper 服务的组成部分

casper 服务由以下四个主要部分组成:

  • cap_ 为前缀的函数,通过 cap_xfer_nvlist(3) 向 casper 服务发送命令;

  • 在沙箱外执行命令相关代码并返回新资源的 command_func

  • 限制服务用途的 limit_func

  • 将服务拼接在一起的 CREATE_SERVICE(3) 宏。

虽然在技术上可以创建返回任意命名资源的 casper 服务,但这会破坏将程序隔离到沙箱的目的。设计良好的 casper 服务接口应当有限且受约束,避免被利用。

检测违规

当程序进入能力模式时,它是否遵循沙箱规则并不总是显而易见。尝试打开受限资源的函数会触发能力违规,并返回 errno = ECAPMODE。即便有适当的错误检查,排查违规也可能耗时。好在 ktrace(2) 内核跟踪工具能帮助发现违规。

通常,程序必须进入能力模式才会报告违规,但 ktrace(2) 能在程序 未进入 能力模式前记录违规。这意味着开发者能在不修改程序的情况下运行违规跟踪,查看违规发生的位置。由于程序未真正进入能力模式,它依然会获取资源并正常执行。

在程序开头添加以下两行即可启用违规跟踪:

open("ktrace.out", O_RDONLY | O_CREAT | O_TRUNC);
ktrace("ktrace.out", KTROP_SET, KTRFAC_CAPFAIL, getpid());

这段代码创建了 ktrace(2) 的输出文件,并指定 KTRFAC_CAPFAIL 跟踪点以记录能力失败。

注意

ktrace(2) 手册页对系统调用的使用有详细说明。启用其他跟踪点(如 KTRFAC_NAMEI 记录文件名查找)有助于定位文件系统违规的来源。

下面的 cap_violate 例程试图触发 ktrace(2) 可捕获的所有违规。无需理解其具体做了什么,只需知道它能触发违规就可以了。

open("ktrace.out", O_RDONLY | O_CREAT | O_TRUNC);
ktrace("ktrace.out", KTROP_SET, KTRFAC_CAPFAIL, getpid());

cap_rights_init(&rights, CAP_READ);
caph_rights_limit(STDERR_FILENO, &rights);
write(STDERR_FILENO, &val, sizeof(val));

cap_rights_set(&rights, CAP_WRITE);
caph_rights_limit(STDERR_FILENO, &rights);

kinf.kf_structsize = sizeof(struct kinfo_file);
fcntl(STDIN_FILENO, F_KINFO, &kinf);

socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

addr.sin_family = AF_INET;
addr.sin_port = htons(5000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP),
    (const struct sockaddr *)&addr, sizeof(addr));
sendto(fd, NULL, 0, 0, (const struct sockaddr *)&addr, sizeof(addr));

kill(getppid(), SIGCONT);

openat(AT_FDCWD, "/", O_RDONLY);

CPU_SET(0, &cpuset_mask);
cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, getppid(),
    sizeof(cpuset_mask), &cpuset_mask);

若进程被跟踪,跟踪数据会一直记录,直到进程退出或清除跟踪点。此时可以使用 kdump(2)ktrace(2) 的转储转换为可读格式。

# ./cap_violate
# kdump
  1915 cap_violate CAP   operation requires CAP_WRITE, descriptor holds CAP_READ
  1915 cap_violate CAP   attempt to increase capabilities from CAP_READ to CAP_READ,CAP_WRITE
  1915 cap_violate CAP   system call not allowed: fcntl, cmd: F_KINFO
  1915 cap_violate CAP   socket: protocol not allowed: IPPROTO_ICMP
  1915 cap_violate CAP   system call not allowed: bind
  1915 cap_violate CAP   sendto: restricted address lookup: struct sockaddr { AF_INET, 0.0.0.0:5000 }
  1915 cap_violate CAP   kill: signal delivery not allowed: SIGCONT
  1915 cap_violate CAP   openat: restricted VFS lookup: AT_FDCWD
  1915 cap_violate CAP   cpuset_setaffinity: restricted cpuset operation

cap_violate 程序中的每次违规都会在 kdump(1) 输出中生成一条 CAP 记录。开发者可利用这些输出定位并替换触发违规的代码。

大多数实际程序应尽量避免违规,而不是像 cap_violate 那样触发它们。下面的 kdump(1) 输出展示了在启用 KTRFAC_CAPFAILKTRFAC_NAMEI 跟踪点后,使用 unzip(1) 解压归档文件(未进行 Capsicum 化)时的结果。

# unzip foo.zip
# kdump
  1926 unzip    NAMI  "foo.zip"
  1926 unzip    CAP   openat: restricted VFS lookup: AT_FDCWD
  1926 unzip    CAP   system call not allowed: open
  1926 unzip    NAMI  "/etc/localtime"
  1926 unzip    NAMI  "bar"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    CAP   system call not allowed: mkdir
  1926 unzip    NAMI  "bar"
  1926 unzip    NAMI  "bar"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    NAMI  "bar/bar.txt"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    NAMI  "bar/bar.txt"
  1926 unzip    CAP   openat: restricted VFS lookup: AT_FDCWD
  1926 unzip    NAMI  "baz"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    CAP   system call not allowed: mkdir
  1926 unzip    NAMI  "baz"
  1926 unzip    NAMI  "baz"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    NAMI  "baz/baz.txt"
  1926 unzip    CAP   fstatat: restricted VFS lookup: AT_FDCWD
  1926 unzip    NAMI  "baz/baz.txt"
  1926 unzip    CAP   openat: restricted VFS lookup: AT_FDCWD

这类输出更接近开发者在自己程序中会看到的情况。unzip(2) 正在重建 zip 文件中的目录结构。所有 open(2)fstat(2)mkdir(2) 调用都被 libc 隐式转换为带 AT_FDCWD*at() 变体。由于 AT_FDCWD 在能力模式下不可用,因此触发违规。这类问题可通过在进入能力模式前打开一个当前目录描述符(等价于 AT_FDCWD),并将其传递给 openat(2)fstatat(2)mkdirat(2) 来规避。

注意

在能力模式下违规总会返回错误,但在跟踪过程中不会被当作错误,因此程序行为可能不同。

违规跟踪是开发者工具箱中的一项实用工具。只需几秒钟即可用 ktrace(2) 运行程序,其结果几乎总能作为使用 Capsicum 沙箱化程序的良好开始。

能力 (Capabilities)

尽管名称相似,能力模式 (capability mode) 和能力 (capabilities) 是不同的内核原语。

  • 能力模式是 Capsicum 实现的安全沙箱。

  • 能力是拥有权限的文件描述符。

如果用户想要对某项能力描述符执行 read(2),则该描述符必须拥有 CAP_READ 权限。如果用户想要对某个套接字能力描述符执行 bind(2),则该描述符必须拥有 CAP_BIND 权限。几乎所有描述符操作都有细粒度的权限;完整列表见手册页 rights(4)

当使用 open(2)socket(2) 等创建文件描述符时,它会被赋予完整的能力集。可以使用 cap_rights_limit(2) 函数限制描述符的能力。

/*
 * 将文件描述符限制为只读。
 */
cap_rights_t rights;
int fd;
char buf[1] = 'x';

fd = open("/home/jfree/foo", O_RDWR);

cap_rights_init(&rights, CAP_READ);
cap_rights_limit(fd, &rights);

if (read(fd, buf, sizeof(buf)) < 0)
    printf("这不会发生,因为我们有 CAP_READ\n");

if (write(fd, buf, sizeof(buf)) < 0)
    printf("这将会发生,因为我们缺少 CAP_WRITE\n");

能力的设计原则是最小化权限。只要描述符的权限被限制,它就不应执行超出权限的操作。描述符的权限始终可以被限制,但不能被提升。

/*
 * 试图扩展描述符的权限。
 */
cap_rights_t rights;
int fd;

fd = open("/home/jfree/foo", O_RDWR);

cap_rights_init(&rights, CAP_READ);
cap_rights_limit(fd, &rights);

cap_rights_set(&rights, CAP_WRITE);
if (cap_rights_limit(fd, &rights) < 0)
    printf("应用权限失败;权限永远不能被提升\n");

在对程序进行沙箱化过于严格的情况下,开发者可以选择限制能力描述符。能力提供了对允许操作的精细控制,但使用这种保护的程序易受到人为疏忽的影响。如果开发者忘记限制某项能力,就可能引入恶意代码滥用的风险。

想要最大化安全性的开发者可以在能力模式下结合使用能力。能力提供了能力模式本身不具备的细粒度功能。例如,仅允许描述符进行 read(2) 操作,这是能力可以做到的,但能力模式不能。

使用 Capsicum 提升安全性

能力模式能力 的设计初衷都是为了让程序更安全。能力模式通过将程序与系统其余部分隔离提供了确定的安全性。能力则提供了一种更灵活但不如前者严格的方式来限制程序权限。只要正确集成其中任意一种原语,开发者就能确信,他们的程序比在引入 Capsicum 前更加安全。

参考资料

  1. "Cost of a Data Breach Report 2023". IBM Corporation. 2023-07-24. Retrieved 2023-08-31.

相关资料

最后更新于