#!/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

if [[ ! -v ASAN_OPTIONS && ! -v UBSAN_OPTIONS && "${TEST_RUN_DFUZZER:-0}" == "0" ]]; then
    echo "no sanitizer is enabled, skipping. (Hint: set TEST_RUN_DFUZZER=1 to run test forcibly)."
    exit 77
fi

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

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

systemctl log-level info

# 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.
MANAGER_METHOD_FILTER=(
    StartUnit
    StartUnitWithFlags
    StartUnitReplace
    StopUnit
    RestartUnit
    TryRestartUnit
    ReloadOrRestartUnit
    ReloadOrTryRestartUnit
    KillUnit
    QueueSignalUnit
    FreezeUnit
    AttachProcessesToUnit
    RemoveSubgroupFromUnit
    AbandonScope
    CancelJob
    Exit
    Reboot
    SoftReboot
    PowerOff
    Halt
    KExec
    SwitchRoot
    EnqueueMarkedJobs
)
UNIT_METHOD_FILTER=(
    Start
    Stop
    Restart
    TryRestart
    ReloadOrRestart
    ReloadOrTryRestart
    Kill
    QueueSignal
    Freeze
)
SCOPE_METHOD_FILTER=(
    Abandon
)
JOB_METHOD_FILTER=(
    Cancel
)
LOGIN_METHOD_FILTER=(
    FlushDevices # This triggers all devices and makes the system super heavy
    PowerOff
    PowerOffWithFlags
    Reboot
    RebootWithFlags
    Halt
    HaltWithFlags
    Suspend
    SuspendWithFlags
    Hibernate
    HibernateWithFlags
    HybridSleep
    HybridSleepWithFlags
    SuspendThenHibernate
    SuspendThenHibernateWithFlags
    ScheduleShutdown
)
for method in "${MANAGER_METHOD_FILTER[@]}"; do
    add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Manager:$method"
done
for method in "${UNIT_METHOD_FILTER[@]}"; do
    add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Unit:$method"
done
for method in "${SCOPE_METHOD_FILTER[@]}"; do
    add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Scope:$method"
done
for method in "${JOB_METHOD_FILTER[@]}"; do
    add_suppression "org.freedesktop.systemd1" "org.freedesktop.systemd1.Job:$method"
done
for method in "${LOGIN_METHOD_FILTER[@]}"; do
    add_suppression "org.freedesktop.login1" "org.freedesktop.login1.Manager:$method"
done

cat /etc/dfuzzer.conf

# TODO
#   * check for possibly newly introduced buses?
NAME_LIST=(
    home
    hostname
    import
    locale
    login
    machine
    portable
    resolve
    timedate
)

# Some services require specific conditions:
#   - systemd-oomd requires PSI
#   - systemd-timesyncd can't run in a container
#   - systemd-networkd can run in a container if it has CAP_NET_ADMIN capability
if tail -n +1 /proc/pressure/{cpu,io,memory}; then
    NAME_LIST+=( oom )
fi

if ! systemd-detect-virt --container; then
    NAME_LIST+=( timesync )
fi

if ip link add dummy-fuzz type dummy; then
    # if a dummy interface is created, then let's also setup it for resolved
    ip link set dummy-fuzz up
    ip address add 192.0.2.1/24 dev dummy-fuzz

    # When we can create a dummy interface, we definitely have CAP_NET_ADMIN
    NAME_LIST+=( network )

    # Create unit files for another dummy interface for networkd
    mkdir -p /run/systemd/network
    cat >/run/systemd/network/10-dummy-fuzz2.netdev <<EOF
[NetDev]
Kind=dummy
Name=dummy-fuzz2
EOF
    cat >/run/systemd/network/10-dummy-fuzz2.network <<EOF
[Match]
Name=dummy-fuzz2
[Network]
Address=192.0.2.2/24
EOF
fi

# 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

# Disable debugging logs from systemd-homed, systemd-nsresourced, and systemd-userdbd.
# Otherwise, journal is filled with the debugging logs by them.
systemctl service-log-level systemd-homed.service info
for service in systemd-nsresourced.service systemd-userdbd.service; do
    mkdir -p "/run/systemd/system/${service}.d"
    cat >"/run/systemd/system/${service}.d/10-disable-debug.conf" <<EOF
[Service]
Environment=SYSTEMD_LOG_LEVEL=info
EOF
    systemctl daemon-reload
    systemctl restart "$service"
done

test_systemd() {
    systemd-run "$@" --pipe --wait \
                -- dfuzzer -b "$PAYLOAD_MAX" -n org.freedesktop.systemd1

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

# Let's first test the session bus before the system one, as it may be in a
# spurious state after fuzzing the system bus or login bus.
echo "Bus: org.freedesktop.systemd1 (session)"
test_systemd --machine 'testuser@.host' --user

# 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

# Next, test the system service buses, as the services may be in a spurious
# state after fuzzing the system service manager bus.
for name in "${NAME_LIST[@]}"; do
    bus="org.freedesktop.${name}1"
    service="systemd-${name}d.service"

    echo "Bus: $bus"

    # Unmask and enable the service.
    systemctl unmask "$service"
    systemctl enable "$service"

    # enable debugging logs
    systemctl service-log-level "$service" debug || :

    systemd-run --pipe --wait \
                -- dfuzzer -b "$PAYLOAD_MAX" -n "$bus"

    # disable debugging logs
    systemctl service-log-level "$service" info || :
done

umount /var/lib/machines

# Finally, test the system bus.
echo "Bus: org.freedesktop.systemd1 (system)"
test_systemd

touch /testok
