Bubblewrap:轻量级非特权容器运行时
项目标题与描述
Bubblewrap 是一个专注于为非特权用户提供沙箱和容器运行时的工具。与 systemd-nspawn、Docker 等面向系统管理员和编排工具的传统容器运行时不同,Bubblewrap 的设计目标是安全地提供给普通用户使用,避免将此类访问权限转变为对主机的完全特权 root shell。
项目核心价值在于实现用户命名空间功能的子集,特别是当 Linux 内核的用户命名空间功能在某些生产发行版中尚未对非特权用户可用或存在安全顾虑时,Bubblewrap 可以作为其 setuid 实现方案。
功能特性
- 非特权容器运行:允许非特权用户运行容器化应用,无需授予完整的 root 访问权限。
- 最小权限原则:使用
PR_SET_NO_NEW_PRIVS关闭 setuid 二进制文件的权限提升,限制容器内进程的权限。 - 命名空间隔离:支持创建新的命名空间(如 PID、网络、挂载、IPC 等)进行环境隔离。
- 绑定挂载控制:提供精细的绑定挂载功能,支持只读 (
BIND_READONLY)、递归 (BIND_RECURSIVE) 等选项,灵活构建容器文件系统视图。 - 叠加文件系统支持:通过
--overlay、--tmp-overlay、--ro-overlay和--overlay-src选项创建 overlay 挂载,实现高效的层叠文件系统。 - 用户命名空间映射:可与
newuidmap、newgidmap工具配合,实现容器内外用户/组 ID 的映射。 - 网络命名空间管理:包含设置环回接口 (
loopback_setup) 和更复杂的网络接口配置能力(通过rtnetlink)。 - Seccomp 沙箱:支持通过 BPF 过滤器(如
flatpak.bpf)进一步限制系统调用,增强安全性。 - SELinux 集成:可选支持 SELinux 上下文设置(
setexeccon,setfscreatecon),实现强制访问控制。 - 详细的错误处理:定义清晰的错误码枚举(如
BIND_MOUNT_ERROR_MOUNT),并提供die_with_bind_result等函数进行错误报告。
安装指南
项目使用 Meson 构建系统(要求 Meson ≥ 0.49.0),已移除了 Autotools 构建系统。
构建依赖:
- gcc 或 clang
- libcap-dev
- libselinux1-dev (可选,用于 SELinux 支持)
- meson (≥ 0.49.0)
- pkg-config
在 Debian/Ubuntu 系统上安装依赖:
apt-get update
apt-get install build-essential libcap-dev libselinux1-dev meson pkg-config
在 RHEL/CentOS/Fedora 系统上安装依赖:
yum install gcc libcap-devel 'pkgconfig(libselinux)' meson
从源码构建与安装:
- 克隆代码仓库:
git clone https://github.com/containers/bubblewrap.git - 进入目录:
cd bubblewrap - 配置构建:
meson setup build - 编译:
meson compile -C build - 安装(可能需要root):
sudo meson install -C build
注意:Bubblewrap 可以以 setuid root 模式安装,此时它旨在遵循与允许非特权用户创建新用户命名空间的内核相同的安全边界。若不设置为 setuid,则它本身不构成用户与操作系统之间的安全边界。
使用说明
Bubblewrap 通过命令行参数定义沙箱环境。其安全模型完全由调用者构建的命令行参数决定。
基础示例:复用主机 /usr,隔离其他目录
此示例创建一个共享主机 /usr(只读)但拥有独立 /tmp、/home、/var、/run、/etc 的 shell 环境,并启用网络共享。
#!/bin/bash
(exec bwrap --ro-bind /usr /usr \
--dir /tmp \
--dir /var \
--symlink ../tmp var/tmp \
--proc /proc \
--dev /dev \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--chdir / \
--unshare-all \
--share-net \
--die-with-parent \
--dir /run/user/$(id -u) \
--setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
--setenv PS1 "bwrap-demo$ " \
--file 11 /etc/passwd \
--file 12 /etc/group \
/bin/sh) \
11< <(getent passwd $UID 65534) \
12< <(getent group $(id -g) 65534)
运行 Flatpak 应用(如 GNOME Weather) 此示例展示了如何利用 Bubblewrap 手动构造一个类似 Flatpak 的运行时环境来启动一个应用。
#!/bin/bash
(
exec bwrap \
--ro-bind ~/.local/share/flatpak/runtime/org.gnome.Platform/x86_64/master/active/files /usr \
--ro-bind ~/.local/share/flatpak/app/org.gnome.Weather/x86_64/master/active/files/ /app \
--dev /dev \
--proc /proc \
--dir /tmp \
--symlink /tmp /var/tmp \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--symlink usr/etc /etc \
--dir /run/user/`id -u` \
--ro-bind /etc/resolv.conf /run/host/monitor/resolv.conf \
--bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X99 \
--unshare-pid \
--setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
--setenv DISPLAY :99 \
--setenv PATH /app/bin:/usr/bin \
--seccomp 13 \
/bin/sh
) 13< `dirname $0`/flatpak.bpf
使用用户命名空间和 ID 映射
此 Python 脚本示例演示了如何结合 newuidmap/newgidmap 创建用户命名空间并进行 UID/GID 映射。
#!/usr/bin/env python3
import os, select, subprocess, sys, json
pipe_info = os.pipe()
userns_block = os.pipe()
pid = os.fork()
if pid != 0:
# 父进程:设置映射
os.close(pipe_info[1]); os.close(userns_block[0])
select.select([pipe_info[0]], [], [])
data = json.load(os.fdopen(pipe_info[0]))
child_pid = str(data['child-pid'])
subprocess.call(["newuidmap", child_pid, "0", str(os.getuid()), "1"])
subprocess.call(["newgidmap", child_pid, "0", str(os.getgid()), "1"])
os.write(userns_block[1], b'1') # 解除子进程阻塞
else:
# 子进程:启动 bwrap
os.close(pipe_info[0]); os.close(userns_block[1])
os.set_inheritable(pipe_info[1], True)
os.set_inheritable(userns_block[0], True)
args = ["bwrap",
"--unshare-all",
"--unshare-user",
"--userns-block-fd", "%i" % userns_block[0],
"--info-fd", "%i" % pipe_info[1],
"--bind", "/", "/",
"cat", "/proc/self/uid_map"]
os.execlp(*args)
常用参数概览:
--bind <src> <dest>: 绑定挂载目录或文件。--ro-bind <src> <dest>: 只读绑定挂载。--dev /dev: 挂载/dev设备目录。--proc /proc: 挂载/proc文件系统。--dir <dir>: 创建一个空目录。--unshare-<namespace>: 不共享指定的命名空间(如 pid, net, ipc, uts)。--share-net: 共享网络命名空间(与主机网络相同)。--die-with-parent: 当父进程退出时终止沙箱。--setenv <VAR> <value>: 设置环境变量。--seccomp <fd>: 从文件描述符读取并加载 seccomp BPF 过滤器。--overlay <upper> <work> <lower> <dest>: 创建 overlay 挂载。
核心代码
以下是 Bubblewrap 项目中几个核心模块的代码片段,展示了其关键实现。
1. 绑定挂载功能头文件 (bind-mount.h):
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include "utils.h"
/* 绑定挂载选项标志位 */
typedef enum {
BIND_READONLY = (1 << 0), /* 只读挂载 */
BIND_DEVICES = (1 << 2), /* 允许创建设备节点 */
BIND_RECURSIVE = (1 << 3), /* 递归挂载 */
} bind_option_t;
/* 绑定挂载操作结果枚举 */
typedef enum
{
BIND_MOUNT_SUCCESS = 0,
BIND_MOUNT_ERROR_MOUNT, /* mount() 系统调用失败 */
BIND_MOUNT_ERROR_REALPATH_DEST, /* 解析目标路径失败 */
BIND_MOUNT_ERROR_REOPEN_DEST, /* 重新打开目标路径失败 */
BIND_MOUNT_ERROR_READLINK_DEST_PROC_FD, /* 读取 /proc/self/fd/ 链接失败 */
BIND_MOUNT_ERROR_FIND_DEST_MOUNT, /* 查找目标挂载点失败 */
BIND_MOUNT_ERROR_REMOUNT_DEST, /* 重新挂载目标失败 */
BIND_MOUNT_ERROR_REMOUNT_SUBMOUNT, /* 重新挂载子挂载点失败 */
} bind_mount_result;
/* 执行绑定挂载的核心函数
* @proc_fd: 指向 /proc 目录的文件描述符
* @src: 源路径
* @dest: 目标路径
* @options: 绑定选项组合
* @failing_path: 输出参数,失败时相关的路径
* @return: 绑定挂载结果
*/
bind_mount_result bind_mount (int proc_fd,
const char *src,
const char *dest,
bind_option_t options,
char **failing_path);
/* 根据绑定挂载结果输出错误信息并终止进程 */
void die_with_bind_result (bind_mount_result res,
int saved_errno,
const char *failing_path,
const char *format,
...)
__attribute__((__noreturn__))
__attribute__((format (printf, 4, 5)));
2. 工具函数与宏定义头文件 (utils.h):
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
/* 计算静态数组元素个数的宏 */
#define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0]))
/* 处理因信号中断而失败的系统调用的重试宏 */
#ifndef TEMP_FAILURE_RETRY
#define TEMP_FAILURE_RETRY(expression) \
(__extension__ \
({ long int __result; \
do __result = (long int) (expression); \
while (__result == -1L && errno == EINTR); \
__result; }))
#endif
/* 管道端点定义 */
#define PIPE_READ_END 0
#define PIPE_WRITE_END 1
/* 全局日志级别前缀标志 */
extern bool bwrap_level_prefix;
/* 日志记录函数,支持 syslog 级别的 severity 和 printf 格式 */
void bwrap_log (int severity,
const char *format,
...) __attribute__((format (printf, 2, 3)));
/* 便捷的警告日志宏 */
#define warn(...) bwrap_log (LOG_WARNING, __VA_ARGS__)
/* 因错误而终止进程的函数,会打印错误信息和 errno 对应的描述 */
void die_with_error (const char *format, ...)
__attribute__((__noreturn__))
__attribute__((format (printf, 1, 2)));
3. 网络命名空间设置(环回接口)头文件 (network.h):
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
/* 设置网络命名空间中的环回接口。
* 通常在创建新的网络命名空间后调用,以确保 lo 接口处于 UP 状态。
*/
void loopback_setup (void);
4. 绑定挂载实现代码片段 (bind-mount.c 部分):
/* bubblewrap
* Copyright (C) 2016 Alexander Larsson
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "config.h"
#include <sys/mount.h>
#include "utils.h"
#include "bind-mount.h"
/* 解析 /proc/self/mountinfo 文件时的辅助函数:跳过当前 token */
static char *
skip_token (char *line, bool eat_whitespace)
{
while (*line != ' ' && *line != '\n')
line++;
if (eat_whitespace && *line == ' ')
line++;
return line;
}
/* 将 mountinfo 中八进制转义的字符串(如 `\134` 表示反斜杠)进行原地解码 */
static char *
unescape_inline (char *escaped)
{
char *unescaped, *res;
const char *end;
res = escaped;
end = escaped + strlen (escaped);
unescaped = escaped;
while (escaped < end)
{
if (*escaped == '\\') /* 遇到转义序列 */
{
/* 将八进制序列 \ooo 解码为一个字符 */
*unescaped++ =
((escaped[1] - '0') << 6) |
((escaped[2] - '0') << 3) |
((escaped[3] - '0') << 0);
escaped += 4; /* 跳过转义序列 */
}
else
{
*unescaped++ = *escaped++; /* 复制普通字符 */
}
}
*unescaped = 0; /* 添加字符串终止符 */
return res;
}
/* 解码挂载选项字符串(如 "ro,nodev")为相应的 mount() 系统调用标志位 */
static unsigned long
decode_mountoptions (const char *options)
{
const char *token, *end_token;
int i;
unsigned long flags = 0;
static const struct { int flag; const char *name; } flag_names[] = {
{ MS_RDONLY, "ro" },
{ MS_NOSUID, "nosuid" },
{ MS_NODEV, "nodev" },
{ MS_NOEXEC, "noexec" },
{ MS_SYNCHRONOUS, "sync" },
{ MS_REMOUNT, "remount" },
{ MS_MANDLOCK, "mand" },
{ MS_DIRSYNC, "dirsync" },
{ MS_NOATIME, "noatime" },
{ MS_NODIRATIME, "nodiratime" },
{ MS_BIND, "bind" },
{ MS_MOVE, "move" },
{ MS_REC, "rec" },
{ MS_SILENT, "silent" },
{ MS_POSIXACL, "acl" },
{ MS_UNBINDABLE, "unbindable" },
{ MS_PRIVATE, "private" },
{ MS_SLAVE, "slave" },
{ MS_SHARED, "shared" },
{ MS_RELATIME, "relatime" },
{ MS_KERNMOUNT, "kernmount" },
{ MS_I_VERSION, "iversion" },
{ MS_STRICTATIME, "strictatime" },
{ MS_LAZYTIME, "lazytime" },
};
/* ... 实际解析逻辑 ... */
}
这些代码展示了 Bubblewrap 如何通过精细的绑定挂载控制、健壮的错误处理、命名空间操作和系统调用封装,来构建一个安全、可控的非特权沙箱环境。其模块化设计使得它可以作为底层工具被更高层的容器框架(如 Flatpak)所集成和使用。FINISHED JwWylm2/DJz0H/UvRktemHCdGgLP0PsR1WU4BWi+LMY=