diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..104a58b --- /dev/null +++ b/Containerfile @@ -0,0 +1,62 @@ +FROM registry.fedoraproject.org/fedora:43 as builder + +ARG RUST_VERSION=stable + +RUN dnf install -y \ + # for stub \ + nasm \ + # for script jit \ + llvm-devel \ + zlib-devel \ + libxml2-devel \ + libstdc++-static \ + # for bpf \ + clang \ + kernel-devel \ + libbpf-devel && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --default-toolchain $RUST_VERSION --profile minimal + +# $HOME is not evaluated here, any better way of getting the toolchain +# directory? +ENV PATH="${PATH}:/root/.cargo/bin" + +RUN rustup component add rustfmt clippy + +RUN if [ "${RUST_VERSION}" == "nightly" ]; then \ + rustup component add rust-src --toolchain nightly; \ + fi + +ADD ./ /berserker/ + +WORKDIR /berserker/ + +RUN nasm -f elf64 -o stub.o stub.asm && ld -o stub stub.o + +RUN cargo fmt --check + +RUN cargo clippy -- -D warnings + +RUN cargo build -r + +# Test will require stub binary to be available +ENV PATH="${PATH}:/berserker:/berserker/target/release" + +RUN if [ "${RUST_VERSION}" == "nightly" ]; then \ + TARGET=$(rustc --version --verbose | grep host | cut -d" " -f2) && \ + RUSTFLAGS="-Z sanitizer=address" cargo +nightly test -Z build-std --target "$TARGET"; \ + else \ + cargo test; \ + fi + +FROM registry.fedoraproject.org/fedora:43 + +RUN mkdir /etc/berserker + +COPY --from=builder /berserker/target/release/berserker /usr/local/bin/berserker +COPY --from=builder /berserker/workload.toml /etc/berserker/workload.toml +COPY --from=builder /berserker/stub /usr/local/bin/stub + +ENV PATH="${PATH}:/usr/local/bin" + +ENTRYPOINT berserker diff --git a/Containerfile.test b/Containerfile.test new file mode 100644 index 0000000..0e54a16 --- /dev/null +++ b/Containerfile.test @@ -0,0 +1,14 @@ +FROM berserker:latest + +RUN dnf install -y \ + which \ + bpftrace \ + bpftool \ + procps-ng + +# Test will require stub binary to be available +ENV PATH="${PATH}:/berserker:/usr/local/bin" + +ADD ./tests /tests/ + +ENTRYPOINT ["/tests/entrypoint.sh"] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b5cd26c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM builder:latest as builder - -FROM registry.fedoraproject.org/fedora:43 - -RUN mkdir /etc/berserker - -COPY --from=builder /berserker/target/release/berserker /usr/local/bin/berserker -COPY --from=builder /berserker/workload.toml /etc/berserker/workload.toml -COPY --from=builder /berserker/stub /usr/local/bin/stub - -ENV PATH="${PATH}:/usr/local/bin" - -ENTRYPOINT berserker diff --git a/Dockerfile.build b/Dockerfile.build deleted file mode 100644 index 284d458..0000000 --- a/Dockerfile.build +++ /dev/null @@ -1,35 +0,0 @@ -FROM registry.fedoraproject.org/fedora:43 - -RUN dnf install -y \ - rust \ - cargo \ - clippy \ - rustfmt \ - # for stub \ - nasm \ - # for script jit \ - llvm-devel \ - zlib-devel \ - libxml2-devel \ - libstdc++-static \ - # for bpf \ - clang \ - kernel-devel \ - libbpf-devel - -ADD ./ /berserker/ - -WORKDIR /berserker/ - -RUN nasm -f elf64 -o stub.o stub.asm && ld -o stub stub.o - -RUN cargo fmt --check - -RUN cargo clippy -- -D warnings - -RUN cargo build -r - -# Test will require stub binary to be available -ENV PATH="${PATH}:/berserker" - -RUN cargo test diff --git a/Makefile b/Makefile index 4171400..8f4408f 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,12 @@ ifeq ($(BERSERKER_TAG),) BERSERKER_TAG=$(shell git describe --tags --abbrev=10 --dirty) endif - .PHONY: all all: - docker build -t builder -f Dockerfile.build . - docker build -t berserker . + docker build -t berserker -f Containerfile . + docker build -t berserker -f Containerfile --build-arg=RUST_VERSION=nightly . + docker build -t berserker-test -f Containerfile.test . + docker run --privileged berserker-test .PHONY: build-network build-berserker-network: diff --git a/src/lib.rs b/src/lib.rs index 69d4808..d5e44b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(sanitize)] + use core_affinity::CoreId; use serde::{Deserialize, Deserializer}; use std::{collections::HashMap, fmt::Display, net::Ipv4Addr, str::FromStr}; @@ -28,7 +30,8 @@ pub struct WorkloadConfig { /// Custom workload configuration. pub workload: Workload, - /// For how long to run the worker. Default value is zero, meaning no limit. + /// For how long to run the worker. Default value is zero, meaning no + /// limit. #[serde(default = "default_duration")] pub duration: u64, } @@ -194,8 +197,8 @@ pub enum Workload { /// Maximum number of dynamic connections connections_dyn_max: u32, - // How many connections to make to the same server address and port with - // different client ports + /// How many connections to make to the same server address and port + /// with different client ports #[serde(default = "default_conns_per_addr")] conns_per_addr: u16, @@ -211,7 +214,8 @@ pub enum Workload { /// Whether or not to wait for a connection to be removed before adding /// a new one, when the dynamic connection limit is reached. /// if true: an old connection will be forcibly removed - /// if false: wait for a connection to naturally age-off before adding a new one + /// if false: wait for a connection to naturally age-off before adding + /// a new one preempt: bool, }, diff --git a/src/main.rs b/src/main.rs index b9c731c..a4ccd9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,7 +204,8 @@ fn main() { .required(false), ) // Add in settings from the environment (with a prefix of APP) - // Eg.. `BERSERKER__WORKLOAD__ARRIVAL_RATE=1` would set the `arrival_rate` key + // Eg.. `BERSERKER__WORKLOAD__ARRIVAL_RATE=1` would set the + // `arrival_rate` key .add_source( config::Environment::with_prefix("BERSERKER") .try_parsing(true) @@ -230,7 +231,8 @@ fn main() { let elapsed = duration_timer.elapsed().unwrap().as_secs(); // Ignore processes without specified duration -- we don't want - // neither terminate them, nor count against processes to compare. + // neither terminate them, nor count against processes to + // compare. let watched_processes = processes .iter() .filter(|(_, duration)| *duration > 0) diff --git a/src/worker/script.rs b/src/worker/script.rs index bcd1ac8..3998e2b 100644 --- a/src/worker/script.rs +++ b/src/worker/script.rs @@ -210,6 +210,7 @@ pub static RUNTIME: LazyLock> = }); impl ScriptWorker { + #[sanitize(address = "off")] fn jit_instruction(name: String, arg: Arg, ctx: &BuildContext) { let mut arg_ptr = Self::get_arg_value(arg, ctx); @@ -320,7 +321,8 @@ impl ScriptWorker { &mut err, ) != 0 { - // In case of error, we must avoid using the uninitialized ExecutionEngineRef. + // In case of error, we must avoid using the uninitialized + // ExecutionEngineRef. assert!(!err.is_null()); panic!( "Failed to create execution engine: {:?}", @@ -385,8 +387,8 @@ impl ScriptWorker { function_type, ); - // Create a basic block in the function and set our builder to generate - // code in it. + // Create a basic block in the function and set our builder to + // generate code in it. let bb = LLVMAppendBasicBlockInContext( context, function, diff --git a/src/worker/syscalls/ioctl.rs b/src/worker/syscalls/ioctl.rs index fcb0fc8..650e828 100644 --- a/src/worker/syscalls/ioctl.rs +++ b/src/worker/syscalls/ioctl.rs @@ -14,8 +14,8 @@ pub struct IoctlCall { impl IoctlCall { pub fn new(_: &ArgsMap) -> Self { - // Zero initialize all fields, fd will be initialized in `Syscaller::init`. - // All other fields can be overridden as needed + // Zero initialize all fields, fd will be initialized in + // `Syscaller::init`. All other fields can be overridden as needed Default::default() } } diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh new file mode 100755 index 0000000..aeb3289 --- /dev/null +++ b/tests/entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +mount -t debugfs none /sys/kernel/debug + +./tests/workers/script/unit/smoke.sh diff --git a/tests/workers/script/dist/smoke.sh b/tests/workers/script/dist/smoke.sh new file mode 100755 index 0000000..eb927d6 --- /dev/null +++ b/tests/workers/script/dist/smoke.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -eou pipefail + +stop() { echo "$*" 1>&2 ; exit 1; } + +which bpftrace &>/dev/null || stop "Don't have bpftrace" +which bpftool &>/dev/null || stop "Don't have bpftool" +which berserker &>/dev/null || stop "Don't have berserker" +which stub &>/dev/null || stop "Don't have random process tool" +which pkill &>/dev/null || stop "Don't have pkill" + +if [ ! -d "tests/workers/script/unit" ]; then + echo "Can't find test directory. Smoke tests have to be run from the project root directory" +fi + +echo "Cleanup..." +rm -f /tmp/berserker.log +rm -f /tmp/events.log + +# in case if it's still running from a previous run +pkill berserker || true + +# make berserkers verbose +export RUST_LOG=trace + +echo "Starting bpftrace..." +bpftrace tests/workers/script/unit/syscalls.bt &> /tmp/events.log & + +# let bpftrace attach probes +attempts=0 + +while ! bpftool prog | grep -q bpftrace ; +do + if [[ "$attempts" -gt 40 ]]; then + echo "Can't find bpftool after ${attempts} attempts." + cat /tmp/events.log + exit 1 + fi; + + attempts=$((attempts+1)) + echo "Wait for bpftrace"; + sleep 0.5; +done + +echo "Starting berserker..." +berserker -f tests/workers/script/dist/workload.ber &> /tmp/berserker.log & + +# let berserker do some work +sleep 5; + +echo "Stopping..." +pkill berserker || true +pkill bpftrace || true + +echo "Verifying the results..." +if ! grep -q -E 'exec .*/stub' /tmp/events.log; then + echo "FAIL: no task instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'openat /tmp/tests/test' /tmp/events.log; then + echo "FAIL: no open instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -E 'openat /tmp/tests/.*' /tmp/events.log | grep -q -v '/tmp/tests/test'; then + echo "FAIL: no open random path instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'connect .*' /tmp/events.log; then + echo "FAIL: no ping instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'sendto .*' /tmp/events.log; then + echo "FAIL: ping instruction did not work" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +echo "PASS" + +rm -f /tmp/berserker.log +rm -f /tmp/events.log + +exit 0; diff --git a/tests/workers/script/dist/syscalls.bt b/tests/workers/script/dist/syscalls.bt new file mode 100644 index 0000000..c47fe22 --- /dev/null +++ b/tests/workers/script/dist/syscalls.bt @@ -0,0 +1,20 @@ +t:syscalls:sys_enter_execve /comm == "berserker"/ +{ + printf("exec %s\n", str(args->filename)); +} + +t:syscalls:sys_enter_openat /comm == "berserker"/ { + printf("openat %s\n", str(args->filename)); +} + +t:syscalls:sys_enter_connect /comm == "berserker"/ { + $addr_in = (struct sockaddr_in *)args->uservaddr; + $addr = ntop($addr_in->sin_addr.s_addr); + printf("connect %s\n", $addr); +} + +t:syscalls:sys_enter_sendto /comm == "berserker"/ { + $addr_in = (struct sockaddr_in *)args->addr; + $addr = ntop($addr_in->sin_addr.s_addr); + printf("sendto %s\n", $addr); +} diff --git a/tests/workers/script/dist/workload.ber b/tests/workers/script/dist/workload.ber new file mode 100644 index 0000000..1338a6c --- /dev/null +++ b/tests/workers/script/dist/workload.ber @@ -0,0 +1,20 @@ +machine { + server(8080); + path("/tmp/tests"); +} + +main (workers = 1) { + debug("run task stub"); + task(stub); + + debug("open file /tmp/tests/test"); + open("/tmp/tests/test"); + + debug("open random file under /tmp/tests/"); + open(random_path("/tmp/tests")); + + debug("ping server"); + ping("127.0.0.1:8080"); +} : exp { + rate = 10.0; +} diff --git a/tests/workers/script/unit/smoke.sh b/tests/workers/script/unit/smoke.sh new file mode 100755 index 0000000..8509640 --- /dev/null +++ b/tests/workers/script/unit/smoke.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -eou pipefail + +stop() { echo "$*" 1>&2 ; exit 1; } + +which bpftrace &>/dev/null || stop "Don't have bpftrace" +which bpftool &>/dev/null || stop "Don't have bpftool" +which berserker &>/dev/null || stop "Don't have berserker" +which stub &>/dev/null || stop "Don't have random process tool" +which pkill &>/dev/null || stop "Don't have pkill" + +if [ ! -d "tests/workers/script/unit" ]; then + echo "Can't find test directory. Smoke tests have to be run from the project root directory" +fi + +echo "Cleanup..." +rm -f /tmp/berserker.log +rm -f /tmp/events.log + +# in case if it's still running from a previous run +pkill berserker || true + +# make berserkers verbose +export RUST_LOG=trace + +echo "Starting bpftrace..." +bpftrace tests/workers/script/unit/syscalls.bt &> /tmp/events.log & + +# let bpftrace attach probes +attempts=0 + +while ! bpftool prog | grep -q bpftrace ; +do + if [[ "$attempts" -gt 40 ]]; then + echo "Can't find bpftool after ${attempts} attempts." + cat /tmp/events.log + bpftool prog + bpftool prog | grep -q bpftrace + exit 1 + fi; + + attempts=$((attempts+1)) + echo "Wait for bpftrace"; + sleep 0.5; +done + +echo "Starting berserker..." +berserker -f tests/workers/script/unit/workload.ber &> /tmp/berserker.log + +echo "Stopping..." +pkill berserker || true +pkill bpftrace || true + +echo "Verifying the results..." +if ! grep -q -E 'exec .*/stub' /tmp/events.log; then + echo "FAIL: no task instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'openat /tmp/tests/test' /tmp/events.log; then + echo "FAIL: no open instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -E 'openat /tmp/tests/.*' /tmp/events.log | grep -q -v '/tmp/tests/test'; then + echo "FAIL: no open random path instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'connect .*' /tmp/events.log; then + echo "FAIL: no ping instruction" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +if ! grep -q -E 'sendto .*' /tmp/events.log; then + echo "FAIL: ping instruction did not work" + cat /tmp/berserker.log + cat /tmp/events.log + exit 1; +fi + +echo "PASS" + +rm -f /tmp/berserker.log +rm -f /tmp/events.log + +exit 0; diff --git a/tests/workers/script/unit/syscalls.bt b/tests/workers/script/unit/syscalls.bt new file mode 100644 index 0000000..c47fe22 --- /dev/null +++ b/tests/workers/script/unit/syscalls.bt @@ -0,0 +1,20 @@ +t:syscalls:sys_enter_execve /comm == "berserker"/ +{ + printf("exec %s\n", str(args->filename)); +} + +t:syscalls:sys_enter_openat /comm == "berserker"/ { + printf("openat %s\n", str(args->filename)); +} + +t:syscalls:sys_enter_connect /comm == "berserker"/ { + $addr_in = (struct sockaddr_in *)args->uservaddr; + $addr = ntop($addr_in->sin_addr.s_addr); + printf("connect %s\n", $addr); +} + +t:syscalls:sys_enter_sendto /comm == "berserker"/ { + $addr_in = (struct sockaddr_in *)args->addr; + $addr = ntop($addr_in->sin_addr.s_addr); + printf("sendto %s\n", $addr); +} diff --git a/tests/workers/script/unit/workload.ber b/tests/workers/script/unit/workload.ber new file mode 100644 index 0000000..09b6c4c --- /dev/null +++ b/tests/workers/script/unit/workload.ber @@ -0,0 +1,18 @@ +machine { + server(8080); + path("/tmp/tests"); +} + +main (workers = 1) { + debug("run task stub"); + task(stub); + + debug("open file /tmp/tests/test"); + open("/tmp/tests/test"); + + debug("open random file under /tmp/tests/"); + open(random_path("/tmp/tests")); + + debug("ping server"); + ping("127.0.0.1:8080"); +}