万物皆可沙箱化!BubbleWrap简单上手

TL;DR

最近存在一个问题,运行一些非自由软件时,它们老喜欢搞事情,比如在后台放一个云同步服务,你把软件关了那个云同步还不会一起关;亦或会闲得没事往你电脑里拉屎,不仅占空间,而且还多处一堆奇奇怪怪的文件夹。

这个时候,就要请出我们的老朋友,bwrap了!它可以仅授权某些文件夹的访问,也可以仅授权某些文件夹的只读访问,也可以更改应用看到的文件夹结构,直观体验就像安卓上的存储空间隔离 by Rikka一样。

这是项目官方的仓库

安装

安装起来十分简单啊,直接一行命令就行了:

1
sudo pacman -S bubblewrap

使用

直接保存这个脚本,然后在它后面接要运行的应用程序名就行,已经做了英特尔和nVidia双显卡自动检测兼容;也可以直接source这个脚本,然后直接在命令行里bwrap_run xxx就行,不过为了安全,这样会跳过bind_maps那块。
举个例子:

1
2
3
4
# 从本地脚本运行
./bwrap_run.sh bash
# source后可以直接运行,但会跳过 bind_maps
bwrap_run bash

这样会打开一个沙箱内的bash,然后在沙箱里再运行不放心的软件就行了,亦或运行wine!这样就不用担心wine闲得没事把你每个磁盘都挂上然后你运行的某些exe往里面拉屎了!

当你在沙箱的shell里运行exit后,整个沙箱内的所有子进程都会被一起结束!十分的纯净,不留痕!软件运行过程中拉的屎都存在内存里,当沙箱结束后会一并从内存中删除,全程0接触物理磁盘!

对于Coding Agent安全:

目标是,除了当前目录、部分允许访问的目录、/dev/nvidia*外,其他都是只读,不许读写。如果看都不给看,可能会引起一些小问题。适合用这个版本:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#!/usr/bin/env bash

bwrap_run() {
(
set -euo pipefail

# =================== 配置 ===================
local WORK_DIR="$(pwd)"
local USER_ID="$(id -u)"
local GROUP_ID="$(id -g)"

# 直通只读的文件夹白名单
local ro_dirs=(
# 系统配置
"$HOME/.local/share/fonts"
)

# 直通读写的文件夹白名单
local allowed_dirs=(
# 直通的目录
"/home/enoch/Documents/bwrap_transfer"
"/home/enoch/Documents/GlobalFileTransfer/WIP"
"$HOME/Documents/WIP"
# Proton
"$HOME/.local/share/Steam"
# UV
"$HOME/.cache/uv"
"$HOME/.local/share/uv"
"$HOME/.local/bin/uv" # 官方脚本的安装目录
# WPS
"$HOME/.config/Kingsoft"
"/opt/kingsoft"
# HMCL
"$HOME/.local/share/hmcl"
# Wine Prefix NCM
"$HOME/Documents/wine_exe"
)

# 跨磁盘允许读写用这个加白
local bind_maps_force=(
)
# 跨磁盘允许读写用这个加白, 但只允许从脚本运行时加白
local bind_maps=(
"/run/media/$USER/Data/哈哈哈/uncompress::gamesuncompress_bwrap"
"/run/media/$USER/Data_Samsung/Env/哈哈哈/uncompress::gamesuncompress_870_bwrap"
"/run/media/$USER/Software_Master/games::gamesuncompress_980_bwrap"
)
local bind_maps_created=()
if [[ ! -v bwrap_run_from_script ]]; then
bind_maps=()
echo "警告: 从source的函数运行, 跳过挂载bind_maps"
fi
# 文件夹映射
local bind_maps_absloute=(
"$HOME/Documents/bwrap_home::$HOME"
)

# 环境变量
local env_vars=(
"LANG=zh_CN.UTF-8"
"LC_ALL=zh_CN.UTF-8"
# NVIDIA(有则生效)
"__GL_THREADED_OPTIMIZATIONS=1"
)

# =================== 构建 bwrap 参数 ===================
local bargs=(
--unshare-pid # 创建独立 PID namespace
--die-with-parent # 父进程退出时 bwrap 自动杀掉
--setenv HOME "$HOME"
--setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR"
--chdir "$WORK_DIR"
--tmpfs /tmp # 给 bwrap 内程序干净 tmpfs
--bind /dev/shm /dev/shm # 对 MIT-SHM 或 X11 必须的共享内存
)

echo "===== Device Checking ====="
# =================== GPU 类型检测 ===================
detect_gpu() {
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
printf '%s' "nvidia"
return
fi
if ls /dev/nvidia* >/dev/null 2>&1; then
printf '%s' "nvidia"
return
fi
for card in /sys/class/drm/card*; do
local vendor_file="$card/device/vendor"
[ -r "$vendor_file" ] || continue
local vendor_hex="$(tr -d ' \n' <"$vendor_file")"
case "$vendor_hex" in
*8086*) printf 'intel'; return ;;
*10de*) printf 'nvidia'; return ;;
*1002*) printf 'intel'; return ;;
esac
done
printf 'unknown'
}
local GPU_TYPE="$(detect_gpu || true)"
echo "检测到 GPU 类型: $GPU_TYPE"
# =================== GPU 绑定 ===================
case "$GPU_TYPE" in
nvidia)
echo "绑定 NVIDIA 设备节点"
for ndev in /dev/nvidia*; do
[ -e "$ndev" ] && bargs+=( --dev-bind "$ndev" "$ndev" )
done
[ -d /dev/dri ] && bargs+=( --dev-bind /dev/dri /dev/dri )
;;
intel|unknown)
[ -d /dev/dri ] && bargs+=( --dev-bind /dev/dri /dev/dri )
;;
esac

# =================== XDG_RUNTIME_DIR / Wayland ===================
local XDG_RUNTIME_DIR="/run/user/$USER_ID"
local WAYLAND_SOCKET=""
if [ -d "$XDG_RUNTIME_DIR" ]; then
for s in "$XDG_RUNTIME_DIR"/wayland-*; do
[ -S "$s" ] || continue
local WAYLAND_SOCKET="$s"
break
done
fi
[ -n "$WAYLAND_SOCKET" ] && echo "Wayland socket: $WAYLAND_SOCKET"
[ -n "$WAYLAND_SOCKET" ] && bargs+=( --setenv WAYLAND_DISPLAY "$(basename "$WAYLAND_SOCKET")" )

# =================== 系统库绑定 ===================
for d in /usr /bin /sbin /etc ; do # /opt
[ -d "$d" ] && bargs+=( --ro-bind "$d" "$d" )
done
[ -d /usr/lib ] && bargs+=( --ro-bind /usr/lib /usr/lib )
[ -d /usr/lib32 ] && bargs+=( --ro-bind /usr/lib32 /usr/lib32 )
[ -d /usr/lib32 ] && bargs+=( --ro-bind /usr/lib32 /lib32 )
[ -d /lib ] && bargs+=( --ro-bind /lib /lib )
[ -d /lib64 ] && bargs+=( --ro-bind /lib64 /lib64 )
bargs+=( --proc /proc )
bargs+=( --ro-bind /sys /sys )
[ -d /usr/share/fonts ] && bargs+=( --ro-bind /usr/share/fonts /usr/share/fonts )
# [ -f /etc/resolv.conf ] && bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
# DNS: Arch 常见是普通文件;Ubuntu 20.04 常见是 /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf
if [ -L /etc/resolv.conf ]; then
# 只把 systemd-resolved 相关的那部分 /run 带进来,避免整个 /run 都暴露
if [ -d /run/systemd/resolve ]; then
bargs+=( --tmpfs /run )
bargs+=( --ro-bind /run/systemd/resolve /run/systemd/resolve )
else
# 极端情况:是链接但宿主机没这个目录,就退回直接 bind(可能仍会失败,但至少有兜底)
bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
fi
else
[ -f /etc/resolv.conf ] && bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
fi

# ============= 实验室GPU服务器适配 =============
local HOST_WHITELIST=(
"ubuntu"
"y"
"5024080"
)
local cur_host="$(hostname -s 2>/dev/null || hostname)"
for h in "${HOST_WHITELIST[@]}"; do
if [[ "$cur_host" == "$h" ]]; then
ro_dirs+=(
"$HOME/.oh-my-bash"
"$HOME/.bashrc"
"$HOME/.profile"
)
env_vars+=(
"FROM_BWRAP=1"
)
allowed_dirs+=(
# 服务器上的
"$HOME/bwrap_files/transfer"
"$HOME/tmp"
)
# 删除个人电脑上的/home绑定设置,改成服务器专用的
for i in "${!bind_maps_absloute[@]}"; do
if [[ "${bind_maps_absloute[i]}" == *"$HOME/Documents/bwrap_home"* ]]; then
unset 'bind_maps_absloute[i]'
bind_maps_absloute=("${bind_maps_absloute[@]}") # 可选:重建索引
break # 只删第一个;要删全部就去掉 break
fi
done
bind_maps_absloute+=("$HOME/bwrap_files/home::$HOME")

break
fi
done

# =================== /dev & /dev/shm & runtime ===================
[ -d /dev ] && bargs+=( --dev-bind /dev /dev )
[ -d /dev/shm ] && bargs+=( --bind /dev/shm /dev/shm )
[ -d "$XDG_RUNTIME_DIR" ] && bargs+=( --bind "$XDG_RUNTIME_DIR" "$XDG_RUNTIME_DIR" )


# =================== 用户自定义目录绑定 ===================

echo "脚本目录: $WORK_DIR"
echo "用户 HOME: $HOME"
echo "UID:GID: $USER_ID:$GROUP_ID"

echo "===== Binding Checking ====="
echo "ro_dirs:"
for d in "${ro_dirs[@]}"; do echo " - $d"; done
echo "allowed_dirs:"
for d in "${allowed_dirs[@]}"; do echo " - $d"; done
echo "bind_maps:"
for m in "${bind_maps[@]}"; do echo " - $m"; done
echo "bind_maps_force:"
for m in "${bind_maps_force[@]}"; do echo " - $m"; done
echo "bind_maps_absloute:"
for m in "${bind_maps_absloute[@]}"; do echo " - $m"; done
echo "===== Actual Binding ====="
# =================== 根目录绑定 ===================

echo "actual bind_maps_absloute:"
for m in "${bind_maps_absloute[@]}"; do
host="${m%%::*}"
target="${m##*::}"
[ ! -d "$host" ] && echo "$host" && continue
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done
if [[ "$WORK_DIR" == "$HOME" ]]; then
echo "警告: WORK_DIR 目录与用户主目录相同! 本次未授权访问主目录"
else
bargs+=( --bind "$WORK_DIR" "$WORK_DIR" )
fi

# =================== ro_dirs ===================
for d in "${ro_dirs[@]}"; do
[ -e "$d" ] && bargs+=( --ro-bind "$d" "$d" )
done

# =================== allowed_dirs ===================
for d in "${allowed_dirs[@]}"; do
[ -e "$d" ] && bargs+=( --bind "$d" "$d" )
done

# =================== 注入环境变量 ===================
for kv in "${env_vars[@]}"; do
key="${kv%%=*}"
val="${kv#*=}"
bargs+=( --setenv "$key" "$val" )
done

# =================== bind_maps ===================
echo "actual bind_maps:"
for m in "${bind_maps[@]}"; do
host="${m%%::*}"
rel="${m##*::}"
target="$WORK_DIR/$rel"
[ ! -d "$host" ] && continue
[ ! -d "$target" ] && mkdir -p "$target" && bind_maps_created+=("$target")
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done
echo "actual bind_maps_force:"
for m in "${bind_maps_force[@]}"; do
host="${m%%::*}"
rel="${m##*::}"
target="$WORK_DIR/$rel"
[ ! -d "$host" ] && continue
[ ! -d "$target" ] && mkdir -p "$target" && bind_maps_created+=("$target")
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done

echo "===== Cleanup Pending: ====="
echo "bind_maps_created:"
for m in "${bind_maps_created[@]}"; do echo " - $m"; done


# =================== PATH ===================
bargs+=( --setenv PATH "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$PATH" )

# 如果要source的话,这段是需要加上的
# 1) 注入一个安全的、简洁的 PS1 / 清理会搞事的 prompt 变量
bargs+=( --setenv PS1 '<sandbox>\u@\h:\w\\$ ' )
bargs+=( --setenv PROMPT_COMMAND '' )
bargs+=( --setenv RPROMPT '' )
bargs+=( --setenv VSCODE_INJECTION '' ) # 视具体环境可选
# 如果你还有别的在 host 导出的 prompt 变量(比如 ZSH_THEME),也可以在这里清空:
bargs+=( --setenv ZLE_RPROMPT '' )
# 2) 决定要执行的命令:若用户传 "bash",用干净交互 bash;否则执行传入命令
if [ "${#@}" -ge 1 ] && [ "$1" = "bash" ]; then
shift
local CMD=( bash -i "$@" )
else
# preserve original args
local CMD=( "$@" )
fi

# trap cleanup EXIT INT TERM
echo "=========="
# =================== 执行 bwrap ===================
# 外层 trap,确保脚本退出时杀掉 bwrap(额外保险)
# bwrap "${bargs[@]}" -- "$@" # 不source的时候用这段
bwrap "${bargs[@]}" -- "${CMD[@]}"

echo "=========="
echo -e "\n执行清理操作..."
if [[ -v bwrap_run_from_script ]]; then
echo "变量 bwrap_run_from_script 已设置"
unset bwrap_run_from_script
fi
# bind_maps_created+=("/tmp/114514")
for index in "${!bind_maps_created[@]}"; do
# echo "元素 $index: ${bind_maps_created[$index]}"
rm -rv "${bind_maps_created[$index]}"
done
echo "清理完成。"

)
}

if [[ ${BASH_SOURCE[0]} == $0 ]]; then
bwrap_run_from_script=1
bwrap_run $@
fi

对于普通应用程序而言

只需要给当前目录权限和/dev/nvidia*权限就行了。

当然也可以用上面那个版本,只要检测到没有只读直通$HOME就是只给当前目录权限、其他目录只读的。

这是完整脚本:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#!/usr/bin/env bash

bwrap_run() {
(
set -euo pipefail

# =================== 配置 ===================
local WORK_DIR="$(pwd)"
local USER_ID="$(id -u)"
local GROUP_ID="$(id -g)"

# 直通只读的文件夹白名单
local ro_dirs=(
# 系统配置
"$HOME/.local/share/fonts"
)

# 直通读写的文件夹白名单
local allowed_dirs=(
# 直通的目录
"$HOME/Documents/WIP"
# Proton
"$HOME/.local/share/Steam"
# UV
"$HOME/.cache/uv"
"$HOME/.local/share/uv"
"$HOME/.local/bin/uv" # 官方脚本的安装目录
# WPS
"$HOME/.config/Kingsoft"
"/opt/kingsoft"
# HMCL
"$HOME/.local/share/hmcl"
# Wine Prefix NCM
"$HOME/Documents/wine_exe"
)

# 跨磁盘允许读写用这个加白
local bind_maps_force=(
)
# 跨磁盘允许读写用这个加白, 但只允许从脚本运行时加白
local bind_maps=(
"/run/media/$USER/Disk/games::games_here"
)
local bind_maps_created=()
if [[ ! -v bwrap_run_from_script ]]; then
bind_maps=()
echo "警告: 从source的函数运行, 跳过挂载bind_maps"
fi
# 文件夹映射
local bind_maps_absloute=(
"$HOME/Documents/bwrap_home::$HOME"
)

# 环境变量
local env_vars=(
"LANG=zh_CN.UTF-8"
"LC_ALL=zh_CN.UTF-8"
# NVIDIA(有则生效)
"__GL_THREADED_OPTIMIZATIONS=1"
)

# =================== 构建 bwrap 参数 ===================
local bargs=(
--unshare-pid # 创建独立 PID namespace
--die-with-parent # 父进程退出时 bwrap 自动杀掉
--setenv HOME "$HOME"
--setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR"
--chdir "$WORK_DIR"
--tmpfs /tmp # 给 bwrap 内程序干净 tmpfs
--bind /dev/shm /dev/shm # 对 MIT-SHM 或 X11 必须的共享内存
)

echo "===== Device Checking ====="
# =================== GPU 类型检测 ===================
detect_gpu() {
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
printf '%s' "nvidia"
return
fi
if ls /dev/nvidia* >/dev/null 2>&1; then
printf '%s' "nvidia"
return
fi
for card in /sys/class/drm/card*; do
local vendor_file="$card/device/vendor"
[ -r "$vendor_file" ] || continue
local vendor_hex="$(tr -d ' \n' <"$vendor_file")"
case "$vendor_hex" in
*8086*) printf 'intel'; return ;;
*10de*) printf 'nvidia'; return ;;
*1002*) printf 'intel'; return ;;
esac
done
printf 'unknown'
}
local GPU_TYPE="$(detect_gpu || true)"
echo "检测到 GPU 类型: $GPU_TYPE"
# =================== GPU 绑定 ===================
case "$GPU_TYPE" in
nvidia)
echo "绑定 NVIDIA 设备节点"
for ndev in /dev/nvidia*; do
[ -e "$ndev" ] && bargs+=( --dev-bind "$ndev" "$ndev" )
done
[ -d /dev/dri ] && bargs+=( --dev-bind /dev/dri /dev/dri )
;;
intel|unknown)
[ -d /dev/dri ] && bargs+=( --dev-bind /dev/dri /dev/dri )
;;
esac

# =================== XDG_RUNTIME_DIR / Wayland ===================
local XDG_RUNTIME_DIR="/run/user/$USER_ID"
local WAYLAND_SOCKET=""
if [ -d "$XDG_RUNTIME_DIR" ]; then
for s in "$XDG_RUNTIME_DIR"/wayland-*; do
[ -S "$s" ] || continue
local WAYLAND_SOCKET="$s"
break
done
fi
[ -n "$WAYLAND_SOCKET" ] && echo "Wayland socket: $WAYLAND_SOCKET"
[ -n "$WAYLAND_SOCKET" ] && bargs+=( --setenv WAYLAND_DISPLAY "$(basename "$WAYLAND_SOCKET")" )

# =================== 系统库绑定 ===================
for d in /usr /bin /sbin /etc ; do # /opt
[ -d "$d" ] && bargs+=( --ro-bind "$d" "$d" )
done
[ -d /usr/lib ] && bargs+=( --ro-bind /usr/lib /usr/lib )
[ -d /usr/lib32 ] && bargs+=( --ro-bind /usr/lib32 /usr/lib32 )
[ -d /usr/lib32 ] && bargs+=( --ro-bind /usr/lib32 /lib32 )
[ -d /lib ] && bargs+=( --ro-bind /lib /lib )
[ -d /lib64 ] && bargs+=( --ro-bind /lib64 /lib64 )
bargs+=( --proc /proc )
bargs+=( --ro-bind /sys /sys )
[ -d /usr/share/fonts ] && bargs+=( --ro-bind /usr/share/fonts /usr/share/fonts )
# [ -f /etc/resolv.conf ] && bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
# DNS: Arch 常见是普通文件;Ubuntu 20.04 常见是 /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf
if [ -L /etc/resolv.conf ]; then
# 只把 systemd-resolved 相关的那部分 /run 带进来,避免整个 /run 都暴露
if [ -d /run/systemd/resolve ]; then
bargs+=( --tmpfs /run )
bargs+=( --ro-bind /run/systemd/resolve /run/systemd/resolve )
else
# 极端情况:是链接但宿主机没这个目录,就退回直接 bind(可能仍会失败,但至少有兜底)
bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
fi
else
[ -f /etc/resolv.conf ] && bargs+=( --ro-bind /etc/resolv.conf /etc/resolv.conf )
fi

# ============= 实验室GPU服务器适配 =============
local HOST_WHITELIST=(
"gpusrv"
"y"
)
local cur_host="$(hostname -s 2>/dev/null || hostname)"
for h in "${HOST_WHITELIST[@]}"; do
if [[ "$cur_host" == "$h" ]]; then
ro_dirs+=(
"$HOME/.oh-my-bash"
"$HOME/.bashrc"
"$HOME/.profile"
)
env_vars+=(
"FROM_BWRAP=1"
)
allowed_dirs+=(
# 服务器上的
"$HOME/bwrap_files/transfer"
"$HOME/tmp"
)
# 删除个人电脑上的/home绑定设置,改成服务器专用的
for i in "${!bind_maps_absloute[@]}"; do
if [[ "${bind_maps_absloute[i]}" == *"$HOME/Documents/bwrap_home"* ]]; then
unset 'bind_maps_absloute[i]'
bind_maps_absloute=("${bind_maps_absloute[@]}") # 可选:重建索引
break # 只删第一个;要删全部就去掉 break
fi
done
bind_maps_absloute+=("$HOME/bwrap_files/home::$HOME")

break
fi
done

# =================== /dev & /dev/shm & runtime ===================
[ -d /dev ] && bargs+=( --dev-bind /dev /dev )
[ -d /dev/shm ] && bargs+=( --bind /dev/shm /dev/shm )
[ -d "$XDG_RUNTIME_DIR" ] && bargs+=( --bind "$XDG_RUNTIME_DIR" "$XDG_RUNTIME_DIR" )


# =================== 用户自定义目录绑定 ===================

echo "脚本目录: $WORK_DIR"
echo "用户 HOME: $HOME"
echo "UID:GID: $USER_ID:$GROUP_ID"

echo "===== Binding Checking ====="
echo "ro_dirs:"
for d in "${ro_dirs[@]}"; do echo " - $d"; done
echo "allowed_dirs:"
for d in "${allowed_dirs[@]}"; do echo " - $d"; done
echo "bind_maps:"
for m in "${bind_maps[@]}"; do echo " - $m"; done
echo "bind_maps_force:"
for m in "${bind_maps_force[@]}"; do echo " - $m"; done
echo "bind_maps_absloute:"
for m in "${bind_maps_absloute[@]}"; do echo " - $m"; done
echo "===== Actual Binding ====="
# =================== 根目录绑定 ===================

echo "actual bind_maps_absloute:"
for m in "${bind_maps_absloute[@]}"; do
host="${m%%::*}"
target="${m##*::}"
[ ! -d "$host" ] && echo "$host" && continue
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done
if [[ "$WORK_DIR" == "$HOME" ]]; then
echo "警告: WORK_DIR 目录与用户主目录相同! 本次未授权访问主目录"
else
bargs+=( --bind "$WORK_DIR" "$WORK_DIR" )
fi

# =================== ro_dirs ===================
for d in "${ro_dirs[@]}"; do
[ -e "$d" ] && bargs+=( --ro-bind "$d" "$d" )
done

# =================== allowed_dirs ===================
for d in "${allowed_dirs[@]}"; do
[ -e "$d" ] && bargs+=( --bind "$d" "$d" )
done

# =================== 注入环境变量 ===================
for kv in "${env_vars[@]}"; do
key="${kv%%=*}"
val="${kv#*=}"
bargs+=( --setenv "$key" "$val" )
done

# =================== bind_maps ===================
echo "actual bind_maps:"
for m in "${bind_maps[@]}"; do
host="${m%%::*}"
rel="${m##*::}"
target="$WORK_DIR/$rel"
[ ! -d "$host" ] && continue
[ ! -d "$target" ] && mkdir -p "$target" && bind_maps_created+=("$target")
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done
echo "actual bind_maps_force:"
for m in "${bind_maps_force[@]}"; do
host="${m%%::*}"
rel="${m##*::}"
target="$WORK_DIR/$rel"
[ ! -d "$host" ] && continue
[ ! -d "$target" ] && mkdir -p "$target" && bind_maps_created+=("$target")
[ -e "$host" ] && bargs+=( --bind "$host" "$target" )
echo " - ${host} -> ${target}"
done

echo "===== Cleanup Pending: ====="
echo "bind_maps_created:"
for m in "${bind_maps_created[@]}"; do echo " - $m"; done


# =================== PATH ===================
bargs+=( --setenv PATH "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$PATH" )

# 如果要source的话,这段是需要加上的
# 1) 注入一个安全的、简洁的 PS1 / 清理会搞事的 prompt 变量
bargs+=( --setenv PS1 '<sandbox>\u@\h:\w\\$ ' )
bargs+=( --setenv PROMPT_COMMAND '' )
bargs+=( --setenv RPROMPT '' )
bargs+=( --setenv VSCODE_INJECTION '' ) # 视具体环境可选
# 如果你还有别的在 host 导出的 prompt 变量(比如 ZSH_THEME),也可以在这里清空:
bargs+=( --setenv ZLE_RPROMPT '' )
# 2) 决定要执行的命令:若用户传 "bash",用干净交互 bash;否则执行传入命令
if [ "${#@}" -ge 1 ] && [ "$1" = "bash" ]; then
shift
local CMD=( bash -i "$@" )
else
# preserve original args
local CMD=( "$@" )
fi

# trap cleanup EXIT INT TERM
echo "=========="
# =================== 执行 bwrap ===================
# 外层 trap,确保脚本退出时杀掉 bwrap(额外保险)
# bwrap "${bargs[@]}" -- "$@" # 不source的时候用这段
bwrap "${bargs[@]}" -- "${CMD[@]}"

echo "=========="
echo -e "\n执行清理操作..."
if [[ -v bwrap_run_from_script ]]; then
echo "变量 bwrap_run_from_script 已设置"
unset bwrap_run_from_script
fi
# bind_maps_created+=("/tmp/114514")
for index in "${!bind_maps_created[@]}"; do
# echo "元素 $index: ${bind_maps_created[$index]}"
rm -rv "${bind_maps_created[$index]}"
done
echo "清理完成。"

)
}

if [[ ${BASH_SOURCE[0]} == $0 ]]; then
bwrap_run_from_script=1
bwrap_run $@
fi


万物皆可沙箱化!BubbleWrap简单上手
http://blog.coolenoch.ink/2026/01/01/Linux/31-万物皆可沙箱化!BubbleWrap简单上手-260101/
作者
CoolestEnoch
发布于
2026年1月1日
许可协议