#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail

# check dfuzzer is present before testing
if ! command -v dfuzzer &>/dev/null; then
    echo "dfuzzer is not installed, skipping" | tee --append /skipped
    exit 77
fi

# Save the end.service state before we start fuzzing, as it might get changed
# on the fly by one of the fuzzers
systemctl list-jobs | grep -F 'end.service' && SHUTDOWN_AT_EXIT=1 || SHUTDOWN_AT_EXIT=0

# shellcheck disable=SC2317
at_exit() {
    set +e
    # We have to call the end.service/poweroff explicitly even if it's specified on
    # the kernel cmdline via systemd.wants=end.service, since dfuzzer calls
    # org.freedesktop.systemd1.Manager.ClearJobs() which drops the service
    # from the queue
    if [[ $SHUTDOWN_AT_EXIT -ne 0 ]] && ! systemctl poweroff; then
        # PID1 is down let's try to save the journal
        journalctl --sync      # journal can be down as well so let's ignore exit codes here
        systemctl -ff poweroff # sync() and reboot(RB_POWER_OFF)
    fi
}

add_suppression() {
    local interface="${1:?}"
    local suppression="${2:?}"

    sed -i "\%\[$interface\]%a$suppression" /etc/dfuzzer.conf
}

trap at_exit EXIT

systemctl log-level info

# FIXME: systemd-run doesn't play well with daemon-reexec
# See: https://github.com/systemd/systemd/issues/27204
add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Manager:Reexecute FIXME"

add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Manager:SoftReboot destructive"
add_suppression "org.freedesktop.login1" "Sleep destructive"

# Skip calling start and stop methods on unit objects, as doing that is not only time consuming, but it also
# starts/stops units that interfere with the machine state. The actual code paths should be covered (to some
# degree) by the respective method counterparts on the manager object.
for method in Start Stop Restart ReloadOrRestart ReloadOrTryRestart Kill; do
    add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Unit:$method"
done

cat /etc/dfuzzer.conf

# TODO
#   * check for possibly newly introduced buses?
BUS_LIST=(
    org.freedesktop.home1
    org.freedesktop.hostname1
    org.freedesktop.import1
    org.freedesktop.locale1
    org.freedesktop.login1
    org.freedesktop.machine1
    org.freedesktop.portable1
    org.freedesktop.resolve1
    org.freedesktop.systemd1
    org.freedesktop.timedate1
)

# systemd-oomd requires PSI
if tail -n +1 /proc/pressure/{cpu,io,memory}; then
    BUS_LIST+=(
        org.freedesktop.oom1
    )
fi

# Some services require specific conditions:
#   - systemd-timesyncd can't run in a container
#   - systemd-networkd can run in a container if it has CAP_NET_ADMIN capability
if ! systemd-detect-virt --container; then
    BUS_LIST+=(
        org.freedesktop.network1
        org.freedesktop.timesync1
    )
elif busctl introspect org.freedesktop.network1 / &>/dev/null; then
    BUS_LIST+=(
        org.freedesktop.network1
    )
fi

SESSION_BUS_LIST=(
    org.freedesktop.systemd1
)

# Maximum payload size generated by dfuzzer (in bytes) - default: 50K
PAYLOAD_MAX=50000
# Tweak the maximum payload size if we're running under sanitizers, since
# with larger payloads we start hitting reply timeouts
if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
    PAYLOAD_MAX=10000 # 10K
fi

# Overmount /var/lib/machines with a size-limited tmpfs, as fuzzing
# the org.freedesktop.machine1 stuff makes quite a mess
mount -t tmpfs -o size=50M tmpfs /var/lib/machines

# Fuzz both the system and the session buses (where applicable)
for bus in "${BUS_LIST[@]}"; do
    echo "Bus: $bus (system)"
    systemd-run --pipe --wait \
                -- dfuzzer -b "$PAYLOAD_MAX" -n "$bus"

    # Let's reload the systemd daemon to test (de)serialization as well
    systemctl daemon-reload
    # FIXME: explicitly trigger reexecute until systemd/systemd#27204 is resolved
    systemctl daemon-reexec
done

umount /var/lib/machines

for bus in "${SESSION_BUS_LIST[@]}"; do
    echo "Bus: $bus (session)"
    systemd-run --machine 'testuser@.host' --user --pipe --wait \
                -- dfuzzer -b "$PAYLOAD_MAX" -n "$bus"

    # Let's reload the systemd user daemon to test (de)serialization as well
    systemctl --machine 'testuser@.host' --user daemon-reload
    # FIXME: explicitly trigger reexecute until systemd/systemd#27204 is resolved
    systemctl --machine 'testuser@.host' --user daemon-reexec
done

touch /testok
