几乎无害:一个 shell 脚本(改编自一个真实故事)

你可能已经听过了一些 war stories,一行看似无害的 shell 脚本如何摧毁整个生产数据库,比如 A shell script that deleted a database, and how ShellCheck could have helped。这样的故事就像 urban legend,所有人都在谈论它,没有人真正见过它。 今天,我可以郑重地宣布,我在现实世界里见证过了一个 shell 脚本如何不可挽回地直接摧毁了很多用户数据,并使得部分机器直接变砖。

下面的脚本就是改编自这个真实故事。我们现在有一个大家来找茬游戏,请尽可能多、尽可能快地找出下面脚本中的茬。 当然,奖励最大的茬是为什么这个脚本会摧毁用户数据并使机器变砖。

#!/bin/bash
srvurl="http://update.example.com/release/20220329105552"
newrootv="Release20220329105552"
newroot='/writable/system-data/var/lib/snapd/snaps/rootfs-Release20220329105552.snap'
newkernelv="Release20220329105552"
newkernel='/writable/system-data/var/lib/snapd/snaps/kernel-Release20220329105552.snap'
bootenv="/writable/system-data/var/lib/snapd/snaps/bootenv"

wget -q http://update.example.com/release/20220329105552/rootfs-Release20220329105552.snap -O /writable/system-data/var/lib/snapd/snaps/rootfs-Release20220329105552.snap
wget -q http://update.example.com/release/20220329105552/kernel-Release20220329105552.snap -O /writable/system-data/var/lib/snapd/snaps/kernel-Release20220329105552.snap

upboot() {
    rm -rf /tmp/tmpboot
    mkdir -p /tmp/tmpboot
    mount /dev/nvme0n1p1 /tmp/tmpboot
    cp /tmp/tmpboot/efi/boot/bootx64.efi{,.bak}
    wget -q http://update.example.com/release/20220329105552/bootx64.efi -O /tmp/tmpboot/efi/boot/bootx64.efi
    echo -e `date '+%Y%m%d%H%M%S'` >/writable/system-data/var/lib/snapd/snaps/UpdateOtaDate
    sync
    umount /tmp/tmpboot
}
wget -q http://update.example.com/release/20220329105552/bootenv -O /writable/system-data/var/lib/snapd/snaps/bootenv
upboot
#exit 0
reboot

我找到的茬有。

#!/bin/bash
# 问题0:/bin/bash 并不一定总是存在,使用 /usr/bin/env bash 更佳。
# 问题1:下面的变量没有被真实使用,这会给后面的维护者理解这个代码造成困难。
# 问题2:因为下面的程序写死的变量过多,之后重构会不太方便。
# 容易出现部分地方被修改,部分地方没有修改。
srvurl="http://update.example.com/release/20220329105552"
newrootv="Release20220329105552"
newroot='/writable/system-data/var/lib/snapd/snaps/rootfs-Release20220329105552.snap'
newkernelv="Release20220329105552"
newkernel='/writable/system-data/var/lib/snapd/snaps/kernel-Release20220329105552.snap'
bootenv="/writable/system-data/var/lib/snapd/snaps/bootenv"

# 问题3:如果机器意外断网,下载到不完整的 snap 包,本程序依旧会接着运行,残缺的文件可能造成系统无法启动。
# 建议使用临时下载文件,下载完毕后移动到所需要的路径
# 建议使用 set -euo pipefail,防止 wget 运行失败后,程序继续运行
wget -q http://update.example.com/release/20220329105552/rootfs-Release20220329105552.snap -O /writable/system-data/var/lib/snapd/snaps/rootfs-Release20220329105552.snap
# 同问题3
wget -q http://update.example.com/release/20220329105552/kernel-Release20220329105552.snap -O /writable/system-data/var/lib/snapd/snaps/kernel-Release20220329105552.snap

upboot() {
    # 问题4:/tmp/tmpboot 是一个写死的目录,这是一个隐含的共享状态。多个程序同时运行会有重大问题。
    # 比如说实例1 mkdir 并 mount 了改目录,在实例1还没运行到 umount的时候,实例2运行到 rm -rf /tmp/tmpboot,
    # 这种情况下整个分区内的所有文件都被删除。
    # 建议1:使用文件锁防止多个实例同时运行。
    # 建议2:每个实例创建一个随机的目录,防止他们之间共享状态
    # 问题5:rm -rf 不管 /tmp/tmpboot 目录下有没有文件,直接全部清空。
    # 这不是一个好的实践,我们只有在 /tmp/tmpboot 是一个新的空目录的情况下,才应该删除它,
    # 建议使用 rmdir /tmp/tmpboot
    # 问题6:/tmp 是一个共享的临时目录,它下面的文件可能会被用户手动清空(比如用户内存不足)
    # 用户没有意识到当前 /tmp/tmpboot 目录下有极其重要的数据。这种情况也可能造成机器无法启动。
    # 建议使用非共享的临时目录来防止此类事故。
    rm -rf /tmp/tmpboot
    mkdir -p /tmp/tmpboot
    mount /dev/nvme0n1p1 /tmp/tmpboot
    # 问题7:此处虽然有备份,但是备份的 efi 文件没法用于引导系统。
    # 建议使用 efibootmgr 创建一个 boot entry
    cp /tmp/tmpboot/efi/boot/bootx64.efi{,.bak}
    # 同问题3,但是比问题2严重,因为如果程序下载失败,机器将没有可用的 bootloader。
    # 正确的方式应该是,先将文件下载到临时目录,再移动过去,因为只有 move 和 ln 操作耗时会非常短
    # 要保证这个过程程序异常中断导致系统状态异常的概率会非常低。
    wget -q http://update.example.com/release/20220329105552/bootx64.efi -O /tmp/tmpboot/efi/boot/bootx64.efi
    echo -e `date '+%Y%m%d%H%M%S'` >/writable/system-data/var/lib/snapd/snaps/UpdateOtaDate
    sync
    umount /tmp/tmpboot
}
# 同问题3
wget -q http://update.example.com/release/20220329105552/bootenv -O /writable/system-data/var/lib/snapd/snaps/bootenv
upboot
#exit 0
# 问题8:没有校验程序是否正确运行,立即重启后设备可能变砖
reboot
# 问题9:本程序升级过程步骤较多,升级并不是原子化的,可能在操作过程中间失败,但是系统还是有残留状态。
# 建议第一步,下载所需要的文件,第二步,移动文件到所需目录,第三步,更新 boot entry。
# 中间过程务必尽可能地短。比如说应该使用独立的目录 /writable/system-data/var/lib/snapd/snaps/Release20220329105552
# mv /writable/system-data/var/lib/snapd/snaps/temp /writable/system-data/var/lib/snapd/snaps/Release20220329105552
# 这样可以极大程度降低 kernel rootfs bootev 三者状态不一致的概率

最大的茬在于注释中的问题4。

Publié le par v dans «misc». Mots-clés: bash, shell, shellcheck