diff --git a/images/instrumentation/preload/Makefile b/images/instrumentation/preload/Makefile new file mode 100644 index 00000000..0617ed5a --- /dev/null +++ b/images/instrumentation/preload/Makefile @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +ARCH := $(shell uname -m) +CFLAGS ?= -Wall -Werror -Wextra -O2 +LIB_LINKER_FLAGS ?= -nostdlib -rdynamic -shared -ldl + +SRC_DIR := src +OBJ_DIR := obj +LIB_DIR := lib +LIB_NAME := $(LIB_DIR)/libdash0envhook_$(ARCH).so +TEST_DIR := test +TESTBIN_DIR := testbin/$(ARCH) +SRC_EXT := c + +OS := $(shell uname -s) +SHELL := sh + +NAMES := $(notdir $(basename $(wildcard $(SRC_DIR)/*.$(SRC_EXT)))) +OBJECTS :=$(patsubst %,$(OBJ_DIR)/%.o,$(NAMES)) + +TEST_NAMES := $(notdir $(basename $(wildcard $(TEST_DIR)/*.$(SRC_EXT)))) +TEST_OBJECTS :=$(patsubst %,$(TESTBIN_DIR)/%.o,$(TEST_NAMES)) + +all: $(LIB_NAME) + +$(LIB_NAME): $(OBJECTS) + $(CC) $(CFLAGS) $(LIB_LINKER_FLAGS) $(OBJECTS) -o $(LIB_NAME) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.$(SRC_EXT) + $(CC) -c -fPIC $^ -o $@ $(DEBUG) $(CFLAGS) $(LIBS) + +clean: clean-test + @rm -f $(OBJECTS) $(LIB_NAME) + +clean-test: + @rm -f $(TEST_OBJECTS) + +build-test: $(TEST_OBJECTS) + +$(TESTBIN_DIR)/%.o: $(TEST_DIR)/%.$(SRC_EXT) + $(CC) $(CFLAGS) -o $@ $(DEBUG) $^ + +.PHONY: all clean install clean-test build-test diff --git a/images/instrumentation/preload/README.md b/images/instrumentation/preload/README.md new file mode 100644 index 00000000..99ff9d26 --- /dev/null +++ b/images/instrumentation/preload/README.md @@ -0,0 +1,5 @@ +Dash0 LD_PRELOAD Env Hook +========================= + +Overrides [getenv](https://man7.org/linux/man-pages/man3/getenv.3.html) to dynamically add environment variables to +executables after the fact. diff --git a/images/instrumentation/preload/docker/Dockerfile-build b/images/instrumentation/preload/docker/Dockerfile-build new file mode 100644 index 00000000..4221c59c --- /dev/null +++ b/images/instrumentation/preload/docker/Dockerfile-build @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +FROM ubuntu:24.10 + +RUN apt update && \ + apt-get install -y \ + build-essential + +WORKDIR /usr/src/dash0/preload/ + +CMD ["scripts/make-clean.sh"] diff --git a/images/instrumentation/preload/docker/Dockerfile-test-glibc b/images/instrumentation/preload/docker/Dockerfile-test-glibc new file mode 100644 index 00000000..3d728af3 --- /dev/null +++ b/images/instrumentation/preload/docker/Dockerfile-test-glibc @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +FROM ubuntu:24.10 + +RUN apt update && \ + apt-get install -y \ + build-essential + +WORKDIR /usr/src/dash0/preload/ + +CMD ["test/run-tests.sh"] diff --git a/images/instrumentation/preload/docker/Dockerfile-test-musl b/images/instrumentation/preload/docker/Dockerfile-test-musl new file mode 100644 index 00000000..a5befcb1 --- /dev/null +++ b/images/instrumentation/preload/docker/Dockerfile-test-musl @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +FROM alpine:3.20.1 + +RUN apk add --no-cache build-base + +WORKDIR /usr/src/dash0/preload/ + +CMD ["test/run-tests.sh"] diff --git a/images/instrumentation/preload/lib/.gitignore b/images/instrumentation/preload/lib/.gitignore new file mode 100644 index 00000000..140f8cf8 --- /dev/null +++ b/images/instrumentation/preload/lib/.gitignore @@ -0,0 +1 @@ +*.so diff --git a/images/instrumentation/preload/obj/.gitignore b/images/instrumentation/preload/obj/.gitignore new file mode 100644 index 00000000..5761abcf --- /dev/null +++ b/images/instrumentation/preload/obj/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/images/instrumentation/preload/scripts/build-in-container.sh b/images/instrumentation/preload/scripts/build-in-container.sh new file mode 100755 index 00000000..a795c149 --- /dev/null +++ b/images/instrumentation/preload/scripts/build-in-container.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +cd "$(dirname "$0")"/.. + +# TODO build multi platform image + +if [ -z "${ARCH:-}" ]; then + ARCH=arm64 +fi +if [ "$ARCH" = arm64 ]; then + docker_platform=linux/arm64 +elif [ "$ARCH" = x86_64 ]; then + docker_platform=linux/amd64 +else + echo "The architecture $ARCH is not supported." + exit 1 +fi + +dockerfile_name=docker/Dockerfile-build +image_name=dash0-env-hook-builder-$ARCH +container_name=$image_name + +docker_run_extra_arguments="" +if [ "${INTERACTIVE:-}" = "true" ]; then + docker_run_extra_arguments=/bin/bash +fi + +echo +echo +echo ">>> Building the library on $ARCH <<<" + +docker rm -f "$container_name" 2> /dev/null + +# Note: This is not the multi-platform image that we will need eventually. The combination of docker build and docker +# run here basically only builds the library binary for the given CPU architecture and places it in the lib folder. And +# since the lib folder is mounted, the binary is then available in the host file system for further testing (for +# example, via other container images using a specific CPU architecture). +docker build \ + --platform "$docker_platform" \ + . \ + -f "$dockerfile_name" \ + -t "$image_name" + +docker run \ + --platform "$docker_platform" \ + --name "$container_name" \ + -it \ + --volume "$(pwd):/usr/src/dash0/preload/" \ + "$image_name" \ + $docker_run_extra_arguments + diff --git a/images/instrumentation/preload/scripts/make-clean.sh b/images/instrumentation/preload/scripts/make-clean.sh new file mode 100755 index 00000000..5bd47a3d --- /dev/null +++ b/images/instrumentation/preload/scripts/make-clean.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +cd "$(dirname "$0")"/.. + +make clean +make + diff --git a/images/instrumentation/preload/scripts/run-tests-in-container.sh b/images/instrumentation/preload/scripts/run-tests-in-container.sh new file mode 100755 index 00000000..b8ffc215 --- /dev/null +++ b/images/instrumentation/preload/scripts/run-tests-in-container.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +cd "$(dirname "$0")"/.. + +if [ -z "${ARCH:-}" ]; then + ARCH=arm64 +fi +if [ "$ARCH" = arm64 ]; then + docker_platform=linux/arm64 + expected_cpu_architecture=aarch64 +elif [ "$ARCH" = x86_64 ]; then + docker_platform=linux/amd64 + expected_cpu_architecture=x86_64 +else + echo "The architecture $ARCH is not supported." + exit 1 +fi + +if [ -z "${LIBC:-}" ]; then + LIBC=glibc +fi + +dockerfile_name="docker/Dockerfile-test-$LIBC" +if [ ! -f "$dockerfile_name" ]; then + echo "The file \"$dockerfile_name\" does not exist, the libc flavor $LIBC is not supported." + exit 1 +fi + +image_name=dash0-env-hook-test-$ARCH-$LIBC +container_name=$image_name + +docker_run_extra_arguments="" +if [ "${INTERACTIVE:-}" = "true" ]; then + if [ "$LIBC" = glibc ]; then + docker_run_extra_arguments=/bin/bash + elif [ "$LIBC" = musl ]; then + docker_run_extra_arguments=/bin/sh + else + echo "The libc flavor $LIBC is not supported." + exit 1 + fi +fi + +echo +echo --------------------------------------- +echo "testing the library on $ARCH and $LIBC" +echo --------------------------------------- + +docker rm -f "$container_name" +docker build \ + --platform "$docker_platform" \ + . \ + -f "$dockerfile_name" \ + -t "$image_name" + +docker run \ + --platform "$docker_platform" \ + --env EXPECTED_CPU_ARCHITECTURE="$expected_cpu_architecture" \ + --name "$container_name" \ + -it \ + --volume "$(pwd):/usr/src/dash0/preload/" \ + "$image_name" \ + $docker_run_extra_arguments + diff --git a/images/instrumentation/preload/scripts/test-all.sh b/images/instrumentation/preload/scripts/test-all.sh new file mode 100755 index 00000000..80beed77 --- /dev/null +++ b/images/instrumentation/preload/scripts/test-all.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +cd "$(dirname "$0")"/.. + +ARCH=arm64 scripts/build-in-container.sh +ARCH=x86_64 scripts/build-in-container.sh + +exit_code=0 +summary="" + +run_tests_for_architecture_and_libc_flavor() { + arch=$1 + libc=$2 + set +e + ARCH=$arch LIBC=$libc scripts/run-tests-in-container.sh + test_exit_code=$? + set -e + echo + echo --------------------------------------- + if [ $test_exit_code != 0 ]; then + printf "${RED}tests for %s/%s failed (see above for details)${NC}\n" "$arch" "$libc" + exit_code=1 + summary="$summary\n$arch/$libc:\tfailed" + else + printf "${GREEN}tests for %s/%s were successful${NC}\n" "$arch" "$libc" + summary="$summary\n$arch/$libc:\tok" + fi + echo --------------------------------------- + echo +} + +run_tests_for_architecture_and_libc_flavor arm64 glibc +run_tests_for_architecture_and_libc_flavor x86_64 glibc +run_tests_for_architecture_and_libc_flavor arm64 musl +run_tests_for_architecture_and_libc_flavor x86_64 musl + +echo "$summary" +exit $exit_code + diff --git a/images/instrumentation/preload/src/libdash0envhook.c b/images/instrumentation/preload/src/libdash0envhook.c new file mode 100644 index 00000000..85ceab73 --- /dev/null +++ b/images/instrumentation/preload/src/libdash0envhook.c @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + +#include "map.h" + +typedef char* (*getenv_fun_ptr)(const char* name); + +typedef char* (*secure_getenv_fun_ptr)(const char* name); + +getenv_fun_ptr original_getenv; +secure_getenv_fun_ptr original_secure_getenv; + +int num_map_entries = 1; +Entry map[1]; + +char* default_node_options_value = "--require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js"; + +__attribute__((constructor)) static void setup(void) { + Entry node_options_entry = { .key = "NODE_OPTIONS", .value = NULL }; + map[0] = node_options_entry; +} + +char* _getenv(char* (*original_function)(const char* name), const char* name) +{ + if (strcmp(name, "NODE_OPTIONS") != 0) { + return original_function(name); + } + + char* cached = get_map_entry(map, num_map_entries, name); + if (cached != NULL) { + return cached; + } + + char* original_value = original_function(name); + if (original_value == NULL) { + return default_node_options_value; + } + + char* modified_value = malloc(strlen(default_node_options_value) + 1 + strlen(original_value) + 1); + strcpy(modified_value, default_node_options_value); + strcat(modified_value, " "); + strcat(modified_value, original_value); + + // Note: it is probably okay to not free the modified_value, as long as we only malloc a very limited number of char* + // instances, i.e. only one for every environment variable name we want to override. + put_map_entry(map, num_map_entries, name, modified_value); + return modified_value; +} + +char* getenv(const char* name) { + if (!original_getenv) { + original_getenv = (getenv_fun_ptr)dlsym(RTLD_NEXT, "getenv"); + } + return _getenv(original_getenv, name); +} + +char* secure_getenv(const char* name) { + if (!original_secure_getenv) { + original_secure_getenv = (secure_getenv_fun_ptr)dlsym(RTLD_NEXT, "secure_getenv"); + } + return _getenv(original_secure_getenv, name); +} + diff --git a/images/instrumentation/preload/src/map.c b/images/instrumentation/preload/src/map.c new file mode 100644 index 00000000..0fcb4e5a --- /dev/null +++ b/images/instrumentation/preload/src/map.c @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "map.h" + +Entry* find(Entry map[], int len, const char* key) { + for (int i = 0; i < len; i++) { + Entry* e = &map[i]; + if (strcmp(e->key, key) == 0) { + return e; + } + } + return NULL; +} + +char* get_map_entry(Entry map[], int len, const char* key) { + Entry* e = find(map, len, key); + if (e == NULL) { + return NULL; + } + return e->value; +} + +void put_map_entry(Entry* map, int len, const char* key, char* value) { + Entry* e = find(map, len, key); + if (e == NULL) { + return; + } + e->value = value; +} + diff --git a/images/instrumentation/preload/src/map.h b/images/instrumentation/preload/src/map.h new file mode 100755 index 00000000..95f013f5 --- /dev/null +++ b/images/instrumentation/preload/src/map.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +typedef struct Entry { + const char* key; + char* value; +} Entry; + +char* get_map_entry(Entry map[], int len, const char* key); + +void put_map_entry(Entry* map, int len, const char* key, char* value); + diff --git a/images/instrumentation/preload/test/appundertest.c b/images/instrumentation/preload/test/appundertest.c new file mode 100644 index 00000000..576c1507 --- /dev/null +++ b/images/instrumentation/preload/test/appundertest.c @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +// SPDX-License-Identifier: Apache-2.0 + +#define _GNU_SOURCE + +#include +#include +#include + +void echo_env_var(const char* name) { + fputs(name, stdout); + fputs(": ", stdout); + + char* value = getenv(name); + if (value != NULL) { + fputs(value, stdout); + } else { + fputs("NULL", stdout); + } +} + +void echo_env_var_secure(const char* name) { + fputs(name, stdout); + fputs(": ", stdout); + + char* value = secure_getenv(name); + if (value != NULL) { + fputs(value, stdout); + } else { + fputs("NULL", stdout); + } +} + +int main(int argc, char* argv[]) { + if (argc < 2) { + fputs("error: not enough arguments, the name of the test case needs to be specifed\n", stdout); + exit(1); + } + char* test_case = argv[1]; + if (strcmp(test_case, "non-existing") == 0) { + echo_env_var("DOES_NOT_EXIST"); + } else if (strcmp(test_case, "term") == 0) { + echo_env_var("TERM"); + } else if (strcmp(test_case, "node_options") == 0) { + echo_env_var("NODE_OPTIONS"); + } else if (strcmp(test_case, "node_options_twice") == 0) { + echo_env_var("NODE_OPTIONS"); + fputs("; ", stdout); + echo_env_var("NODE_OPTIONS"); + } else if (strcmp(test_case, "term-gnu-secure") == 0) { + echo_env_var_secure("TERM"); + } else if (strcmp(test_case, "node_options-gnu-secure") == 0) { + echo_env_var_secure("NODE_OPTIONS"); + } else if (strcmp(test_case, "node_options_twice-gnu-secure") == 0) { + echo_env_var_secure("NODE_OPTIONS"); + fputs("; ", stdout); + echo_env_var_secure("NODE_OPTIONS"); + } else { + fputs("unknown test case: ", stdout); + fputs(test_case, stdout); + fputs("\n", stdout); + exit(1); + } +} + diff --git a/images/instrumentation/preload/test/run-tests.sh b/images/instrumentation/preload/test/run-tests.sh new file mode 100755 index 00000000..e575bb82 --- /dev/null +++ b/images/instrumentation/preload/test/run-tests.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +relative_directory="$(dirname "$0")"/.. +directory="$(realpath "$relative_directory")" +cd "$directory" + +if [ -z "${EXPECTED_CPU_ARCHITECTURE:-}" ]; then + echo "EXPECTED_CPU_ARCHITECTURE is not set for $0." + exit 1 +fi + +arch=$(uname -m) +arch_exit_code=$? +if [ $arch_exit_code != 0 ]; then + printf "${RED}verifying CPU architecture failed:${NC}\n" + echo "exit code: $arch_exit_code" + echo "output: $arch" + exit 1 +elif [ "$arch" != "$EXPECTED_CPU_ARCHITECTURE" ]; then + printf "${RED}verifying CPU architecture failed:${NC}\n" + echo "expected: $EXPECTED_CPU_ARCHITECTURE" + echo "actual: $arch" + exit 1 +else + printf "${GREEN}verifying CPU architecture %s successful${NC}\n" "$EXPECTED_CPU_ARCHITECTURE" +fi + +preload_lib=$directory/lib/libdash0envhook_$arch.so +if [ ! -f $preload_lib ]; then + printf "${RED}error: $preload_lib does not exist, not running any tests.${NC}\n" + exit 1 +fi + +appundertest=testbin/"${arch}"/appundertest.o +echo appundertest: $appundertest + +run_test_case() { + test_case=$1 + command=$2 + expected=$3 + existing_node_options_value=${4:-} + set +e + if [ "$existing_node_options_value" != "" ]; then + test_output=$(LD_PRELOAD="$preload_lib" NODE_OPTIONS="$existing_node_options_value" "$appundertest" "$command") + else + test_output=$(LD_PRELOAD="$preload_lib" "$appundertest" "$command") + fi + test_exit_code=$? + set -e + if [ $test_exit_code != 0 ]; then + printf "${RED}test \"%s\" crashed:${NC}\n" "$test_case" + echo "received exit code: $test_exit_code" + echo "output: $test_output" + exit_code=1 + elif [ "$test_output" != "$expected" ]; then + printf "${RED}test \"%s\" failed:${NC}\n" "$test_case" + echo "expected: $expected" + echo "actual: $test_output" + exit_code=1 + else + printf "${GREEN}test \"%s\" successful${NC}\n" "$test_case" + fi +} + +# We always need to clean out the old appundertest.o, it might have been built for a different libc flavor. +make clean-test +make build-test + +exit_code=0 + +run_test_case "getenv: returns NULL for non-existing environment variable" non-existing "DOES_NOT_EXIST: NULL" +run_test_case "getenv: returns environment variable unchanged" term "TERM: xterm" +run_test_case "getenv: overrides NODE_OPTIONS if it is not present" node_options "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js" +run_test_case "getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js; NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js" +run_test_case "getenv: appends to NODE_OPTIONS if it is present" node_options "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options" "--existing-node-options" +run_test_case "getenv: ask for NODE_OPTIONS (set) twice" node_options_twice "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options; NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options" "--existing-node-options" + +run_test_case "secure_getenv: returns NULL for non-existing environment variable" non-existing "DOES_NOT_EXIST: NULL" +run_test_case "secure_getenv: returns environment variable unchanged" term-gnu-secure "TERM: xterm" +run_test_case "secure_getenv: overrides NODE_OPTIONS if it is not present" node_options-gnu-secure "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js" +run_test_case "secure_getenv: ask for NODE_OPTIONS (unset) twice" node_options_twice-gnu-secure "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js; NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js" +run_test_case "secure_getenv: appends to NODE_OPTIONS if it is present" node_options-gnu-secure "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options" "--existing-node-options" +run_test_case "secure_getenv: ask for NODE_OPTIONS (set) twice" node_options_twice-gnu-secure "NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options; NODE_OPTIONS: --require /opt/dash0/instrumentation/node.js/node_modules/@dash0/opentelemetry/src/index.js --existing-node-options" "--existing-node-options" + +exit $exit_code + diff --git a/images/instrumentation/preload/testbin/aarch64/.gitignore b/images/instrumentation/preload/testbin/aarch64/.gitignore new file mode 100644 index 00000000..5761abcf --- /dev/null +++ b/images/instrumentation/preload/testbin/aarch64/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/images/instrumentation/preload/testbin/x86_64/.gitignore b/images/instrumentation/preload/testbin/x86_64/.gitignore new file mode 100644 index 00000000..5761abcf --- /dev/null +++ b/images/instrumentation/preload/testbin/x86_64/.gitignore @@ -0,0 +1 @@ +*.o