針對 cgroup 環境因中間層組件對新 syscall 不佳的 fallback 機制導致操作阻斷的變通解決方案

這篇文章是針對宿主 Ubuntu 18.04, 容器 ArchLinux 而言, 但是我認爲這也會發生在同
樣是滾動更新的 Fedora 上.
和之前的文章一樣, 沒有 md 版本. 最多隻有嵌入到文章裏的腳本編碼.
相關命令, 出於便於理解, 減少混淆的角度出發, 是能用長參數, 就用長參數. 
使用此方法, 風險自擔.

起因
目前發現, ArchLinux 在 glibc 升級到至少是 2.33-3 這個版本後, 通過 Ubuntu 18.04 
的 

systemd-nspawn --directory /IMG_DIR 

進入 bash 後, 無法通過 pacman 進行更新. 直接運行 pacman 即可看
到存在錯誤提示

error: failed to initialize alpm library
(could not find or read directory: /var/lib/pacman/)

更準確來說, 是 pacman 更新成功完成後, 立即發生, 不用退出 systemd-nspawn, 再重新
進入. 只要執行 

pacman 

就可出現上出錯誤提示. 

如果單單從提示看是一個名爲 alpm 庫的原因, 或者是錯誤修改了 /var/lib/pacman 目錄
訪問權限. 

執行 

test -r /; echo $? 

返回代碼爲 1, 而不是期望的 0. 知道這個方法, 是一開始自己以爲是有哪個 so 文件有
問題, 所以用 ldd 查看, 結果報錯. 而 ldd 是一個腳本, 所以在裏面開 set -x, 然後就
發現了.

這裏可以看出問題, 可能與 so 文件損壞或者錯誤的目錄權限設置無關.

用 

strace -c pacman
strace pacman 2>&1 | less


可以看到一個名爲 faccessat2 的系統調用報錯. 據此在網上搜索, 可看到在 20 年 8 月
有一個關於此的討論

Newer Glibc use faccessat2 to implement faccessat #16739
https://github.com/systemd/systemd/pull/16739

我沒有查看較老版本 systemd-nspawn 是如何處理宿主系統不支持 syscall 的代碼. 也不
知道較老版本的 Ubuntu LTS 是否會 backport 到 systemd-nspawn. 所以我這裏選擇了兩
種變通解決方案.

補充說明:
systemd 環境下, 查看當前系統支持哪些 syscall

systemd-analyze syscall-filter

在 Ubuntu 18.04 所支持的兩種內核都存在此問題, 4.15 和 hwe 內核.

可通過

pacman --query --list glibc | less
dpkg-query --listfiles libc6-dev | less

查看 c header 文件, 來確認當前系統的 glibc 是否支持特定 syscall 的調用.

變通解決方案
無論是哪種方法, 其核心有很大部分是容器內的相關包, 自己做了 fallback. 如果其強制
指定用新 syscall, 則兩種方法皆失效.

第一種方案

使用 systemd-chroot, 把 ArchLinux 的更新制作成宿主系統的一個 .serivce 
文件.

因爲在我之前的文章裏, 使用此方案來運行一些 ArchLinux 的軟件, 加上使用
systemd-nspawn 是進行 ArchLinux 更新, 所以自然而然選擇了這種方式.
具體配置方法, 參照 man systemd.service, man systemd.unit, man systemd.kexec 等
manual, 或者 man -k systemd, 也包括我之前的文章. 
這裏只說明一個關鍵的地方. 需要鏡像裏的

/etc/mtab

文件, 存在關於鏡像 / 目錄的定義. 從自己簡單的測試看, 只要有一條這個定義就可以完
成更新. 添加該定義的一種簡單方法, 是先用

systemd-nspawn --directory /IMG_DIR

然後在容器內執行

findmnt /

根據輸出內容手動在容器的 /etc/mtab 填寫相應的內容. 或者在容器外通過

findmnt /IMG_DIR

獲得相關信息, 然後通過腳本生成相關信息給容器.
然後在容器內的適當位置放置一個腳本, 比如是 /usr/local/bin 目錄, 該腳本用於更新.
在更新前, 如果有報錯, 可能需要先執行

pacman-db-upgrade

修復 pacman 的數據庫. 此命令不需要添加額外參數.
用這種方法, 看結果比較麻煩, 我是在宿主系統用 journalctl 看的, 而且不好交互.
針對這種方法, 補充兩個用於更新的關鍵 pacman 命令參數

pacman --sync --refresh --sysupgrade --noconfirm

更新數據庫以及包. 使用長參數, 這樣即便是初次接觸 Arch 的用戶應該也能一定程度知
曉命令含義.

echo -e 'y\ny\n' | pacman --sync --clean --clean

自動清理緩存.
可考慮通過分析第一條命令的執行結果, 來自動執行第二個命令.

第二種方案

基於自己之前看過的一些文章. Linux 發行版不像 Windows 那樣系統底層集
成了可以控制任意進程是否具有入站/出站的防火牆功能. 我看到有人通過 

unshare
nsenter

這些工具, 用不太方便不太優雅的方式實現了這個需求. 
這裏我不限制容器內進程訪問網絡的需求, 而是儘量限制容器以 root 運行進程的可能
對宿主系統造成潛在影響. 簡單來說, 就是通過 unshare/nsenter 結合 mount/bind 仿造
systemd-nspawn 限制容器內以 root 方式運行的進程.

這個需求靠兩個腳本實現. 一個位於宿主系統, 一個位於容器內.
必須強調的是, 腳本我寫的簡單, 沒有明確提示用戶此時是在宿主系統還是容器. 所以操
作時務必小心, 以免操作錯對象, 導致不必要的損失.
相關 unshare/nsenter 的說明, 參見其 manual.

首先是宿主系統的腳本. 我起的名字是 enter-arch-apps

#!/bin/bash

set -o nounset
export IFS=$'\n'

declare -r IMG_DIR='/entry/arch/apps/rw'
declare -r UTS_FILE='/entry/arch/apps/misc/arch.image'
declare -r HOST_NAME='arch.image'

function prepare()
{
    findmnt --mountpoint ${IMG_DIR}'/sys' &> /nul
    [ $? -ne 0 ] && mount /sys ${IMG_DIR}'/sys' --options=bind,private,ro
}

function unmount-ns()
{
    while [ 0 ];
    do
        umount ${UTS_FILE} 2> /nul
        [ $? -ne 0 ] && break
    done

    while [ 0 ];
    do
        umount ${IMG_DIR}'/sys' 2> /nul
        [ $? -ne 0 ] && break
    done
}

function finalize()
{
    unmount-ns
}

trap finalize EXIT

function main()
{
    [ ${UID} -ne 0 ] && return 1

    [ ! -f ${IMG_DIR}'/etc/os-release' ] && return 1
    [ ! -f ${UTS_FILE} ] && return 1

    prepare

    unshare --uts=${UTS_FILE} hostname ${HOST_NAME}
    nsenter --uts=${UTS_FILE} unshare --ipc --pid --mount --cgroup --fork \
                    chroot ${IMG_DIR} /usr/local/bin/unshare-shell

    return $?
}

main

這裏簡單解釋一下: 
1. 因爲我想指定容器內系統的名字, 所以使用了命令 nsenter. 如不想, 可以直接使用 
unshare. 

2. 目錄 /entry/arch/apps/... 這個是自定義關於容器的目錄, 參見我之前的文章.

3. 用於 unshare namespace 的文件, 需要先行自行建立. 

4. 我注意到使用 unshare namespace 操作的那些文件, 比如這裏的 arch.image. 存
在重複掛載的問題, 所以用了個循環來 umount. /nul 這些是 /dev/null 這些文件的符號
鏈接, 可以少寫幾個字.

5. 從自己實際的操作看, 只需要存在有效的 /proc 目錄, 即可完成 pacman 相關操作. 
但是因爲沒有做廣泛測試, 所以還是參考 systemd-nspawn, 爲容器添加有效的 /sys 目錄
, 以及 /run 目錄.

6. 處於安全考慮, 同樣參考 systemd-nspawn 把個別系統目錄 ro 掛載. 雖然程序可以改
回 rw.

7. 最後上述提到的系統目錄掛載, 我是放在容器內的腳本. 比如 /proc, 雖然 unshare 
有一個參數, 可自動掛載, 但是在 chroot 調用時沒有看到其存在, 可能其不適用於 
chroot.

這裏是放在容器內的腳本, 我起名 /usr/local/bin/unshare-shell

#!/bin/bash

set -o nounset
export IFS=$'\n'

declare -r HOST='arch.image'

grep --quiet "${HOST}" /etc/hostname
[ $? -ne 0 ] && exit 1

mount proc /proc --types proc --options=rw,nosuid,nodev,noexec,relatime
# mount sys /sys --types sysfs --options=rw,nosuid,nodev,noexec,relatime
mount tmpfs /run --types tmpfs --options=rw,nosuid,noexec,relatime,size=798576k,mode=755
mount /proc/sys /proc/sys --options=bind,private,ro
mount /proc/sysrq-trigger /proc/sysrq-trigger --options=bind,private,ro

if [[ ! -c /dev/pts/1 || ! -e /dev/pts/1 ]]; then
  mkdir --parent /dev/pts
  mknod --mode=620 /dev/pts/1 c 136 1
fi

if [[ ! -c /dev/null || ! -e /dev/null ]]; then
  [ -f /dev/null ] && rm /dev/null
  mknod --mode=666 /dev/null c 1 3
fi

if [[ ! -c /dev/zero || ! -e /dev/zero ]]; then
  [ -f /dev/zero ] && rm /dev/zero
  mknod --mode=666 /dev/zero c 1 5
fi

ln --symbolic --force /proc/self/fd/0 /dev/stdin
ln --symbolic --force /proc/self/fd/1 /dev/stdout
ln --symbolic --force /proc/self/fd/2 /dev/stderr

unset DISPLAY GTK_IM_MODULE QT_IM_MODULE XAUTHORITY XMODIFIERS
export SUDO_UID=0
export SUDO_GID=0
export SUDO_USER=root
export HOME=/root

/bin/bash

這裏依舊簡單解釋一下:
1. 是容器的 /usr/local/bin, 不是宿主的.

2. mount 用於創建屬於容器的 /proc, 以及對某些系統目錄掛載爲 ro. 個別參數我是硬
編碼. 需要根據自己的環境進行更改. 相關參數來自 findmnt 輸出.

3. 清空不必要的環境變量. 同時修改相關的 UID 和 GID, 我不想在容器內部映射一個和
宿主系統用戶 UID/GID 相同的用戶, 而是直接用 root, 反正只是更新用.

4. 沒有找到很好的, 可指示 bash 交互式 shell, 啓動後自動進入特定目錄的方法. 所以
是在 /root/.bashrc 寫入

cd DIR

3. 仿造宿主系統, 在 /dev 目錄創建基本的若干字符文件. stdin, stdout, stderr, 
zero, null. 由於是在容器內, 所以對已存在的文件, 處理比較粗暴.

這個方法在實際使用和 systemd-nspawn 沒有什麼差別.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章