Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(preload): experimental LD_PRELOAD env hook #28

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
44 changes: 44 additions & 0 deletions images/dash0-instrumentation/preload/Makefile
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions images/dash0-instrumentation/preload/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions images/dash0-instrumentation/preload/docker/Dockerfile-build
Original file line number Diff line number Diff line change
@@ -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"]
12 changes: 12 additions & 0 deletions images/dash0-instrumentation/preload/docker/Dockerfile-test-glibc
Original file line number Diff line number Diff line change
@@ -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"]
10 changes: 10 additions & 0 deletions images/dash0-instrumentation/preload/docker/Dockerfile-test-musl
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions images/dash0-instrumentation/preload/lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.so
1 change: 1 addition & 0 deletions images/dash0-instrumentation/preload/obj/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.o
56 changes: 56 additions & 0 deletions images/dash0-instrumentation/preload/scripts/build-in-container.sh
Original file line number Diff line number Diff line change
@@ -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

12 changes: 12 additions & 0 deletions images/dash0-instrumentation/preload/scripts/make-clean.sh
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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

48 changes: 48 additions & 0 deletions images/dash0-instrumentation/preload/scripts/test-all.sh
Original file line number Diff line number Diff line change
@@ -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

70 changes: 70 additions & 0 deletions images/dash0-instrumentation/preload/src/libdash0envhook.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

#include <dlfcn.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#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);
}

33 changes: 33 additions & 0 deletions images/dash0-instrumentation/preload/src/map.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

#include <string.h>

#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;
}

12 changes: 12 additions & 0 deletions images/dash0-instrumentation/preload/src/map.h
Original file line number Diff line number Diff line change
@@ -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);

Loading