1
0
mirror of https://github.com/opencontainers/umoci.git synced 2026-02-05 09:45:50 +01:00
Files
umoci/test/unpack.bats
Aleksa Sarai 712f78763a *: update license headers
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
2026-01-10 11:14:14 +01:00

470 lines
13 KiB
Bash

#!/usr/bin/env bats -t
# SPDX-License-Identifier: Apache-2.0
# umoci: Umoci Modifies Open Containers' Images
# Copyright (C) 2016-2025 SUSE LLC
# Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
load helpers
function setup() {
setup_tmpdirs
setup_image
}
function teardown() {
teardown_tmpdirs
teardown_image
}
@test "umoci unpack" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# We need to make sure these files properly exist.
[ -f "$BUNDLE/config.json" ]
[ -d "$ROOTFS" ]
# Check that the image appears about right.
# NOTE: Since we could be using different images, this will be fairly
# generic.
[ -e "$ROOTFS/bin/sh" ]
[ -e "$ROOTFS/etc/passwd" ]
[ -e "$ROOTFS/etc/group" ]
# Ensure that mtree validation succeeds on the unpacked bundle.
mtree-validate -p "$ROOTFS" -f "$BUNDLE"/sha256_*.mtree
[ "$status" -eq 0 ]
[ -z "$output" ]
# Make sure that unpack fails without a bundle path.
umoci unpack --image "${IMAGE}:${TAG}"
[ "$status" -ne 0 ]
# ... or with too many
umoci unpack --image "${IMAGE}:${TAG}" too many arguments
[ "$status" -ne 0 ]
! [ -d too ]
! [ -d many ]
! [ -d arguments ]
image-verify "${IMAGE}"
}
@test "umoci unpack [invalid arguments]" {
# Missing --image and bundle argument.
umoci unpack
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Missing --image argument.
new_bundle_rootfs
umoci unpack "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Missing bundle argument.
umoci unpack --image="${IMAGE}:${TAG}"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Empty image path.
umoci unpack --image ":${TAG}" "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Non-existent image path.
umoci unpack --image "${IMAGE}-doesnotexist:${TAG}" "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Empty image source tag.
umoci unpack --image "${IMAGE}:" "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Invalid image source tag.
umoci unpack --image "${IMAGE}:${INVALID_TAG}" "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Unknown flag argument.
umoci unpack --this-is-an-invalid-argument \
--image="${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
# Too many positional arguments.
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE" \
this-is-an-invalid-argument
[ "$status" -ne 0 ]
image-verify "${IMAGE}"
}
@test "umoci unpack [config.json contains mount namespace]" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Ensure that we have a mount namespace enabled.
sane_run jq -SM 'any(.linux.namespaces[] | .type; . == "mount")' "$BUNDLE/config.json"
[ "$status" -eq 0 ]
[[ "$output" == "true" ]]
image-verify "${IMAGE}"
}
@test "umoci unpack [consistent results]" {
# Unpack the image.
new_bundle_rootfs && BUNDLE_A="$BUNDLE" ROOTFS_A="$ROOTFS"
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Wait a beat.
sleep 5s
# Unpack it again.
new_bundle_rootfs && BUNDLE_B="$BUNDLE" ROOTFS_B="$ROOTFS"
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Ensure that mtree validation cross-succeeds.
mtree-validate -p "$ROOTFS_A" -f "$BUNDLE_B"/sha256_*.mtree
[ "$status" -eq 0 ]
[ -z "$output" ]
mtree-validate -p "$ROOTFS_B" -f "$BUNDLE_A"/sha256_*.mtree
[ "$status" -eq 0 ]
[ -z "$output" ]
image-verify "${IMAGE}"
}
@test "umoci unpack [setuid]" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Make some files setuid and setgid.
touch "$ROOTFS/setuid" && chmod u+xs "$ROOTFS/setuid"
touch "$ROOTFS/setgid" && chmod g+xs "$ROOTFS/setgid"
touch "$ROOTFS/setugid" && chmod ug+xs "$ROOTFS/setugid"
# Repack the image.
umoci repack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Check that the set{uid,gid} bits were preserved.
[ -u "$ROOTFS/setuid" ]
[ -g "$ROOTFS/setgid" ]
[ -u "$ROOTFS/setugid" ] && [ -g "$ROOTFS/setugid" ]
image-verify "${IMAGE}"
}
@test "umoci unpack [setcap]" {
# We need to setcap which requires root on quite a few kernels -- and we
# don't support v3 capabilities yet (which allow us as an unprivileged user
# to write capabilities).
requires root
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Make some files setuid and setgid.
touch "$ROOTFS/setcap1" && setcap "cap_net_raw+eip" "$ROOTFS/setcap1"
touch "$ROOTFS/setcap2" && setcap "cap_sys_admin,cap_setfcap+eip" "$ROOTFS/setcap2"
# Repack the image.
umoci repack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Unpack the image (as root).
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Ensure that the capability bits were preserved.
sane_run getcap "$ROOTFS/setcap1"
[ "$status" -eq 0 ]
[[ "$output" == *" cap_net_raw=eip"* ]]
sane_run getcap "$ROOTFS/setcap2"
[ "$status" -eq 0 ]
[[ "$output" == *" cap_sys_admin,cap_setfcap=eip"* ]]
# Unpack the image (as rootless).
new_bundle_rootfs
umoci unpack --rootless --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# TODO: Actually set capabilities as an unprivileged user and then test
# that the correct v3 capabilities were set.
image-verify "${IMAGE}"
}
@test "umoci unpack [mknod]" {
# We need to mknod which requires root on most kernels. Since Linux 4.18 it's
# been possible for unprivileged users to mknod(2) but we can't use that here
# (it requires owning the filesystem's superblock).
requires root
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Make some mknod.
mknod "$ROOTFS/block1" b 128 42 # 80:2a 61a4
mknod "$ROOTFS/block2" b 255 128 # ff:80 61a4
mknod "$ROOTFS/char1" c 133 37 # 85:25 21a4
mknod "$ROOTFS/char2" c 253 97 # fd:61 21a4
mkfifo "$ROOTFS/fifo"
# Repack the image.
umoci repack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Unpack the image (as root).
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Check that all of the bits were preserved.
[ -b "$ROOTFS/block1" ]
[[ "$(stat -c '%t:%T' "$ROOTFS/block1")" == *"80:2a"* ]]
[ -b "$ROOTFS/block2" ]
[[ "$(stat -c '%t:%T' "$ROOTFS/block2")" == *"ff:80"* ]]
[ -c "$ROOTFS/char1" ]
[[ "$(stat -c '%t:%T' "$ROOTFS/char1")" == *"85:25"* ]]
[ -c "$ROOTFS/char2" ]
[[ "$(stat -c '%t:%T' "$ROOTFS/char2")" == *"fd:61"* ]]
[ -p "$ROOTFS/fifo" ]
# Unpack the image (as rootless).
new_bundle_rootfs
umoci unpack --rootless --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# At the least, check that the files exist.
[ -e "$ROOTFS/block1" ]
[ -e "$ROOTFS/block2" ]
[ -e "$ROOTFS/char1" ]
[ -e "$ROOTFS/char2" ]
# But the FIFOs should be preserved.
[ -p "$ROOTFS/fifo" ]
image-verify "${IMAGE}"
}
@test "umoci unpack --keep-dirlinks" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Create some links for us to play with in the next layer.
mkdir "$ROOTFS/dir"
touch "$ROOTFS/dir/a"
ln -s dir "$ROOTFS/link"
ln -s link "$ROOTFS/link2"
ln -s loop2 "$ROOTFS/loop1"
ln -s loop3 "$ROOTFS/loop2"
ln -s link2/loop4 "$ROOTFS/loop3"
ln -s ../loop1 "$ROOTFS/dir/loop4"
chmod 000 "$ROOTFS/dir"
# Repack the image.
umoci repack --refresh-bundle --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "$IMAGE"
# Create a fake rootfs which contains entries inside symlinks.
ROOTFS="$(setup_tmpdir)"
mkdir "$ROOTFS/link" # == /dir
touch "$ROOTFS/link/b"
mkdir "$ROOTFS/link2" # == /link == /dir
touch "$ROOTFS/link2/c"
mkdir "$ROOTFS/loop1" # == /loop{1..4} ... (symlink loop)
touch "$ROOTFS/loop1/broken"
sane_run tar cvfC "$UMOCI_TMPDIR/layer1.tar" "$ROOTFS" .
[ "$status" -eq 0 ]
# Insert our fake layer manually.
umoci raw add-layer --image "${IMAGE}:${TAG}" "$UMOCI_TMPDIR/layer1.tar"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Unpack our weird image.
new_bundle_rootfs
umoci unpack --keep-dirlinks --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Resolution of links without destroying the links themselves.
chmod 755 "$ROOTFS/dir"
[ -f "$ROOTFS/dir/a" ]
[ -f "$ROOTFS/dir/b" ]
[ -f "$ROOTFS/dir/c" ]
[ -L "$ROOTFS/link" ]
[ -L "$ROOTFS/link2" ]
[ "$(readlink "$ROOTFS/link")" = "dir" ]
[ "$(readlink "$ROOTFS/link2")" = "link" ]
# ... but symlink loops have to be broken.
[ -d "$ROOTFS/loop1" ]
[ -f "$ROOTFS/loop1/broken" ]
[ -L "$ROOTFS/loop2" ]
[ -L "$ROOTFS/loop3" ]
[ -L "$ROOTFS/dir/loop4" ]
[ "$(readlink "$ROOTFS/loop2")" = "loop3" ]
[ "$(readlink "$ROOTFS/loop3")" = "link2/loop4" ]
[ "$(readlink "$ROOTFS/dir/loop4")" = "../loop1" ]
}
@test "umoci unpack [mixed compression]" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
# Create a few layers with different compression algorithms.
# zstd layer
touch "$ROOTFS/zstd1"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=zstd "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# gzip layer
touch "$ROOTFS/gzip1"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=gzip "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# plain layer
touch "$ROOTFS/plain1"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=none "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# zstd layer
touch "$ROOTFS/zstd2"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=zstd "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# plain layer
touch "$ROOTFS/plain2"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle --compress=none "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# zstd layer (auto)
touch "$ROOTFS/zstd3"
umoci repack --image "${IMAGE}:${TAG}" --refresh-bundle "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Re-extract the latest image and make sure all of the files were correctly
# extracted.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
[ -f "$ROOTFS/zstd1" ]
[ -f "$ROOTFS/gzip1" ]
[ -f "$ROOTFS/plain1" ]
[ -f "$ROOTFS/zstd2" ]
[ -f "$ROOTFS/plain2" ]
[ -f "$ROOTFS/zstd3" ]
}
@test "umoci unpack [config.json HOME]" {
# Unpack the image.
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
user="dummy-$RANDOM"
uid="100$RANDOM"
home="/foo/bar/baz"
# Add entries to /etc/{passwd,group}.
echo "$user:x:$uid:$uid::$home:/bin/bash" >>"$ROOTFS/etc/passwd"
echo "$user:!:$uid:" >>"$ROOTFS/etc/group"
umoci repack --image "${IMAGE}:${TAG}" "$BUNDLE"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
# Add a user configuration, and make sure that HOME is not set.
umoci config --image "${IMAGE}:${TAG}" \
--config.user "$user:$user" \
--clear config.env
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "${BUNDLE}"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
sane_run jq -Mr '.process.env[]' "$BUNDLE/config.json"
[ "$status" -eq 0 ]
grep -Fx "HOME=$home" <<<"$output"
# Now set HOME to some other value -- that should take precedence.
other_home="/other/home"
umoci config --image "${IMAGE}:${TAG}" --config.env "HOME=$other_home"
[ "$status" -eq 0 ]
image-verify "${IMAGE}"
new_bundle_rootfs
umoci unpack --image "${IMAGE}:${TAG}" "${BUNDLE}"
[ "$status" -eq 0 ]
bundle-verify "$BUNDLE"
sane_run jq -Mr '.process.env[]' "$BUNDLE/config.json"
[ "$status" -eq 0 ]
grep -Fx "HOME=$other_home" <<<"$output"
}