From: Kris Van Hees <kris.van.hees@oracle.com>
To: dtrace@lists.linux.dev, dtrace-devel@oss.oracle.com
Subject: [PATCH 17/20] uprobe: add support for uprobe systemwide probes
Date: Fri, 30 Oct 2026 19:36:59 +0000 [thread overview]
Message-ID: <3a4910515ffe2cc8b3f3f7bdefda3444@oracle.com> (raw)
Signed-off-by: Kris Van Hees <kris.van.hees@oracle.com>
---
libdtrace/dt_prov_uprobe.c | 170 ++++++++++-
test/triggers/Build | 9 +-
test/triggers/uprobe-wildcards.c | 122 ++++++++
.../providers/uprobe/tst.dlib-path.sh | 70 +++++
test/unittest/providers/uprobe/tst.dlopen.sh | 135 +++++++++
.../providers/uprobe/tst.elf-offsets.sh | 278 ++++++++++++++++++
.../providers/uprobe/tst.lifecycle.sh | 134 +++++++++
test/unittest/providers/uprobe/tst.multicpu.r | 4 +
.../providers/uprobe/tst.multicpu.r.p | 8 +
.../unittest/providers/uprobe/tst.multicpu.sh | 138 +++++++++
test/unittest/providers/uprobe/tst.multicpu.x | 9 +
test/unittest/providers/uprobe/tst.multipid.r | 20 ++
.../providers/uprobe/tst.multipid.r.p | 4 +
.../unittest/providers/uprobe/tst.multipid.sh | 117 ++++++++
.../providers/uprobe/tst.multithread.sh | 160 ++++++++++
.../providers/uprobe/tst.probespec.sh | 68 +++++
.../providers/uprobe/tst.search-path.sh | 91 ++++++
test/unittest/providers/uprobe/tst.shlib.sh | 81 +++++
test/unittest/providers/uprobe/tst.symbols.sh | 52 ++++
.../providers/uprobe/tst.systemwide.sh | 158 ++++++++++
20 files changed, 1826 insertions(+), 2 deletions(-)
create mode 100644 test/triggers/uprobe-wildcards.c
create mode 100755 test/unittest/providers/uprobe/tst.dlib-path.sh
create mode 100755 test/unittest/providers/uprobe/tst.dlopen.sh
create mode 100755 test/unittest/providers/uprobe/tst.elf-offsets.sh
create mode 100755 test/unittest/providers/uprobe/tst.lifecycle.sh
create mode 100644 test/unittest/providers/uprobe/tst.multicpu.r
create mode 100755 test/unittest/providers/uprobe/tst.multicpu.r.p
create mode 100755 test/unittest/providers/uprobe/tst.multicpu.sh
create mode 100755 test/unittest/providers/uprobe/tst.multicpu.x
create mode 100644 test/unittest/providers/uprobe/tst.multipid.r
create mode 100755 test/unittest/providers/uprobe/tst.multipid.r.p
create mode 100755 test/unittest/providers/uprobe/tst.multipid.sh
create mode 100755 test/unittest/providers/uprobe/tst.multithread.sh
create mode 100755 test/unittest/providers/uprobe/tst.probespec.sh
create mode 100755 test/unittest/providers/uprobe/tst.search-path.sh
create mode 100755 test/unittest/providers/uprobe/tst.shlib.sh
create mode 100755 test/unittest/providers/uprobe/tst.symbols.sh
create mode 100755 test/unittest/providers/uprobe/tst.systemwide.sh
diff --git a/libdtrace/dt_prov_uprobe.c b/libdtrace/dt_prov_uprobe.c
index 1d2b9755..97276fe0 100644
--- a/libdtrace/dt_prov_uprobe.c
+++ b/libdtrace/dt_prov_uprobe.c
@@ -37,6 +37,7 @@
#include "dt_dctx.h"
#include "dt_cg.h"
#include "dt_list.h"
+#include "dt_module.h"
#include "dt_provider_tp.h"
#include "dt_probe.h"
#include "dt_program.h"
@@ -401,6 +402,161 @@ static int populate_usdt(dtrace_hdl_t *dtp)
return 0;
}
+/* Create a concrete probe, retaining existing probes across provide calls. */
+static int provide_uprobe(dtrace_hdl_t *dtp, dt_provider_t *prv,
+ dt_module_t *dmp, const char *fun, uint64_t off,
+ int flags)
+{
+ dtrace_probedesc_t pd = {
+ .prv = prv->desc.dtvd_name,
+ .mod = dmp->dm_name,
+ .fun = fun,
+ .prb = flags & PP_IS_RETURN ? "return" : "entry",
+ };
+ dt_uprobe_t *upp;
+
+ if (dt_probe_lookup(dtp, &pd) != NULL)
+ return 0;
+
+ upp = dt_zalloc(dtp, sizeof(dt_uprobe_t));
+ if (upp == NULL)
+ return 0;
+
+ upp->fn = strdup(dmp->dm_file);
+ if (upp->fn == NULL) {
+ dt_free(dtp, upp);
+ return 0;
+ }
+ upp->off = off;
+ upp->fd = -1;
+ upp->pid = -1;
+ upp->flags = flags;
+
+ if (dt_probe_insert(dtp, prv, pd.prv, pd.mod, pd.fun, pd.prb,
+ upp) != NULL)
+ return 1;
+
+ dt_free(dtp, upp->fn);
+ dt_free(dtp, upp);
+ return 0;
+}
+
+/* Probe kinds selected by provide(), independent of per-probe PP_IS_* flags. */
+#define PROVIDE_ENTRY 0x1
+#define PROVIDE_RETURN 0x2
+
+/* Match defined functions in both regular and dynamic ELF symbol tables. */
+static int provide_module(dtrace_hdl_t *dtp, dt_provider_t *prv,
+ dt_module_t *dmp, const dtrace_probedesc_t *pdp,
+ int flags)
+{
+ dtrace_objinfo_t info;
+ Elf_Scn *scn = NULL;
+ int n = 0;
+
+ if ((dmp->dm_flags & DT_DM_KERNEL) || dmp->dm_file[0] == '\0' ||
+ dtrace_object_info(dtp, dmp->dm_name, &info) == -1 ||
+ dmp->dm_elf == NULL)
+ return 0;
+
+ while ((scn = elf_nextscn(dmp->dm_elf, scn)) != NULL) {
+ GElf_Shdr sh;
+ Elf_Data *data;
+ size_t i;
+
+ if (gelf_getshdr(scn, &sh) == NULL ||
+ (sh.sh_type != SHT_SYMTAB && sh.sh_type != SHT_DYNSYM) ||
+ sh.sh_entsize == 0 || (data = elf_getdata(scn, NULL)) == NULL)
+ continue;
+
+ for (i = 0; i < sh.sh_size / sh.sh_entsize; i++) {
+ GElf_Sym sym;
+ GElf_Shdr fsh;
+ Elf_Scn *fscn;
+ const char *fun;
+ uint64_t off;
+
+ if (gelf_getsym(data, i, &sym) == NULL ||
+ GELF_ST_TYPE(sym.st_info) != STT_FUNC ||
+ sym.st_shndx == SHN_UNDEF ||
+ sym.st_shndx >= SHN_LORESERVE)
+ continue;
+
+ fun = elf_strptr(dmp->dm_elf, sh.sh_link, sym.st_name);
+ if (fun == NULL || *fun == '\0' ||
+ !dt_gmatch(fun, pdp->fun))
+ continue;
+
+ /* perf uprobes take file offsets, not virtual addresses. */
+ fscn = elf_getscn(dmp->dm_elf, sym.st_shndx);
+ if (fscn == NULL || gelf_getshdr(fscn, &fsh) == NULL ||
+ !(fsh.sh_flags & SHF_EXECINSTR) ||
+ fsh.sh_type == SHT_NOBITS || sym.st_value < fsh.sh_addr ||
+ sym.st_value - fsh.sh_addr >= fsh.sh_size)
+ continue;
+ off = sym.st_value - fsh.sh_addr + fsh.sh_offset;
+
+ if (flags & PROVIDE_ENTRY)
+ n += provide_uprobe(dtp, prv, dmp, fun, off, 0);
+ if (flags & PROVIDE_RETURN)
+ n += provide_uprobe(dtp, prv, dmp, fun, off,
+ PP_IS_RETURN);
+ }
+ }
+
+ return n;
+}
+
+typedef struct provide_module_arg {
+ dt_provider_t *prv;
+ const dtrace_probedesc_t *pdp;
+ int flags;
+ int n;
+} provide_module_arg_t;
+
+static int provide_module_cb(dtrace_hdl_t *dtp, dt_module_t *dmp, void *arg)
+{
+ provide_module_arg_t *data = arg;
+
+ data->n += provide_module(dtp, data->prv, dmp, data->pdp, data->flags);
+ return 0;
+}
+
+static int provide(dtrace_hdl_t *dtp, const dtrace_probedesc_t *pdp)
+{
+ dt_provider_t *prv = dt_provider_lookup(dtp, pdp->prv);
+ dt_module_t *dmp;
+ provide_module_arg_t arg = { .prv = prv, .pdp = pdp };
+
+ if (prv == NULL)
+ return 0;
+
+ /* Disallow uprobe probe specifications with empty module component. */
+ if (pdp->mod && pdp->mod[0] == 0)
+ return 0;
+
+ if (dt_gmatch("entry", pdp->prb))
+ arg.flags |= PROVIDE_ENTRY;
+ if (dt_gmatch("return", pdp->prb))
+ arg.flags |= PROVIDE_RETURN;
+ if (arg.flags == 0)
+ return 0;
+
+ if (pdp->mod[0] != '\0' && !strisglob(pdp->mod)) {
+ dmp = dt_module_lookup_by_name(dtp, pdp->mod);
+ if (dmp == NULL)
+ dmp = dt_module_create_user(dtp, pdp->mod);
+ if (dmp == NULL)
+ return 0;
+
+ return provide_module(dtp, prv, dmp, pdp, arg.flags);
+ }
+
+ dt_module_user_iter(dtp, pdp->mod, provide_module_cb, &arg);
+
+ return arg.n;
+}
+
static void free_probe_list(dtrace_hdl_t *dtp, list_probe_t *elem)
{
while (elem != NULL) {
@@ -1390,6 +1546,16 @@ static int trampoline(dt_pcb_t *pcb, uint_t exitlbl)
*/
dt_cg_tramp_copy_regs(pcb);
+ if (!dt_list_empty(&uprp->stmts)) {
+ /* Populate probe arguments. */
+ if (upp->flags & PP_IS_RETURN)
+ dt_cg_tramp_copy_rval_from_regs(pcb);
+ else
+ dt_cg_tramp_copy_args_from_regs(pcb, 1);
+
+ dt_cg_tramp_call_clauses(pcb, uprp, DT_ACTIVITY_ACTIVE);
+ }
+
/*
* pid probes.
*
@@ -1542,7 +1708,8 @@ static int uprobe_create(dtrace_hdl_t *dtp, const dt_uprobe_t *upp,
attr.uprobe_path = (uint64_t)upp->fn;
attr.probe_offset = upp->off;
- return dt_perf_event_open(&attr, upp->pid, -1, -1, 0);
+ return dt_perf_event_open(&attr, upp->pid, upp->pid == -1 ? 0 : -1,
+ -1, 0);
}
static int attach(dtrace_hdl_t *dtp, const dt_probe_t *uprp, int bpf_fd)
@@ -1720,6 +1887,7 @@ dt_provimpl_t dt_uprobe = {
.name = prvname,
.prog_type = BPF_PROG_TYPE_KPROBE,
.populate = &populate,
+ .provide = &provide,
.load_prog = &dt_bpf_prog_load,
.trampoline = &trampoline,
.attach = &attach,
diff --git a/test/triggers/Build b/test/triggers/Build
index 0dd546ac..fed3474b 100644
--- a/test/triggers/Build
+++ b/test/triggers/Build
@@ -25,7 +25,8 @@ EXTERNAL_TRIGGERS = $(EXTERNAL_64BIT_TRIGGERS) $(if $(NATIVE_BITNESS_ONLY),,$(EX
INTERNAL_64BIT_TRIGGERS = libproc-pldd libproc-consistency libproc-sleeper \
libproc-sleeper-pie libproc-dlmadopen libproc-lookup-by-name \
libproc-lookup-victim libproc-execing-bkpts libproc-execing-bkpts-victim \
- btf-cyclic-types btf-lazy-types shared-ctf module-discovery elf-symbols
+ btf-cyclic-types btf-lazy-types shared-ctf module-discovery elf-symbols \
+ uprobe-wildcards
INTERNAL_32BIT_TRIGGERS := libproc-sleeper-32 libproc-sleeper-pie-32
INTERNAL_TRIGGERS = $(INTERNAL_64BIT_TRIGGERS) $(if $(NATIVE_BITNESS_ONLY),,$(INTERNAL_32BIT_TRIGGERS))
@@ -258,6 +259,12 @@ ustack-tst-spin_CFLAGS := -O0
ustack-tst-mtspin_CFLAGS := -O0
ustack-tst-mtspin_LIBS := -lpthread
+uprobe-wildcards_CFLAGS := -Ilibproc -Ilibdtrace -Ilibcommon -Wno-pedantic
+uprobe-wildcards_LDFLAGS := -Wl,--export-dynamic
+uprobe-wildcards_DEPS := build-libdtrace.a build-libproc.a libcommon.a libport.a
+uprobe-wildcards_LIBS := $(objdir)/build-libdtrace.a $(objdir)/build-libproc.a \
+ $(objdir)/build-libcommon.a $(objdir)/build-libport.a $(libdtrace_LIBS)
+
module-discovery_CFLAGS := -Ilibproc -Ilibdtrace -Ilibcommon -Wno-pedantic
module-discovery_LDFLAGS := -Wl,--wrap=fopen64 \
-Wl,--wrap=dt_bpf_btf_get_next_id -Wl,--wrap=dt_bpf_btf_get_fd_by_id \
diff --git a/test/triggers/uprobe-wildcards.c b/test/triggers/uprobe-wildcards.c
new file mode 100644
index 00000000..d8815ce5
--- /dev/null
+++ b/test/triggers/uprobe-wildcards.c
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+ * Licensed under the Universal Permissive License v 1.0 as shown at
+ * http://oss.oracle.com/licenses/upl.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dt_module.h>
+#include <dt_probe.h>
+#include <dt_provider.h>
+
+#define CHECK(expr) do { \
+ if (!(expr)) { \
+ fprintf(stderr, "Check failed at line %d: %s\n", __LINE__, #expr); \
+ return 1; \
+ } \
+} while (0)
+
+/* Export these to exercise both .symtab and stripped .dynsym objects. */
+int uprobe_test_data;
+int uprobe_test_alpha(void) { return 1; }
+int uprobe_test_beta(void) { return 2; }
+
+static int
+count_probe(dtrace_hdl_t *dtp, const dtrace_probedesc_t *pdp, void *arg)
+{
+ int *count = arg;
+
+ (*count)++;
+ return 0;
+}
+
+typedef struct module_count {
+ int count;
+ int result;
+} module_count_t;
+
+static int
+count_module(dtrace_hdl_t *dtp, dt_module_t *dmp, void *arg)
+{
+ module_count_t *data = arg;
+
+ data->count++;
+ return data->result;
+}
+
+int
+main(int argc, char **argv)
+{
+ dtrace_hdl_t *dtp;
+ dt_probe_t *prp;
+ int err, count = 0;
+ module_count_t modules = { 0 };
+ dtrace_probedesc_t pd = {
+ .prv = "uprobe", .mod = "uprobe-wildcards-a",
+ .fun = "uprobe_test_*", .prb = "*",
+ };
+
+ CHECK(argc == 2);
+ CHECK(setenv("LD_LIBRARY_PATH", argv[1], 1) == 0);
+ CHECK((dtp = dtrace_open(DTRACE_VERSION, 0, &err)) != NULL);
+ dt_probe_init(dtp);
+ CHECK(dt_uprobe.populate(dtp) == 0);
+
+ CHECK(dt_uprobe.provide(dtp, &pd) == 4);
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+ pd.mod = "uprobe-wildcards-[ab]";
+ CHECK(dt_uprobe.provide(dtp, &pd) == 4);
+ pd.mod = "uprobe-wildcards-?";
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+ pd.prv = "uprob?";
+ pd.fun = "uprobe_test_?lpha";
+ pd.prb = "[er]*";
+ CHECK(dtrace_probe_iter(dtp, &pd, count_probe, &count) == 0);
+ CHECK(count == 4);
+ pd.prv = "uprobe";
+
+ /* Visit cached modules once each and honor callback termination. */
+ CHECK(dt_module_user_iter(dtp, "uprobe-wildcards-[ab]",
+ count_module, &modules) == 0);
+ CHECK(modules.count == 2);
+ modules.count = 0;
+ modules.result = 7;
+ CHECK(dt_module_user_iter(dtp, "uprobe-wildcards-[ab]",
+ count_module, &modules) == 7);
+ CHECK(modules.count == 1);
+ modules.count = 0;
+ CHECK(dt_module_user_iter(dtp, "uprobe-no-such-module-*",
+ count_module, &modules) == 0);
+ CHECK(modules.count == 0);
+
+ pd.mod = "uprobe-wildcards-b";
+ pd.fun = "uprobe_test_alpha";
+ pd.prb = "entry";
+ CHECK((prp = dt_probe_lookup(dtp, &pd)) != NULL);
+ CHECK(strcmp(prp->desc->mod, pd.mod) == 0);
+ CHECK(strcmp(prp->desc->fun, pd.fun) == 0);
+ pd.prb = "return";
+ CHECK(dt_probe_lookup(dtp, &pd) != NULL);
+ pd.fun = "uprobe_test_data";
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+ CHECK(dt_probe_lookup(dtp, &pd) == NULL);
+ pd.fun = "fprintf";
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+ pd.prb = "no-such-probe";
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+
+ /* Empty components match everything, including both probe names. */
+ pd.mod = "uprobe-wildcards-b";
+ pd.fun = "";
+ pd.prb = "";
+ CHECK(dt_uprobe.provide(dtp, &pd) > 0);
+ CHECK(dt_uprobe.provide(dtp, &pd) == 0);
+ pd.mod = "";
+ pd.fun = "uprobe_test_?lpha";
+ CHECK(dt_uprobe.provide(dtp, &pd) >= 0);
+
+ dtrace_close(dtp);
+ return 0;
+}
diff --git a/test/unittest/providers/uprobe/tst.dlib-path.sh b/test/unittest/providers/uprobe/tst.dlib-path.sh
new file mode 100755
index 00000000..6c457daa
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.dlib-path.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Specifying -L does not break system-wide uprobe probes.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+NWORKERS=20
+DIRNAME="$tmpdir/uprobe-dlib-path.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+ return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 2)
+ return 2;
+
+ value = strtol(argv[1], NULL, 0);
+ got = churn_probe(value);
+
+ return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+$dtrace $dt_flags -L/nothing -c './worker 1' -qn '
+uprobe:libswchurn.so:churn_probe:entry
+{
+ printf("%ld = PID %d\n", arg0, pid);
+ exit(0);
+}
+
+profile:::tick-1s
+{
+ exit(1);
+}
+'
+
+exit $?
diff --git a/test/unittest/providers/uprobe/tst.dlopen.sh b/test/unittest/providers/uprobe/tst.dlopen.sh
new file mode 100755
index 00000000..04a281af
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.dlopen.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe fires for a library that is dlopen()ed only
+# after tracing has started.
+#
+# @@timeout: 30
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-dlopen.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+lazy_probe(long value)
+{
+ return value * 3 + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+typedef long (*probe_f)(long);
+
+int
+main(int argc, char **argv)
+{
+ char ch;
+ int fd;
+ long got;
+ void *hdl;
+ probe_f probe;
+
+ if (argc != 2)
+ return 2;
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd == -1)
+ return 3;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 4;
+ close(fd);
+
+ hdl = dlopen("libswlazy.so", RTLD_NOW);
+ if (hdl == NULL) {
+ fprintf(stderr, "dlopen failed: %s\n", dlerror());
+ return 5;
+ }
+
+ probe = (probe_f)dlsym(hdl, "lazy_probe");
+ if (probe == NULL) {
+ fprintf(stderr, "dlsym failed: %s\n", dlerror());
+ return 6;
+ }
+
+ got = probe(17);
+ dlclose(hdl);
+
+ return got == 52 ? 0 : 7;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswlazy.so probe.c || exit 1
+if ! ${CC:-cc} $test_cppflags -o worker worker.c -ldl; then
+ ${CC:-cc} $test_cppflags -o worker worker.c || exit 1
+fi
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+mkfifo gate
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+./worker gate &
+worker_pid=$!
+
+# $dtrace $dt_flags -qn '
+$dtrace $dt_flags -n '
+uprobe:libswlazy.so:lazy_probe:entry
+{
+ self->pid = pid;
+ self->expected = arg0 * 3 + 1;
+ printf("E %ld\n", arg0);
+}
+
+uprobe:libswlazy.so:lazy_probe:return
+/self->pid == pid/
+{
+ printf("R %ld (expected %ld)\n", arg1, self->expected);
+ exit(self->expected == arg1 ? 0 : 1);
+}
+
+profile:::tick-5s
+{
+ exit(1);
+}
+' > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+printf x > gate
+
+wait "$worker_pid" || exit 1
+worker_pid=
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.elf-offsets.sh b/test/unittest/providers/uprobe/tst.elf-offsets.sh
new file mode 100755
index 00000000..af9fe16f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.elf-offsets.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobes use correct file offsets for shared objects,
+# PIE executables, and non-PIE executables when the toolchain supports them.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-elf-offsets.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > libprobe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+lib_offset_probe(long value)
+{
+ return value + 101;
+}
+EOF
+
+cat > libworker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long lib_offset_probe(long);
+
+static int
+wait_for_gate(const char *path)
+{
+ char ch;
+ int fd = open(path, O_RDONLY);
+
+ if (fd == -1)
+ return 1;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 1;
+ close(fd);
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 3)
+ return 2;
+ if (wait_for_gate(argv[1]) != 0)
+ return 3;
+
+ value = strtol(argv[2], NULL, 0);
+ got = lib_offset_probe(value);
+
+ return got == value + 101 ? 0 : 4;
+}
+EOF
+
+cat > pieprog.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+long
+pie_offset_probe(long value)
+{
+ return value + 202;
+}
+
+static int
+wait_for_gate(const char *path)
+{
+ char ch;
+ int fd = open(path, O_RDONLY);
+
+ if (fd == -1)
+ return 1;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 1;
+ close(fd);
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 3)
+ return 2;
+ if (wait_for_gate(argv[1]) != 0)
+ return 3;
+
+ value = strtol(argv[2], NULL, 0);
+ got = pie_offset_probe(value);
+
+ return got == value + 202 ? 0 : 4;
+}
+EOF
+
+cat > nopieprog.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+long
+nopie_offset_probe(long value)
+{
+ return value + 303;
+}
+
+static int
+wait_for_gate(const char *path)
+{
+ char ch;
+ int fd = open(path, O_RDONLY);
+
+ if (fd == -1)
+ return 1;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 1;
+ close(fd);
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 3)
+ return 2;
+ if (wait_for_gate(argv[1]) != 0)
+ return 3;
+
+ value = strtol(argv[2], NULL, 0);
+ got = nopie_offset_probe(value);
+
+ return got == value + 303 ? 0 : 4;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libuoffset.so libprobe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o libworker libworker.c -luoffset || exit 1
+${CC:-cc} $test_cppflags -fPIE -pie -o pieprog pieprog.c || exit 1
+
+have_nopie=0
+if ${CC:-cc} $test_cppflags -fno-pie -no-pie -o nopieprog nopieprog.c; then
+ have_nopie=1
+else
+ echo "non-PIE executable build failed: non-PIE subtest skipped"
+fi
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+export PATH="$PWD${PATH:+:$PATH}"
+mkfifo gate
+
+cat > trace.d <<'EOF'
+uprobe:libuoffset.so:lib_offset_probe:entry
+{
+ libhit = 1;
+ printf("lib %d %ld\n", pid, arg0);
+}
+
+uprobe:pieprog:pie_offset_probe:entry
+{
+ piehit = 1;
+ printf("pie %d %ld\n", pid, arg0);
+}
+EOF
+
+if [ "$have_nopie" = 1 ]; then
+ cat >> trace.d <<'EOF'
+
+uprobe:nopieprog:nopie_offset_probe:entry
+{
+ nopiehit = 1;
+ printf("nopie %d %ld\n", pid, arg0);
+}
+EOF
+fi
+
+cat >> trace.d <<EOF
+
+profile:::tick-1s
+EOF
+if [ "$have_nopie" = 1 ]; then
+ cat >> trace.d <<'EOF'
+/libhit && piehit && nopiehit/
+EOF
+else
+ cat >> trace.d <<'EOF'
+/libhit && piehit/
+EOF
+fi
+cat >> trace.d <<'EOF'
+{
+ exit(0);
+}
+
+profile:::tick-12s
+{
+ exit(1);
+}
+EOF
+
+dtrace_pid=
+pids=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+./libworker gate 10 & pids="$pids $!"
+./pieprog gate 20 & pids="$pids $!"
+if [ "$have_nopie" = 1 ]; then
+ ./nopieprog gate 30 & pids="$pids $!"
+fi
+
+$dtrace $dt_flags -qs trace.d > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+expected=2
+[ "$have_nopie" = 1 ] && expected=3
+i=0
+while [ $i -lt $expected ]; do
+ printf x
+ i=$((i + 1))
+done > gate
+
+for pid in $pids; do
+ wait "$pid" || exit 1
+done
+pids=
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+for name in lib pie; do
+ if ! grep -q "^$name " dtrace.out; then
+ echo "ERROR: missing $name uprobe hit"
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+ fi
+done
+if [ "$have_nopie" = 1 ] && ! grep -q '^nopie ' dtrace.out; then
+ echo "ERROR: missing nopie uprobe hit"
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.lifecycle.sh b/test/unittest/providers/uprobe/tst.lifecycle.sh
new file mode 100755
index 00000000..2c38a235
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.lifecycle.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe can be enabled, fired, detached, and enabled
+# again repeatedly for the same ELF object.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-lifecycle.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+life_probe(long value)
+{
+ return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long life_probe(long);
+
+static int
+wait_for_gate(const char *path)
+{
+ char ch;
+ int fd = open(path, O_RDONLY);
+
+ if (fd == -1)
+ return 1;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 1;
+ close(fd);
+ return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 3)
+ return 2;
+
+ value = strtol(argv[1], NULL, 0);
+ if (wait_for_gate(argv[2]) != 0)
+ return 3;
+
+ got = life_probe(value);
+
+ return got == value + 1 ? 0 : 4;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswlife.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswlife || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+i=1
+while [ $i -le 5 ]; do
+ gate="gate.$i"
+ mkfifo "$gate"
+ ./worker "$i" "$gate" &
+ worker_pid=$!
+
+ $dtrace $dt_flags -qn '
+uprobe:libswlife.so:life_probe:entry
+{
+ printf("%ld\n", arg0);
+ exit(arg0 != $1);
+}
+
+profile:::tick-8s
+{
+ exit(1);
+}
+' $i > "dtrace.$i.out" 2> "dtrace.$i.err" &
+ dtrace_pid=$!
+
+ sleep 2
+ printf x > "$gate"
+
+ if ! wait "$worker_pid"; then
+ cat "dtrace.$i.err"
+ cat "dtrace.$i.out"
+ exit 1
+ fi
+ worker_pid=
+
+ if ! wait "$dtrace_pid"; then
+ cat "dtrace.$i.err"
+ cat "dtrace.$i.out"
+ exit 1
+ fi
+ dtrace_pid=
+
+ if ! grep -qx "$i" "dtrace.$i.out"; then
+ echo "ERROR: iteration $i did not report the expected argument"
+ cat "dtrace.$i.err"
+ cat "dtrace.$i.out"
+ exit 1
+ fi
+
+ i=$((i + 1))
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multicpu.r b/test/unittest/providers/uprobe/tst.multicpu.r
new file mode 100644
index 00000000..8f30a28f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.r
@@ -0,0 +1,4 @@
+1st CPU is A
+2nd CPU is B
+cpu A
+cpu B
diff --git a/test/unittest/providers/uprobe/tst.multicpu.r.p b/test/unittest/providers/uprobe/tst.multicpu.r.p
new file mode 100755
index 00000000..89a1dbb4
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.r.p
@@ -0,0 +1,8 @@
+#!/usr/bin/gawk -f
+
+# 1st CPU is %d
+/^1st/ { cpu_a = $NF; $NF = "A"; print; next; }
+# 2nd CPU is %d
+/^2nd/ { cpu_b = $NF; $NF = "B"; print; next; }
+# cpu %d
+/^cpu/ { $NF = ($NF == cpu_a) ? "A" : ($NF == cpu_b) ? "B" : "X"; print; }
diff --git a/test/unittest/providers/uprobe/tst.multicpu.sh b/test/unittest/providers/uprobe/tst.multicpu.sh
new file mode 100755
index 00000000..9a4760ab
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe can fires on every CPU allowed by the current
+# cpuset.
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-allcpus.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+#include <unistd.h>
+
+__attribute__((noinline, visibility("default")))
+void
+cpu_probe(unsigned long value)
+{
+ __asm__ volatile("" : : "r" (value) : "memory");
+}
+
+void
+run_probes(void)
+{
+ int i;
+
+ for (i = 0; i < 100; i++) {
+ cpu_probe((unsigned long)i);
+ usleep(10000);
+ }
+}
+EOF
+
+cat > worker.c <<'EOF'
+extern void run_probes(void);
+int main(void) { run_probes(); return 0; }
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswcpu.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswcpu || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+# Expand the affinity list and choose up to two actual allowed CPUs. This is
+# cpuset-safe and intentionally does not assume that CPU 0 is available.
+cpuspec=$(awk '/^Cpus_allowed_list:/ { print $2 }' /proc/self/status)
+cpus=()
+IFS=',' read -ra ranges <<< "$cpuspec"
+for range in "${ranges[@]}"; do
+ if [[ $range == *-* ]]; then
+ lo=${range%-*}; hi=${range#*-}
+ for ((cpu = lo; cpu <= hi; cpu++)); do
+ cpus+=("$cpu")
+ done
+ else
+ cpus+=("$range")
+ fi
+done
+
+[ ${#cpus[@]} -gt 0 ] || { echo ERROR: no allowed CPUs; exit 1; }
+[ ${#cpus[@]} -gt 2 ] || { echo SKIPPED: not enough CPUs; exit 2; }
+
+# We do not want to use CPU 0 because that is specified in perf_event_open()
+cpu_a=${cpus[1]}
+cpu_b=${cpus[${#cpus[@]}-1]}
+
+dtrace_pid=
+workers=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$workers" ] && kill $workers 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+dtrace:::BEGIN {
+ printf("1st CPU is %d\n2nd CPU is %d\n", $1, $2);
+}
+
+uprobe:libswcpu.so:cpu_probe:entry
+/seen[cpu] == 0/
+{
+ seen[cpu] = 1;
+ printf("cpu %d\n", cpu);
+}
+
+uprobe:libswcpu.so:cpu_probe:entry
+/seen[$1] > 0 && seen[$2] > 0/
+{
+ exit(0);
+}
+
+profile:::tick-12s
+{
+ exit(1);
+}
+' $cpu_a $cpu_b > dtrace.out &
+dtrace_pid=$!
+
+sleep 2
+taskset -c "$cpu_a" ./worker & w1=$!; workers="$workers $w1"
+if [ "$cpu_b" != "$cpu_a" ]; then
+ taskset -c "$cpu_b" ./worker & w2=$!; workers="$workers $w2"
+fi
+
+wait "$w1" || exit 1
+if [ -n "${w2:-}" ]; then
+ wait "$w2" || exit 1
+fi
+workers=
+
+mv $DIRNAME /tmp/check-it
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+cat dtrace.out
+for cpu in "$cpu_a" "$cpu_b"; do
+ if ! grep -qx "cpu $cpu" dtrace.out; then
+ echo "ERROR: no uprobe fired on allowed CPU $cpu"
+ exit 1
+ fi
+done
+
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multicpu.x b/test/unittest/providers/uprobe/tst.multicpu.x
new file mode 100755
index 00000000..1a1a05c8
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multicpu.x
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Skip test if taskset is not available.
+if ! command -v taskset > /dev/null; then
+ echo "taskset not found, skipping test"
+ exit 2
+fi
+
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multipid.r b/test/unittest/providers/uprobe/tst.multipid.r
new file mode 100644
index 00000000..e2d34299
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.r
@@ -0,0 +1,20 @@
+0 = PID NNN
+1 = PID NNN
+2 = PID NNN
+3 = PID NNN
+4 = PID NNN
+5 = PID NNN
+6 = PID NNN
+7 = PID NNN
+8 = PID NNN
+9 = PID NNN
+11 = PID NNN
+10 = PID NNN
+12 = PID NNN
+13 = PID NNN
+14 = PID NNN
+15 = PID NNN
+16 = PID NNN
+17 = PID NNN
+18 = PID NNN
+19 = PID NNN
diff --git a/test/unittest/providers/uprobe/tst.multipid.r.p b/test/unittest/providers/uprobe/tst.multipid.r.p
new file mode 100755
index 00000000..a78716f1
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.r.p
@@ -0,0 +1,4 @@
+#!/usr/bin/gawk -f
+
+# n = PID NNN
+$3 == "PID" { $4 = "NNN"; print; }
diff --git a/test/unittest/providers/uprobe/tst.multipid.sh b/test/unittest/providers/uprobe/tst.multipid.sh
new file mode 100755
index 00000000..023092bd
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multipid.sh
@@ -0,0 +1,117 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe remains active for many short-lived
+# processes created after tracing starts.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+NWORKERS=20
+DIRNAME="$tmpdir/uprobe-multipid.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+ return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 2)
+ return 2;
+
+ value = strtol(argv[1], NULL, 0);
+ got = churn_probe(value);
+
+ return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+pids=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+uprobe:libswchurn.so:churn_probe:entry
+/seen[pid] == 0/
+{
+ seen[pid] = 1;
+ hits++;
+ printf("%ld = PID %d\n", arg0, pid);
+}
+
+profile:::tick-1s
+/hits >= $1/
+{
+ exit(0);
+}
+
+profile:::tick-20s
+{
+ exit(1);
+}
+' $NWORKERS > dtrace.out &
+dtrace_pid=$!
+
+sleep 2
+
+i=0
+while [ $i -lt $NWORKERS ]; do
+ ./worker "$i" &
+ pids="$pids $!"
+ i=$((i + 1))
+done
+
+for pid in $pids; do
+ wait "$pid" || exit 1
+done
+pids=
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+cat dtrace.out
+if [ "$(grep PID dtrace.out | sort -u | wc -l)" != "$NWORKERS" ]; then
+ echo "ERROR: expected $NWORKERS distinct traced processes"
+ cat dtrace.out
+ exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.multithread.sh b/test/unittest/providers/uprobe/tst.multithread.sh
new file mode 100755
index 00000000..3c44546f
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.multithread.sh
@@ -0,0 +1,160 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobe entry and return probes preserve per-thread
+# self state under concurrent calls from multiple threads.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+NTHREADS=4
+NITER=20
+EXPECTED=$((NTHREADS * NITER))
+DIRNAME="$tmpdir/uprobe-multithread.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+thread_probe(long thread_id, long iter)
+{
+ return thread_id * 100000 + iter;
+}
+EOF
+
+cat > worker.c <<EOF
+#include <pthread.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#define NTHREADS $NTHREADS
+#define NITER $NITER
+
+extern long thread_probe(long, long);
+
+static void *
+run_thread(void *arg)
+{
+ long id = (long)(intptr_t)arg;
+ long i;
+
+ for (i = 0; i < NITER; i++) {
+ long got = thread_probe(id, i);
+
+ if (got != id * 100000 + i)
+ return (void *)1;
+ usleep(1000);
+ }
+
+ return NULL;
+}
+
+int
+main(void)
+{
+ pthread_t threads[NTHREADS];
+ long i;
+
+ for (i = 0; i < NTHREADS; i++) {
+ if (pthread_create(&threads[i], NULL, run_thread,
+ (void *)(intptr_t)i) != 0)
+ return 2;
+ }
+
+ for (i = 0; i < NTHREADS; i++) {
+ void *status = NULL;
+
+ if (pthread_join(threads[i], &status) != 0 || status != NULL)
+ return 3;
+ }
+
+ return 0;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswthread.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswthread -pthread || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+dtrace_pid=
+worker_pid=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$worker_pid" ] && kill "$worker_pid" 2>/dev/null
+}
+trap cleanup EXIT
+
+$dtrace $dt_flags -qn '
+BEGIN
+{
+ entries = 0;
+ bad = 0;
+ tids = 0;
+}
+
+uprobe:libswthread.so:thread_probe:entry
+{
+ entries++;
+ self->active = 1;
+ self->expected = arg0 * 100000 + arg1;
+}
+
+uprobe:libswthread.so:thread_probe:entry
+/seen[tid] == 0/
+{
+ seen[tid] = 1;
+ tids++;
+}
+
+uprobe:libswthread.so:thread_probe:return
+/self->active/
+{
+ returns++;
+ bad += arg1 != self->expected;
+ self->active = 0;
+}
+
+uprobe:libswthread.so:thread_probe:return
+/returns == $1/
+{
+ exit(bad != 0 || entries != $1 || tids != $2);
+}
+
+profile:::tick-20s
+{
+ exit(1);
+}
+' $EXPECTED $NTHREADS > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+./worker &
+worker_pid=$!
+
+if ! wait "$worker_pid"; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+worker_pid=
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.probespec.sh b/test/unittest/providers/uprobe/tst.probespec.sh
new file mode 100755
index 00000000..ccf2f5fc
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.probespec.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Exact and globbed system-wide uprobe descriptions create probes
+# with canonical entry and return names.
+#
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-probespec.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long named_probe(long value) { return value + 1; }
+EOF
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswpspec.so probe.c || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+check()
+{
+ spec=$1
+ shift
+
+ if ! $dtrace $dt_flags -l -n "$spec" > list.out 2> list.err; then
+ cat list.err
+ exit 1
+ fi
+
+ awk '$2 == "uprobe" && $3 == "libswpspec.so" &&
+ $4 == "named_probe" { print $5 }' list.out | sort -u > names.out
+ printf '%s\n' "$@" | sort -u > expected.out
+ if ! diff -u expected.out names.out; then
+ echo "ERROR: wrong probes for $spec"
+ cat list.err
+ cat list.out
+ exit 1
+ fi
+}
+
+check 'uprobe:libswpspec.so:named_probe:entry' entry
+check 'uprobe:libswpspec.so:named_probe:return' return
+check 'uprobe:libswpspec.so:named_probe:e*' entry
+check 'uprobe:libswpspec.so:named_probe:ret*' return
+check 'uprobe:libswpspec.so:named_probe:*' entry return
+check 'uprobe:libswpspec.so:named_probe:' entry return
+
+for spec in \
+ 'uprobe:libswpspec.so:named_probe:offset' \
+ 'uprobe:libswpspec.so:no_such_function:entry' \
+ 'uprobe:no_such_module.so:named_probe:entry'; do
+ if $dtrace $dt_flags -e -n "$spec" > /dev/null 2>&1; then
+ echo "ERROR: unexpectedly matched $spec"
+ exit 1
+ fi
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.search-path.sh b/test/unittest/providers/uprobe/tst.search-path.sh
new file mode 100755
index 00000000..5ee8ec9d
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.search-path.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: Userspace modules are resolved in LD_LIBRARY_PATH/PATH order,
+# including empty path components, the current directory, and relative search
+# paths.
+#
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-search-path.$$.$RANDOM"
+mkdir -p "$DIRNAME/A" "$DIRNAME/B" "$DIRNAME/bin" "$DIRNAME/sub"
+cd "$DIRNAME" || exit 1
+
+cat > a.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_a(void) {}
+EOF
+cat > b.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_b(void) {}
+EOF
+cat > cwd.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_cwd(void) {}
+EOF
+cat > path.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_path(void) {}
+int main(void) { from_path(); return 0; }
+EOF
+cat > relative.c <<'EOF'
+__attribute__((noinline, visibility("default"))) void from_relative(void) {}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o A/libchoice.so a.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o B/libchoice.so b.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o libcwd.so cwd.c || exit 1
+${CC:-cc} $test_cppflags -o bin/swpathprog path.c || exit 1
+${CC:-cc} $test_cppflags -fPIC -shared -o sub/librelative.so relative.c || exit 1
+
+# Retain the runner's path to libdtrace after the test's search directories.
+match()
+{
+ env_ld="$1${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+ env_path=$2
+ spec=$3
+ if ! LD_LIBRARY_PATH="$env_ld" PATH="$env_path" \
+ "$dtrace" $dt_flags -e -n "$spec" > dtrace.out 2> dtrace.err; then
+ echo "ERROR: did not resolve $spec"
+ cat dtrace.err
+ exit 1
+ fi
+}
+
+no_match()
+{
+ env_ld="$1${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+ env_path=$2
+ spec=$3
+ if LD_LIBRARY_PATH="$env_ld" PATH="$env_path" \
+ "$dtrace" $dt_flags -e -n "$spec" > dtrace.out 2> dtrace.err; then
+ echo "ERROR: unexpectedly resolved $spec"
+ exit 1
+ fi
+}
+
+sys_path=${PATH:-/usr/bin:/bin}
+
+# Search order must select one of two objects with the same module name.
+match "$PWD/A:$PWD/B" "$sys_path" 'uprobe:libchoice.so:from_a:entry'
+no_match "$PWD/A:$PWD/B" "$sys_path" 'uprobe:libchoice.so:from_b:entry'
+match "$PWD/B:$PWD/A" "$sys_path" 'uprobe:libchoice.so:from_b:entry'
+no_match "$PWD/B:$PWD/A" "$sys_path" 'uprobe:libchoice.so:from_a:entry'
+
+# Empty components denote '.', and duplicate components are harmless.
+match ":$PWD/A::$PWD/A" "$sys_path" 'uprobe:libcwd.so:from_cwd:entry'
+
+# Executables can be found through PATH.
+match "" "$PWD/bin:$sys_path" 'uprobe:swpathprog:from_path:entry'
+
+# Relative search paths resolve modules by their basename.
+no_match "" "$sys_path" 'uprobe:librelative.so:from_relative:entry'
+match "sub" "$sys_path" 'uprobe:librelative.so:from_relative:entry'
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.shlib.sh b/test/unittest/providers/uprobe/tst.shlib.sh
new file mode 100755
index 00000000..a4596166
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.shlib.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe probe in a shared library works.
+#
+# @@timeout: 45
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-shlib.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+churn_probe(long value)
+{
+ return value + 1;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <stdlib.h>
+
+extern long churn_probe(long);
+
+int
+main(int argc, char **argv)
+{
+ long value, got;
+
+ if (argc != 2)
+ return 2;
+
+ value = strtol(argv[1], NULL, 0);
+ got = churn_probe(value);
+
+ return got == value + 1 ? 0 : 3;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswchurn.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswchurn || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+if ! $dtrace $dt_flags -c "./worker 17" -qn '
+uprobe:libswchurn.so:churn_probe:entry
+{
+ printf("%ld = PID %d\n", arg0, pid);
+ exit(arg0 != 17);
+}
+
+profile:::tick-1s
+{
+ exit(1);
+}
+' > dtrace.out 2> dtrace.err; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+
+if ! grep -Eq '^17 = PID [0-9]+$' dtrace.out; then
+ echo "ERROR: shared-library probe did not report the expected argument"
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.symbols.sh b/test/unittest/providers/uprobe/tst.symbols.sh
new file mode 100755
index 00000000..e46c1f76
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.symbols.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: System-wide uprobes accept defined function symbols in stripped
+# DSOs, and reject data, undefined, and nonexistent symbols.
+#
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-symbols.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > symbols.c <<'EOF'
+extern void undefined_function(void);
+__attribute__((visibility("default"))) long exported_data = 123;
+__attribute__((noinline, visibility("default"))) long exported_function(void)
+{
+ if (exported_data == -1)
+ undefined_function();
+ return exported_data;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswsymbols.so symbols.c || exit 1
+${STRIP:-strip} --strip-unneeded lib/libswsymbols.so || exit 1
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+
+if ! $dtrace $dt_flags -e -n \
+ 'uprobe:libswsymbols.so:exported_function:entry' >/dev/null 2>&1; then
+ echo ERROR: exported function was not found in stripped DSO
+ exit 1
+fi
+
+for symbol in exported_data undefined_function does_not_exist; do
+ if $dtrace $dt_flags -e -n \
+ "uprobe:libswsymbols.so:$symbol:entry" >/dev/null 2>&1; then
+ echo "ERROR: non-function or undefined symbol $symbol was accepted"
+ exit 1
+ fi
+done
+
+echo success
+exit 0
diff --git a/test/unittest/providers/uprobe/tst.systemwide.sh b/test/unittest/providers/uprobe/tst.systemwide.sh
new file mode 100755
index 00000000..e934d64d
--- /dev/null
+++ b/test/unittest/providers/uprobe/tst.systemwide.sh
@@ -0,0 +1,158 @@
+#!/bin/bash
+#
+# Oracle Linux DTrace.
+# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at
+# http://oss.oracle.com/licenses/upl.
+#
+# ASSERTION: A system-wide uprobe fires for existing and newly-created,
+# unrelated processes, and supplies entry arguments and return values.
+#
+# @@timeout: 30
+
+if [ $# != 1 ]; then
+ echo expected one argument: '<'dtrace-path'>'
+ exit 2
+fi
+
+dtrace=$1
+DIRNAME="$tmpdir/uprobe-systemwide.$$.$RANDOM"
+mkdir -p "$DIRNAME/lib"
+cd "$DIRNAME" || exit 1
+
+cat > probe.c <<'EOF'
+__attribute__((noinline, visibility("default")))
+long
+sw_probe(long a, long b, long c, long d, long e, long f)
+{
+ return a + b + c + d + e + f;
+}
+EOF
+
+cat > worker.c <<'EOF'
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+extern long sw_probe(long, long, long, long, long, long);
+
+int
+main(int argc, char **argv)
+{
+ char ch;
+ long base, got;
+ int fd;
+
+ if (argc != 3)
+ return 2;
+
+ fd = open(argv[1], O_RDONLY);
+ if (fd == -1)
+ return 3;
+ if (read(fd, &ch, 1) == -1 && errno != EINTR)
+ return 4;
+ close(fd);
+
+ base = strtol(argv[2], NULL, 0);
+ got = sw_probe(base, base + 1, base + 2, base + 3, base + 4,
+ base + 5);
+
+ return got == base * 6 + 15 ? 0 : 5;
+}
+EOF
+
+${CC:-cc} $test_cppflags -fPIC -shared -o lib/libswuprobe.so probe.c || exit 1
+${CC:-cc} $test_cppflags -Llib -o worker worker.c -lswuprobe || exit 1
+
+export LD_LIBRARY_PATH="$PWD/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+mkfifo gate
+
+pids=
+dtrace_pid=
+cleanup()
+{
+ [ -n "$dtrace_pid" ] && kill "$dtrace_pid" 2>/dev/null
+ [ -n "$pids" ] && kill $pids 2>/dev/null
+}
+trap cleanup EXIT
+
+# These workers exist before DTrace attaches. They are unrelated to DTrace
+# and wait until the tracing process has had time to enable its perf events.
+./worker gate 10 & p1=$!; pids="$pids $p1"
+./worker gate 20 & p2=$!; pids="$pids $p2"
+
+$dtrace $dt_flags -qn '
+dtrace:::BEGIN
+{
+ bad = 0;
+}
+
+uprobe:libswuprobe.so:sw_probe:entry
+{
+ entries++;
+ self->expected = arg0 + arg1 + arg2 + arg3 + arg4 + arg5;
+ bad += arg1 != arg0 + 1 || arg2 != arg0 + 2 ||
+ arg3 != arg0 + 3 || arg4 != arg0 + 4 || arg5 != arg0 + 5;
+ bad += probemod != "libswuprobe.so" || probefunc != "sw_probe" ||
+ probename != "entry";
+ printf("E %d %d %ld\n", pid, tid, self->expected);
+}
+
+/* A second clause must share the underlying uprobe and still be called. */
+uprobe:libswuprobe.so:sw_probe:entry
+{
+ extra_entries++;
+}
+
+uprobe:libswuprobe.so:sw_probe:return
+{
+ returns++;
+ bad += arg1 != self->expected;
+ self->expected = 0;
+ printf("R %d %d %ld\n", pid, tid, arg1);
+}
+
+uprobe:libswuprobe.so:sw_probe:return
+/ returns == 3 /
+{
+ exit(bad != 0 || entries != 3 || extra_entries != 3);
+}
+
+profile:::tick-12s
+{
+ exit(1);
+}
+' > dtrace.out 2> dtrace.err &
+dtrace_pid=$!
+
+sleep 2
+printf 'xx' > gate
+
+# This worker is created only after the probe has been enabled.
+./worker /dev/null 30 & p3=$!; pids="$pids $p3"
+
+wait "$p1" || exit 1
+wait "$p2" || exit 1
+wait "$p3" || exit 1
+pids=
+
+if ! wait "$dtrace_pid"; then
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+dtrace_pid=
+
+if [ "$(awk '$1 == "E" { n++ } END { print n + 0 }' dtrace.out)" != 3 ] ||
+ [ "$(awk '$1 == "R" { n++ } END { print n + 0 }' dtrace.out)" != 3 ] ||
+ [ "$(awk '$1 == "E" { print $2 }' dtrace.out | sort -u | wc -l)" != 3 ]; then
+ echo ERROR: expected one entry and return from each of three processes
+ cat dtrace.err
+ cat dtrace.out
+ exit 1
+fi
+
+echo success
+exit 0
--
2.52.0
reply other threads:[~2026-10-30 19:38 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=3a4910515ffe2cc8b3f3f7bdefda3444@oracle.com \
--to=kris.van.hees@oracle.com \
--cc=dtrace-devel@oss.oracle.com \
--cc=dtrace@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox