Skip to content

Commit

Permalink
test: run dbus-broker under ASan and UBsan
Browse files Browse the repository at this point in the history
Let's introduce a test that runs dbus-broker under Address Sanitizer and
Undefined Behavior Sanitizer, while running other tests against it.

The setup to achieve this is slightly convoluted, since we need to run
(and restart) sanitized dbus-broker without nuking the host machine. For
that we setup an nspawn-container that re-uses host's rootfs (to some
degree) and overlays our additions on top of that. This way we can test
(not-only) the full user-space boot with sanitized dbus-broker without
risking "damage" to the host machine.
  • Loading branch information
mrc0mmand committed Apr 17, 2024
1 parent 9eb0b7e commit 4dd4726
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 0 deletions.
18 changes: 18 additions & 0 deletions test/integration/fuzz/sanitizers/main.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
summary: Concise summary describing what the test does
test: ./test.sh
recommend:
- dbus-daemon
- dfuzzer
- expat-devel
- gcc
- git
- glibc-devel
- libasan
- libubsan
- meson
- systemd
- systemd-container
- systemd-devel
- systemd-libs
- util-linux
duration: 30m
207 changes: 207 additions & 0 deletions test/integration/fuzz/sanitizers/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#!/bin/bash
# vi: set sw=4 ts=4 et tw=110:
# shellcheck disable=SC2016

set -eux
set -o pipefail

export ASAN_OPTIONS=strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1:detect_invalid_pointer_pairs=2:handle_ioctl=1:print_cmdline=1:disable_coredump=0:use_madv_dontdump=1
export UBSAN_OPTIONS=print_stacktrace=1:print_summary=1:halt_on_error=1

# shellcheck disable=SC2317
at_exit() {
set +e

# Let's do some cleanup and export logs if necessary

if [[ -n "${CONTAINER:-}" ]]; then
if systemctl -q is-active "systemd-nspawn@$CONTAINER.service"; then
systemctl stop "systemd-nspawn@$CONTAINER.service"
fi

# Export the container journal and sanitizer logs if $TMT_TEST_DATA is set, either by TMT directly
# manually.
if [[ -n "${TMT_TEST_DATA:-}" ]]; then
journalctl -D "/var/log/journal/${CONTAINER_ID:?}" -b -o short-monotonic >"$TMT_TEST_DATA/container-journal.log"
fi

rm -rf "/var/lib/machines/$CONTAINER"
rm -rf "/var/log/journal/$CONTAINER_ID"
rm -rf "/run/systemd/system/systemd-nspawn@$CONTAINER.service.d"
systemctl daemon-reload
fi
}

trap at_exit EXIT

# Switch SELinux to permissive (if enabled), so it doesn't interfere with the container shenanigans below.
setenforce 0 || :
# We need persistent journal for the systemd-nspawn --link= stuff
mkdir -p /var/log/journal
journalctl --flush

: "=== Prepare dbus-broker's source tree ==="
# Since we need to build dbus-broker from scratch, let's do some magic to get the correct source tree.
if [[ -n "${PACKIT_TARGET_URL:-}" ]]; then
# If we're running in Packit's context, use the set of provided environment variables to checkout the
# correct branch (and possibly rebase it on top of the latest source base branch so we always test the
# latest revision possible).
git clone "$PACKIT_TARGET_URL" dbus-broker
cd dbus-broker
git checkout "$PACKIT_TARGET_BRANCH"
# If we're invoked from a pull request context, rebase on top of the latest source base branch.
if [[ -n "${PACKIT_SOURCE_URL:-}" ]]; then
git remote add pr "${PACKIT_SOURCE_URL:?}"
git fetch pr "${PACKIT_SOURCE_BRANCH:?}"
git merge "pr/$PACKIT_SOURCE_BRANCH"
fi
git log --oneline -5
elif [[ -n "${DBUS_BROKER_TREE:-}" ]]; then
# Useful for quick local debugging when running this script directly, e.g. running
#
# $ DBUS_BROKER_TREE=$PWD test/integration/fuzz/sanitizers/test.sh
#
# from the dbus-broker repo root.
cd "${DBUS_BROKER_TREE:?}"
else
# If we're running outside of Packit's context, pull the latest dbus-broker upstream.
git clone https://github.com/bus1/dbus-broker dbus-broker
git log --oneline -5
fi


: "=== Build dbus-broker with sanitizers and run the unit test suite ==="
meson setup build-san --wipe -Db_sanitize=address,undefined -Dprefix=/usr
ninja -C build-san
meson test -C build-san --timeout-multiplier=2 --print-errorlogs


: "=== Run tests against dbus-broker running under sanitizers ==="
# So, this one is a _bit_ convoluted. We want to run dbus-broker under sanitizers, but this bears a couple of
# issues:
#
# 1) We need to restart dbus-broker (and hence the machine we're currently running on)
# 2) If dbus-broker crashes due to ASan/UBSan error, the whole machine is hosed
#
# To make the test a bit more robust without too much effort, let's use systemd-nspawn to run an ephemeral
# container on top of the current rootfs. To get the "sanitized" dbus-broker into that container, we need to
# prepare a special rootfs with just the sanitized dbus-broker (and a couple of other things) which we then
# simply overlay on top of the ephemeral rootfs in the container.
#
# This way, we'll do a full user-space boot with a sanitized dbus-broker without affecting the host machine,
# and without having to build a custom container/VM just for the test.
CONTAINER="dbus-broker-sanitizers-$RANDOM"
CONTAINER_ID="$(systemd-id128 new)"
CONTAINER_OVERLAY="/var/lib/machines/$CONTAINER"

# Prepare the nspawn container service
mkdir -p "/var/lib/machines/$CONTAINER"
# Notes:
# - with systemd v256+ this can be replaced by systemctl edit --stdin --runtime ..., and the
# mkdir/daemon-reload can be dropped
# - systemd-nspawn can't overlay the whole rootfs (/), so we need to cherry-pick a couple of subdirectories
# we're interested in (in this case it's pretty simple, since dbus-broker installs everything under /usr,
# and we need /etc with our dbus-broker.service override)
# - since the whole container is ephemeral, use --link-journal=host, so the journal directory for the
# container is created on the _host_ under /var/log/journal/<machine-id> and bind-mounted into the
# container; that way we can fetch the container journal for debugging even if something goes horribly
# wrong
mkdir -p "/run/systemd/system/systemd-nspawn@$CONTAINER.service.d"
cat >"/run/systemd/system/systemd-nspawn@$CONTAINER.service.d/override.conf" <<EOF
[Service]
ExecStart=
ExecStart=systemd-nspawn --quiet --private-network --keep-unit --machine=%i --boot \
--link-journal=host \
--volatile=yes \
--directory=/ \
--uuid=$CONTAINER_ID \
--hostname=$CONTAINER \
--overlay-ro=/etc:$CONTAINER_OVERLAY/etc:/etc \
--overlay-ro=/usr:$CONTAINER_OVERLAY/usr:/usr
EOF
systemctl daemon-reload

# Prepare the nspawn container overlay
#
# Install sanitized dbus-broker into the overlay
DESTDIR="$CONTAINER_OVERLAY" ninja -C build-san install
# Let systemd-nspawn propagate the machine ID we passed it via --uuid=
mkdir "$CONTAINER_OVERLAY/etc"
: >"$CONTAINER_OVERLAY/etc/machine-id"
# Pass $ASAN_OPTIONS and $UBSAN_OPTIONS to the dbus-broker service in the container
mkdir -p "$CONTAINER_OVERLAY/etc/systemd/system/dbus-broker.service.d/"
cat >"$CONTAINER_OVERLAY/etc/systemd/system/dbus-broker.service.d/sanitizer-env.conf" <<EOF
[Service]
Environment=ASAN_OPTIONS=$ASAN_OPTIONS
Environment=UBSAN_OPTIONS=$UBSAN_OPTIONS
# Useful for debugging LSan errors, but it's very verbose, hence disabled by default
#Environment=LSAN_OPTIONS=verbosity=1:log_threads=1
EOF
# Run both dbus-broker-launch and dbus-broker under root instead of the usual "dbus" user. This is necessary
# to let sanitizers generate stack traces (killing the process on sanitizer error works even without this
# tweak though, but it's very hard to then tell what went wrong without a stack trace).
mkdir -p "$CONTAINER_OVERLAY/etc/dbus-1/"
cat >"$CONTAINER_OVERLAY/etc/dbus-1/system-local.conf" <<EOF
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<user>root</user>
</busconfig>
EOF

# Wrap the long-ish systemd-run cmdline in something a bit shorter
CONTAINER_RUN=(systemd-run -M "$CONTAINER" -q --wait --pipe)

check_journal_for_sanitizer_errors() {
if journalctl -q -D "/var/log/journal/${CONTAINER_ID:?}" --grep "SUMMARY:.+Sanitizer"; then
# Dump all messages recorded for the dbus-broker.service, as that's usually where the stack trace ends
# up. If that's not the case, the full container journal is exported on test exit anyway, so we'll
# still have everything we need to debug the fail further.
journalctl -q -D "/var/log/journal/${CONTAINER_ID:?}" -o short-monotonic --no-hostname -u dbus-broker.service --no-pager
exit 1
fi
}

run_and_check() {
# Run the passed command in the container
"${CONTAINER_RUN[@]}" "$@"
# Check if dbus-broker is still running...
"${CONTAINER_RUN[@]}" systemctl status --full --no-pager dbus-broker.service
# ... and if it didn't generate any sanitizer errors
check_journal_for_sanitizer_errors
}

# Start the container and wait until it's fully booted up
machinectl start "$CONTAINER"
timeout 30s bash -ec "until ${CONTAINER_RUN[*]} true; do sleep .5; done"
# is-system-running returns > 0 if the system is running in degraded mode, but we don't care about that, we
# just need to wait until the bootup is finished
"${CONTAINER_RUN[@]}" systemctl is-system-running -q --wait || :
"${CONTAINER_RUN[@]}" systemctl status --full --no-pager dbus-broker.service
# Check if dbus-broker runs under root, see above for reasoning
"${CONTAINER_RUN[@]}" bash -xec '[[ $(stat --format=%u /proc/$(systemctl show -P MainPID dbus-broker.service)) -eq 0 ]]'
# Make _extra_ sure we're running the sanitized dbus-broker with the correct environment
"${CONTAINER_RUN[@]}" bash -xec 'ldd /proc/$(systemctl show -P MainPID dbus-broker.service)/exe | grep -qF libasan.so'
"${CONTAINER_RUN[@]}" bash -xec 'ldd $(command -v dbus-broker-launch) | grep -qF libasan.so'
"${CONTAINER_RUN[@]}" bash -xec 'ldd $(command -v dbus-broker) | grep -qF libasan.so'
"${CONTAINER_RUN[@]}" systemctl show -p Environment dbus-broker.service | grep -q ASAN_OPTIONS
journalctl -D "/var/log/journal/${CONTAINER_ID:?}" -e -n 10 --no-pager
check_journal_for_sanitizer_errors

# Now we should have a container ready for our shenanigans

# Let's start with something simple and run dfuzzer on the org.freedesktop.DBus bus
run_and_check dfuzzer -v -n org.freedesktop.DBus
# TODO

# Shut down the container and check for any sanitizer errors, since some of the errors can be detected only
# after we start shutting things down.
#
# Note: machinectl poweroff doesn't wait until the container shuts down completely, stop stop the service
# behind it instead which does wait
systemctl stop "systemd-nspawn@$CONTAINER.service"
check_journal_for_sanitizer_errors
# Also, check if dbus-broker didn't fail during the lifetime of the container
(! journalctl -q -D "/var/log/journal/$CONTAINER_ID" _PID=1 --grep "dbus-broker.service.*Failed with result")

exit 0

0 comments on commit 4dd4726

Please sign in to comment.