mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH bpf-next v3 0/2] bpftool: Make the LLVM disassembler an optional runtime dependency
@ 2026-10-29 13:05 Mark Korenberg via B4 Relay
  2026-10-29 13:05 ` [PATCH bpf-next v3 1/2] bpftool: Fix disposal of the LLVM disassembler context Mark Korenberg via B4 Relay
  2026-10-29 13:05 ` [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
  0 siblings, 2 replies; 5+ messages in thread
From: Mark Korenberg via B4 Relay @ 2026-10-29 13:05 UTC (permalink / raw)
  To: Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa,
	Nathan Chancellor, Bill Wendling, Justin Stitt, Emil Tsalapatis,
	Ihor Solodrai, Nick Desaulniers
  Cc: linux-kernel, bpf, llvm, Mark Korenberg

libLLVM (~140 MiB on Fedora) is a hard dependency of bpftool, although it
is only used by "bpftool prog dump jited". This series moves the LLVM
disassembler to a plugin that bpftool loads on demand, so that
distributions can make it optional. Patch 1 fixes a leak found on the way.

Changes in v3:
- Look for the plugin next to the bpftool binary before its install
  directory, so that bpftool run from its build directory does not pick
  an older installed plugin (Sashiko)
- Link to v2: https://lore.kernel.org/r/20261029-bpftool-plugin-v2-0-1a47b2391b0b@gmail.com

Changes in v2:
- Move the LLVMDisasmDispose() fix to a separate patch (Quentin)
- Look for the plugin next to the bpftool binary when it is not installed,
  so that bpftool works from its build directory (Quentin, Sashiko). Not in
  the current directory: bpftool usually runs as root.
- Export a single versioned structure of callbacks from the plugin (Quentin)
- Only report "llvm" in "bpftool version" if the plugin loads (Quentin)
- Handle dlerror() returning NULL (Quentin)
- Rebuild jit_disasm.o if libdir changes between "make" and
  "make install" (Quentin)
- uninstall: use QUIET_UNINST, remove $(libdir)/bpftool (Quentin)
- With a static libLLVM, which a static bpftool requires, link the
  disassembler into bpftool as before (Sashiko). This drops
  LLVM_LINK_STATIC: static LLVM archives are never linked into a shared
  object.
- Commit log and comment fixes (Quentin, Sashiko)
- Rebase on bpf-next
- Link to v1: https://lore.kernel.org/r/20260603-bpftool-plugin-v1-1-68c0ac91f5a3@gmail.com

Tested on Fedora 44 x86_64, LLVM 22: plugin, built-in and SKIP_LLVM
builds; plugin lookup (installed, build directory, missing, wrong ABI);
install/uninstall; "prog dump jited" (plain and JSON) on live programs,
and disassembly of aarch64 code through the plugin. Not tested: a fully
static build (no static libraries available).

The v2 changes were written with the help of an AI assistant (Claude),
from the review comments; I reviewed and tested them.

---
Mark Korenberg (2):
      bpftool: Fix disposal of the LLVM disassembler context
      bpftool: Make the LLVM disassembler an optional runtime dependency

 tools/bpf/bpftool/Makefile      |  38 +++++++++-
 tools/bpf/bpftool/jit_disasm.c  | 158 ++++++++++++++++++++++++++++------------
 tools/bpf/bpftool/llvm_disasm.c |  84 +++++++++++++++++++++
 tools/bpf/bpftool/llvm_disasm.h |  41 +++++++++++
 tools/bpf/bpftool/main.c        |   3 +-
 tools/bpf/bpftool/main.h        |   3 +
 6 files changed, 276 insertions(+), 51 deletions(-)
---
base-commit: d80373a8e70d3193151232c3062abbbe367ff5ad
change-id: 20260603-bpftool-plugin-c994bc3e0643

Best regards,
-- 
Mark Korenberg <socketpair@gmail.com>



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH bpf-next v3 1/2] bpftool: Fix disposal of the LLVM disassembler context
  2026-10-29 13:05 [PATCH bpf-next v3 0/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
@ 2026-10-29 13:05 ` Mark Korenberg via B4 Relay
  2026-10-29 13:05 ` [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
  1 sibling, 0 replies; 5+ messages in thread
From: Mark Korenberg via B4 Relay @ 2026-10-29 13:05 UTC (permalink / raw)
  To: Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa,
	Nathan Chancellor, Bill Wendling, Justin Stitt, Emil Tsalapatis,
	Ihor Solodrai, Nick Desaulniers
  Cc: linux-kernel, bpf, llvm, Mark Korenberg

From: Mark Korenberg <socketpair@gmail.com>

destroy_context() frees the disassembler context with LLVMDisposeMessage(),
which is a plain free() meant for strings returned by LLVM. The context is
therefore never destroyed, leaking about 28 KiB per disassembled function.
Use LLVMDisasmDispose() instead.

Fixes: eb9d1acf634b ("bpftool: Add LLVM as default library for disassembling JIT-ed programs")
Signed-off-by: Mark Korenberg <socketpair@gmail.com>
---
 tools/bpf/bpftool/jit_disasm.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/bpf/bpftool/jit_disasm.c b/tools/bpf/bpftool/jit_disasm.c
index 04541155e9cc..5ff144ce84a1 100644
--- a/tools/bpf/bpftool/jit_disasm.c
+++ b/tools/bpf/bpftool/jit_disasm.c
@@ -115,7 +115,7 @@ init_context(disasm_ctx_t *ctx, const char *arch,
 
 static void destroy_context(disasm_ctx_t *ctx)
 {
-	LLVMDisposeMessage(*ctx);
+	LLVMDisasmDispose(*ctx);
 }
 
 static int

-- 
2.56.0



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency
  2026-10-29 13:05 [PATCH bpf-next v3 0/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
  2026-10-29 13:05 ` [PATCH bpf-next v3 1/2] bpftool: Fix disposal of the LLVM disassembler context Mark Korenberg via B4 Relay
@ 2026-10-29 13:05 ` Mark Korenberg via B4 Relay
  2026-10-29 13:46   ` bot+bpf-ci
  2026-10-29 15:24   ` Alexei Starovoitov
  1 sibling, 2 replies; 5+ messages in thread
From: Mark Korenberg via B4 Relay @ 2026-10-29 13:05 UTC (permalink / raw)
  To: Quentin Monnet, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman,
	Kumar Kartikeya Dwivedi, Song Liu, Yonghong Song, Jiri Olsa,
	Nathan Chancellor, Bill Wendling, Justin Stitt, Emil Tsalapatis,
	Ihor Solodrai, Nick Desaulniers
  Cc: linux-kernel, bpf, llvm, Mark Korenberg

From: Mark Korenberg <socketpair@gmail.com>

libLLVM is only needed for "bpftool prog dump jited", yet it is a hard
dependency of bpftool: on Fedora 43, installing the ~730 KiB bpftool pulls
in ~140 MiB of llvm20-libs.

Move the LLVM disassembler to a plugin, $(libdir)/bpftool/bpftool-llvm.so,
that bpftool loads with dlopen() only when disassembling JITed programs.
Distributions can then ship it as an optional package.

The plugin exports a single structure of callbacks, with an ABI version
checked by bpftool. bpftool looks for the plugin next to its own binary,
then in its install directory, but not in the current directory since it
usually runs as root. "bpftool version" reports "llvm" only if the plugin
can be loaded.

If libLLVM is static, as required for a static bpftool, the disassembler is
linked into bpftool as before.

Link: https://github.com/libbpf/bpftool/issues/262
Signed-off-by: Mark Korenberg <socketpair@gmail.com>
---
 tools/bpf/bpftool/Makefile      |  38 +++++++++-
 tools/bpf/bpftool/jit_disasm.c  | 158 ++++++++++++++++++++++++++++------------
 tools/bpf/bpftool/llvm_disasm.c |  84 +++++++++++++++++++++
 tools/bpf/bpftool/llvm_disasm.h |  41 +++++++++++
 tools/bpf/bpftool/main.c        |   3 +-
 tools/bpf/bpftool/main.h        |   3 +
 6 files changed, 276 insertions(+), 51 deletions(-)

diff --git a/tools/bpf/bpftool/Makefile b/tools/bpf/bpftool/Makefile
index b0f7168e7943..dc8e4be86296 100644
--- a/tools/bpf/bpftool/Makefile
+++ b/tools/bpf/bpftool/Makefile
@@ -63,6 +63,7 @@ $(LIBBPF_BOOTSTRAP)-clean: FORCE | $(LIBBPF_BOOTSTRAP_OUTPUT)
 	$(Q)$(MAKE) -C $(BPF_DIR) OUTPUT=$(LIBBPF_BOOTSTRAP_OUTPUT) clean >/dev/null
 
 prefix ?= /usr/local
+libdir ?= $(prefix)/lib
 bash_compdir ?= /usr/share/bash-completion/completions
 
 CFLAGS += -O2
@@ -170,7 +171,7 @@ include $(wildcard $(OUTPUT)*.d)
 
 all: $(OUTPUT)bpftool
 
-SRCS := $(wildcard *.c)
+SRCS := $(filter-out llvm_disasm.c,$(wildcard *.c))
 
 ifeq ($(feature-llvm),1)
 ifneq ($(SKIP_LLVM),1)
@@ -186,10 +187,19 @@ ifeq ($(HAS_LLVM),1)
   # Makefile includes lib.mk which sets -D_GNU_SOURCE) which would cause
   # compilation error due to redefinition. Let's filter it out here.
   CFLAGS  += $(filter-out -D_GNU_SOURCE,$(shell $(LLVM_CONFIG) --cflags))
-  LIBS    += $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
+  LLVM_LIBS := $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
   ifeq ($(shell $(LLVM_CONFIG) --shared-mode),static)
+    # No libLLVM.so dependency to avoid, link the disassembler into bpftool
+    SRCS += llvm_disasm.c
+    LIBS += $(LLVM_LIBS)
     LIBS += $(shell $(LLVM_CONFIG) --system-libs $(LLVM_CONFIG_LIB_COMPONENTS))
     LIBS += -lstdc++
+  else
+    # Build the disassembler as a plugin, the only object linked against
+    # libLLVM, that bpftool loads only to disassemble JITed programs
+    LLVM_PLUGIN := $(OUTPUT)bpftool-llvm.so
+    CFLAGS += -DLLVM_PLUGIN_DIR='"$(libdir)/bpftool"'
+    LIBS += -ldl
   endif
   LDFLAGS += $(shell $(LLVM_CONFIG) --ldflags)
 else
@@ -290,6 +300,21 @@ $(BPFTOOL_BOOTSTRAP): $(BOOTSTRAP_OBJS) $(LIBBPF_BOOTSTRAP)
 $(OUTPUT)bpftool: $(OBJS) $(LIBBPF)
 	$(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
 
+ifneq ($(LLVM_PLUGIN),)
+all: $(LLVM_PLUGIN)
+
+$(LLVM_PLUGIN): CFLAGS += -fPIC
+$(LLVM_PLUGIN): $(OUTPUT)llvm_disasm.o
+	$(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) -shared $< $(LLVM_LIBS) -o $@
+
+# The plugin directory, $(libdir)/bpftool, is compiled into jit_disasm.o.
+# Recompile it if libdir changes, e.g. on "make" then "make install libdir=...".
+$(OUTPUT)llvm-plugin-dir: FORCE
+	$(Q)echo '$(libdir)/bpftool' | cmp -s - $@ || echo '$(libdir)/bpftool' > $@
+
+$(OUTPUT)jit_disasm.o: $(OUTPUT)llvm-plugin-dir
+endif
+
 $(BOOTSTRAP_OUTPUT)%.o: %.c $(LIBBPF_BOOTSTRAP_INTERNAL_HDRS) | $(BOOTSTRAP_OUTPUT)
 	$(QUIET_CC)$(HOSTCC) $(HOST_CFLAGS) -c -MMD $< -o $@
 
@@ -303,16 +328,21 @@ feature-detect-clean:
 clean: $(LIBBPF)-clean $(LIBBPF_BOOTSTRAP)-clean feature-detect-clean
 	$(call QUIET_CLEAN, bpftool)
 	$(Q)$(RM) -- $(OUTPUT)bpftool $(OUTPUT)*.o $(OUTPUT)*.d
+	$(Q)$(RM) -- $(OUTPUT)bpftool-llvm.so $(OUTPUT)llvm-plugin-dir
 	$(Q)$(RM) -- $(OUTPUT)*.skel.h $(OUTPUT)vmlinux.h
 	$(Q)$(RM) -r -- $(LIBBPF_OUTPUT) $(BOOTSTRAP_OUTPUT)
 	$(call QUIET_CLEAN, core-gen)
 	$(Q)$(RM) -- $(OUTPUT)FEATURE-DUMP.bpftool
 	$(Q)$(RM) -r -- $(OUTPUT)feature/
 
-install-bin: $(OUTPUT)bpftool
+install-bin: $(OUTPUT)bpftool $(LLVM_PLUGIN)
 	$(call QUIET_INSTALL, bpftool)
 	$(Q)$(INSTALL) -m 0755 -d $(DESTDIR)$(prefix)/sbin
 	$(Q)$(INSTALL) $(OUTPUT)bpftool $(DESTDIR)$(prefix)/sbin/bpftool
+ifneq ($(LLVM_PLUGIN),)
+	$(call QUIET_INSTALL, bpftool-llvm.so)
+	$(Q)$(INSTALL) -m 0755 -D -t $(DESTDIR)$(libdir)/bpftool $(LLVM_PLUGIN)
+endif
 
 install: install-bin
 	$(Q)$(INSTALL) -m 0755 -d $(DESTDIR)$(bash_compdir)
@@ -321,6 +351,8 @@ install: install-bin
 uninstall:
 	$(call QUIET_UNINST, bpftool)
 	$(Q)$(RM) -- $(DESTDIR)$(prefix)/sbin/bpftool
+	$(call QUIET_UNINST, bpftool-llvm.so)
+	$(Q)$(RM) -r -- $(DESTDIR)$(libdir)/bpftool
 	$(Q)$(RM) -- $(DESTDIR)$(bash_compdir)/bpftool
 
 doc:
diff --git a/tools/bpf/bpftool/jit_disasm.c b/tools/bpf/bpftool/jit_disasm.c
index 5ff144ce84a1..c95808a96914 100644
--- a/tools/bpf/bpftool/jit_disasm.c
+++ b/tools/bpf/bpftool/jit_disasm.c
@@ -25,10 +25,10 @@
 #include <bpf/libbpf.h>
 
 #ifdef HAVE_LLVM_SUPPORT
-#include <llvm-c/Core.h>
-#include <llvm-c/Disassembler.h>
-#include <llvm-c/Target.h>
-#include <llvm-c/TargetMachine.h>
+#ifdef LLVM_PLUGIN_DIR
+#include <dlfcn.h>
+#endif
+#include "llvm_disasm.h"
 #endif
 
 #ifdef HAVE_LIBBFD_SUPPORT
@@ -45,7 +45,102 @@ static int oper_count;
 #ifdef HAVE_LLVM_SUPPORT
 #define DISASM_SPACER
 
-typedef LLVMDisasmContextRef disasm_ctx_t;
+typedef void *disasm_ctx_t;
+
+static const struct bpftool_llvm_ops *llvm_ops;
+
+#ifdef LLVM_PLUGIN_DIR
+#define LLVM_PLUGIN_NAME "bpftool-llvm.so"
+
+static const char *dl_error(void)
+{
+	const char *err = dlerror();
+
+	/* The error may already be consumed, e.g. by an LD_PRELOAD-ed library */
+	return err ? err : "unknown error";
+}
+
+/*
+ * Look for the plugin next to the bpftool binary, so that bpftool run from
+ * its build directory uses its own plugin, then in its install directory.
+ * Do not look in the current directory: bpftool usually runs as root, and
+ * would then load any library planted in the directory it is run from.
+ */
+static int llvm_plugin_find(char *path, size_t size)
+{
+	char exe[PATH_MAX];
+	ssize_t len;
+	char *dir;
+	int n;
+
+	len = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+	if (len > 0) {
+		exe[len] = '\0';
+		dir = strrchr(exe, '/');
+		if (dir) {
+			*dir = '\0';
+			n = snprintf(path, size, "%s/%s", exe, LLVM_PLUGIN_NAME);
+			if (n > 0 && (size_t)n < size && !access(path, F_OK))
+				return 0;
+		}
+	}
+
+	snprintf(path, size, "%s/%s", LLVM_PLUGIN_DIR, LLVM_PLUGIN_NAME);
+	return access(path, F_OK);
+}
+
+static int llvm_load(bool verbose)
+{
+	const struct bpftool_llvm_ops *ops;
+	char path[PATH_MAX];
+	void *handle;
+
+	if (llvm_ops)
+		return 0;
+
+	if (llvm_plugin_find(path, sizeof(path))) {
+		if (verbose)
+			p_err("%s not found next to bpftool or in %s, it is required to disassemble JITed programs",
+			      LLVM_PLUGIN_NAME, LLVM_PLUGIN_DIR);
+		return -1;
+	}
+
+	handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+	if (!handle) {
+		if (verbose)
+			p_err("failed to load LLVM disassembler plugin: %s",
+			      dl_error());
+		return -1;
+	}
+
+	ops = dlsym(handle, BPFTOOL_LLVM_OPS_SYM);
+	if (!ops) {
+		if (verbose)
+			p_err("invalid LLVM disassembler plugin: %s", dl_error());
+		goto err_close;
+	}
+	if (ops->abi_version != BPFTOOL_LLVM_ABI_VERSION) {
+		if (verbose)
+			p_err("%s has ABI version %u, expected %u",
+			      path, ops->abi_version, BPFTOOL_LLVM_ABI_VERSION);
+		goto err_close;
+	}
+
+	/* Keep the plugin loaded until bpftool exits */
+	llvm_ops = ops;
+	return 0;
+
+err_close:
+	dlclose(handle);
+	return -1;
+}
+#else
+static int llvm_load(__maybe_unused bool verbose)
+{
+	llvm_ops = &bpftool_llvm_ops;
+	return 0;
+}
+#endif /* LLVM_PLUGIN_DIR */
 
 static int printf_json(char *s)
 {
@@ -63,48 +158,13 @@ static int printf_json(char *s)
 	return 0;
 }
 
-/* This callback to set the ref_type is necessary to have the LLVM disassembler
- * print PC-relative addresses instead of byte offsets for branch instruction
- * targets.
- */
-static const char *
-symbol_lookup_callback(__maybe_unused void *disasm_info,
-		       __maybe_unused uint64_t ref_value,
-		       uint64_t *ref_type, __maybe_unused uint64_t ref_PC,
-		       __maybe_unused const char **ref_name)
-{
-	*ref_type = LLVMDisassembler_ReferenceType_InOut_None;
-	return NULL;
-}
-
 static int
 init_context(disasm_ctx_t *ctx, const char *arch,
 	     __maybe_unused const char *disassembler_options,
 	     __maybe_unused unsigned char *image, __maybe_unused ssize_t len,
 	     __maybe_unused __u64 func_ksym)
 {
-	char *triple;
-
-	if (arch)
-		triple = LLVMNormalizeTargetTriple(arch);
-	else
-		triple = LLVMGetDefaultTargetTriple();
-	if (!triple) {
-		p_err("Failed to retrieve triple");
-		return -1;
-	}
-
-	/*
-	 * Enable all aarch64 ISA extensions so the disassembler can handle any
-	 * instruction the kernel JIT might emit (e.g. ARM64 LSE atomics).
-	 */
-	if (!strncmp(triple, "aarch64", 7))
-		*ctx = LLVMCreateDisasmCPUFeatures(triple, "", "+all", NULL, 0, NULL,
-						   symbol_lookup_callback);
-	else
-		*ctx = LLVMCreateDisasm(triple, NULL, 0, NULL, symbol_lookup_callback);
-	LLVMDisposeMessage(triple);
-
+	*ctx = llvm_ops->create_context(arch);
 	if (!*ctx) {
 		p_err("Failed to create disassembler");
 		return -1;
@@ -115,7 +175,7 @@ init_context(disasm_ctx_t *ctx, const char *arch,
 
 static void destroy_context(disasm_ctx_t *ctx)
 {
-	LLVMDisasmDispose(*ctx);
+	llvm_ops->destroy_context(*ctx);
 }
 
 static int
@@ -125,7 +185,7 @@ disassemble_insn(disasm_ctx_t *ctx, unsigned char *image, ssize_t len, int pc,
 	char buf[256];
 	int count;
 
-	count = LLVMDisasmInstruction(*ctx, image + pc, len - pc, func_ksym + pc,
+	count = llvm_ops->disassemble(*ctx, image + pc, len - pc, func_ksym + pc,
 				      buf, sizeof(buf));
 	if (json_output)
 		printf_json(buf);
@@ -135,12 +195,16 @@ disassemble_insn(disasm_ctx_t *ctx, unsigned char *image, ssize_t len, int pc,
 	return count;
 }
 
+bool disasm_llvm_available(void)
+{
+	return !llvm_load(false);
+}
+
 int disasm_init(void)
 {
-	LLVMInitializeAllTargetInfos();
-	LLVMInitializeAllTargetMCs();
-	LLVMInitializeAllDisassemblers();
-	return 0;
+	if (llvm_load(true))
+		return -1;
+	return llvm_ops->init();
 }
 #endif /* HAVE_LLVM_SUPPORT */
 
diff --git a/tools/bpf/bpftool/llvm_disasm.c b/tools/bpf/bpftool/llvm_disasm.c
new file mode 100644
index 000000000000..b92df1e8966f
--- /dev/null
+++ b/tools/bpf/bpftool/llvm_disasm.c
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+/*
+ * libLLVM-based disassembler for JITed programs.
+ *
+ * This file is usually built as bpftool-llvm.so, the only bpftool component
+ * linked against libLLVM, which bpftool loads only when it needs to
+ * disassemble JITed programs (see jit_disasm.c). It is linked into bpftool
+ * directly when libLLVM is a static library.
+ */
+#include <string.h>
+
+#include <llvm-c/Core.h>
+#include <llvm-c/Disassembler.h>
+#include <llvm-c/Target.h>
+#include <llvm-c/TargetMachine.h>
+
+#include "llvm_disasm.h"
+
+/*
+ * This callback to set the ref_type is necessary to have the LLVM disassembler
+ * print PC-relative addresses instead of byte offsets for branch instruction
+ * targets.
+ */
+static const char *
+symbol_lookup_callback(void *disasm_info, uint64_t ref_value,
+		       uint64_t *ref_type, uint64_t ref_PC,
+		       const char **ref_name)
+{
+	*ref_type = LLVMDisassembler_ReferenceType_InOut_None;
+	return NULL;
+}
+
+static int llvm_init(void)
+{
+	LLVMInitializeAllTargetInfos();
+	LLVMInitializeAllTargetMCs();
+	LLVMInitializeAllDisassemblers();
+	return 0;
+}
+
+static void *llvm_create_context(const char *arch)
+{
+	LLVMDisasmContextRef ctx;
+	char *triple;
+
+	if (arch)
+		triple = LLVMNormalizeTargetTriple(arch);
+	else
+		triple = LLVMGetDefaultTargetTriple();
+	if (!triple)
+		return NULL;
+
+	/*
+	 * Enable all aarch64 ISA extensions so the disassembler can handle any
+	 * instruction the kernel JIT might emit (e.g. ARM64 LSE atomics).
+	 */
+	if (!strncmp(triple, "aarch64", 7))
+		ctx = LLVMCreateDisasmCPUFeatures(triple, "", "+all", NULL, 0, NULL,
+						  symbol_lookup_callback);
+	else
+		ctx = LLVMCreateDisasm(triple, NULL, 0, NULL, symbol_lookup_callback);
+	LLVMDisposeMessage(triple);
+
+	return ctx;
+}
+
+static void llvm_destroy_context(void *ctx)
+{
+	LLVMDisasmDispose(ctx);
+}
+
+static size_t llvm_disassemble(void *ctx, uint8_t *bytes, uint64_t size,
+			       uint64_t pc, char *buf, size_t buf_size)
+{
+	return LLVMDisasmInstruction(ctx, bytes, size, pc, buf, buf_size);
+}
+
+const struct bpftool_llvm_ops bpftool_llvm_ops = {
+	.abi_version		= BPFTOOL_LLVM_ABI_VERSION,
+	.init			= llvm_init,
+	.create_context		= llvm_create_context,
+	.destroy_context	= llvm_destroy_context,
+	.disassemble		= llvm_disassemble,
+};
diff --git a/tools/bpf/bpftool/llvm_disasm.h b/tools/bpf/bpftool/llvm_disasm.h
new file mode 100644
index 000000000000..34123a80df02
--- /dev/null
+++ b/tools/bpf/bpftool/llvm_disasm.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+#ifndef __BPFTOOL_LLVM_DISASM_H
+#define __BPFTOOL_LLVM_DISASM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * Interface between bpftool and its libLLVM-based JIT disassembler.
+ *
+ * The disassembler is usually built as a plugin, bpftool-llvm.so, that
+ * bpftool loads with dlopen(); bpftool then looks up the
+ * BPFTOOL_LLVM_OPS_SYM symbol in it. The plugin can come from a different
+ * build than the bpftool binary, so bump BPFTOOL_LLVM_ABI_VERSION on any
+ * incompatible change to struct bpftool_llvm_ops, and always keep
+ * abi_version as its first member.
+ */
+#define BPFTOOL_LLVM_ABI_VERSION	1U
+#define BPFTOOL_LLVM_OPS_SYM		"bpftool_llvm_ops"
+
+struct bpftool_llvm_ops {
+	unsigned int abi_version;
+	/* Initialize libLLVM targets and disassemblers. Returns 0 on success. */
+	int (*init)(void);
+	/*
+	 * Create a disassembler context for @arch, or for the host if @arch
+	 * is NULL. Returns NULL on failure.
+	 */
+	void *(*create_context)(const char *arch);
+	void (*destroy_context)(void *ctx);
+	/*
+	 * Disassemble the instruction at @bytes, located at address @pc, into
+	 * @buf. Returns the size of the instruction, or 0 if it is invalid.
+	 */
+	size_t (*disassemble)(void *ctx, uint8_t *bytes, uint64_t size,
+			      uint64_t pc, char *buf, size_t buf_size);
+};
+
+extern const struct bpftool_llvm_ops bpftool_llvm_ops;
+
+#endif /* __BPFTOOL_LLVM_DISASM_H */
diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c
index d15fec71b64e..321e928d651c 100644
--- a/tools/bpf/bpftool/main.c
+++ b/tools/bpf/bpftool/main.c
@@ -124,7 +124,8 @@ static int do_version(int argc, char **argv)
 	const bool has_libbfd = false;
 #endif
 #ifdef HAVE_LLVM_SUPPORT
-	const bool has_llvm = true;
+	/* The LLVM disassembler may be a plugin that is not installed */
+	const bool has_llvm = disasm_llvm_available();
 #else
 	const bool has_llvm = false;
 #endif
diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
index b1eb4b5d991b..96d7b147aa9d 100644
--- a/tools/bpf/bpftool/main.h
+++ b/tools/bpf/bpftool/main.h
@@ -214,6 +214,9 @@ static inline int disasm_init(void)
 	return -1;
 }
 #endif
+#ifdef HAVE_LLVM_SUPPORT
+bool disasm_llvm_available(void);
+#endif
 void print_data_json(uint8_t *data, size_t len);
 void print_hex_data_json(uint8_t *data, size_t len);
 

-- 
2.56.0



^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency
  2026-10-29 13:05 ` [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
@ 2026-10-29 13:46   ` bot+bpf-ci
  2026-10-29 15:24   ` Alexei Starovoitov
  1 sibling, 0 replies; 5+ messages in thread
From: bot+bpf-ci @ 2026-10-29 13:46 UTC (permalink / raw)
  To: socketpair, qmo, ast, daniel, andrii, martin.lau, eddyz87,
	memxor, song, yonghong.song, jolsa, nathan, morbo, justinstitt,
	emil, ihor.solodrai, ndesaulniers
  Cc: linux-kernel, bpf, llvm, socketpair, ast, andrii, daniel,
	martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 3760 bytes --]

> bpftool: Make the LLVM disassembler an optional runtime dependency

TL;DR: Nits only: the new generated llvm-plugin-dir stamp file may need a
.gitignore entry, and the bpftool man page may need updating, since "llvm" in
version output now depends on the plugin loading at runtime.

> diff --git a/tools/bpf/bpftool/Makefile b/tools/bpf/bpftool/Makefile
> index b0f7168e7943..dc8e4be86296 100644
> --- a/tools/bpf/bpftool/Makefile
> +++ b/tools/bpf/bpftool/Makefile

[ ... ]

> @@ -290,6 +300,21 @@ $(BPFTOOL_BOOTSTRAP): $(BOOTSTRAP_OBJS) $(LIBBPF_BOOTSTRAP)
>  $(OUTPUT)bpftool: $(OBJS) $(LIBBPF)
>  	$(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
>
> +ifneq ($(LLVM_PLUGIN),)
> +all: $(LLVM_PLUGIN)
> +
> +$(LLVM_PLUGIN): CFLAGS += -fPIC
> +$(LLVM_PLUGIN): $(OUTPUT)llvm_disasm.o
> +	$(QUIET_LINK)$(CC) $(CFLAGS) $(LDFLAGS) -shared $< $(LLVM_LIBS) -o $@
> +
> +# The plugin directory, $(libdir)/bpftool, is compiled into jit_disasm.o.
> +# Recompile it if libdir changes, e.g. on "make" then "make install libdir=...".
> +$(OUTPUT)llvm-plugin-dir: FORCE
> +	$(Q)echo '$(libdir)/bpftool' | cmp -s - $@ || echo '$(libdir)/bpftool' > $@
                                                          ^^^^

The new stamp file $(OUTPUT)llvm-plugin-dir is a generated build artifact,
but it does not look like any ignore pattern matches it.

bpftool-llvm.so is covered by '*.so' and llvm_disasm.o by '*.o', but
'git check-ignore' finds no pattern for tools/bpf/bpftool/llvm-plugin-dir.

On an in-tree build with a shared libLLVM (make -C tools/bpf/bpftool with
no OUTPUT), the stamp is written to tools/bpf/bpftool/llvm-plugin-dir and
shows up as an untracked file in 'git status'.

tools/bpf/bpftool/.gitignore already lists the similar generated file
FEATURE-DUMP.bpftool.  Should this patch add '/llvm-plugin-dir' there as
well?

> +
> +$(OUTPUT)jit_disasm.o: $(OUTPUT)llvm-plugin-dir
> +endif
> +

[ ... ]

> diff --git a/tools/bpf/bpftool/main.c b/tools/bpf/bpftool/main.c
> index d15fec71b64e..321e928d651c 100644
> --- a/tools/bpf/bpftool/main.c
> +++ b/tools/bpf/bpftool/main.c
> @@ -124,7 +124,8 @@ static int do_version(int argc, char **argv)
>  	const bool has_libbfd = false;
>  #endif
>  #ifdef HAVE_LLVM_SUPPORT
> -	const bool has_llvm = true;
> +	/* The LLVM disassembler may be a plugin that is not installed */
> +	const bool has_llvm = disasm_llvm_available();
                              ^^^^

With a shared libLLVM, the "llvm" entry printed by "bpftool version" and
"-V" now depends on whether bpftool-llvm.so can be dlopen()ed at runtime
rather than on how bpftool was built.  The man page text was not updated
to match.

tools/bpf/bpftool/Documentation/common_options.rst, which every bpftool man
page includes, still says:

    Print bpftool's version number (similar to **bpftool version**), the
    number of the libbpf version in use, and optional features that were
    included when bpftool was compiled. Optional features include linking
    against LLVM or libbfd to provide the disassembler for JIT-ted programs
    (**bpftool prog dump jited**) ...

In this mode bpftool is no longer linked against LLVM at all, and
do_version() -> disasm_llvm_available() -> llvm_load(false) can fail in
llvm_plugin_find() or dlopen().  has_llvm then comes back false even though
bpftool was compiled with HAVE_LLVM_SUPPORT.

Should common_options.rst say that "llvm" is reported only when the
disassembler plugin is installed and loadable?

>  #else
>  	const bool has_llvm = false;
>  #endif


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/40881490673

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency
  2026-10-29 13:05 ` [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
  2026-10-29 13:46   ` bot+bpf-ci
@ 2026-10-29 15:24   ` Alexei Starovoitov
  1 sibling, 0 replies; 5+ messages in thread
From: Alexei Starovoitov @ 2026-10-29 15:24 UTC (permalink / raw)
  To: socketpair, Quentin Monnet, Daniel Borkmann, Andrii Nakryiko,
	Martin KaFai Lau, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Song Liu, Yonghong Song, Jiri Olsa, Nathan Chancellor,
	Bill Wendling, Justin Stitt, Emil Tsalapatis, Ihor Solodrai,
	Nick Desaulniers
  Cc: linux-kernel, bpf, llvm

On Thu, Oct 29, 2026 at 06:05 AM Mark Korenberg via B4 Relay <devnull+socketpair.gmail.com@kernel.org> wrote:
> Move the LLVM disassembler to a plugin, $(libdir)/bpftool/bpftool-llvm.so,
> that bpftool loads with dlopen() only when disassembling JITed programs.
> Distributions can then ship it as an optional package.
>
> The plugin exports a single structure of callbacks, with an ABI version
> checked by bpftool. bpftool looks for the plugin next to its own binary,
> then in its install directory, but not in the current directory since it
> usually runs as root. "bpftool version" reports "llvm" only if the plugin
> can be loaded.

This is overkill.
The plugin wraps 7 llvm-c functions and the target init calls.
bpftool can dlopen() libLLVM.so itself and dlsym() them.
perf does that for libcapstone. See
commit b5c9bcde61b8 ("perf capstone: Support for dlopen-ing libcapstone.so").

                     this patch                  dlopen(libLLVM.so)
  -----------------  --------------------------  ------------------
  installed files    bpftool, bpftool-llvm.so    bpftool
  who finds the lib  llvm_plugin_find()          ld.so
  ABI to maintain    struct bpftool_llvm_ops     none
  Makefile           libdir, llvm-plugin-dir,    one -D
                     install, uninstall rules

v1, v2 and v3 each look for the plugin in a different way.
The bpftool that the selftests build still doesn't find it.
selftests/bpf, selftests/hid, selftests/sched_ext and tools/sched_ext do:

  make -C tools/bpf/bpftool prefix= DESTDIR=<dir>/ install-bin

  bpftool           <dir>/sbin/bpftool
  plugin            <dir>/lib/bpftool/bpftool-llvm.so
  bpftool looks in  <dir>/sbin, then /lib/bpftool

"prog dump jited" fails there with this patch.
"make install" in selftests/bpf copies only bpftool. The plugin stays behind.

In the diff below llvm-config gives the name of the library at build time.
ld.so finds it the same way as today.
The distro package recommends the LLVM libs and doesn't require them.
"bpftool version" keeps "llvm" as a compile time feature.
That's what common_options.rst says.
LLVM built with BUILD_SHARED_LIBS=ON is not handled.

Something like this (not even compile tested):

diff --git a/tools/bpf/bpftool/Makefile b/tools/bpf/bpftool/Makefile
index b0f7168..7a49182 100644
--- a/tools/bpf/bpftool/Makefile
+++ b/tools/bpf/bpftool/Makefile
@@ -186,10 +186,15 @@ ifeq ($(HAS_LLVM),1)
   # Makefile includes lib.mk which sets -D_GNU_SOURCE) which would cause
   # compilation error due to redefinition. Let's filter it out here.
   CFLAGS  += $(filter-out -D_GNU_SOURCE,$(shell $(LLVM_CONFIG) --cflags))
-  LIBS    += $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
   ifeq ($(shell $(LLVM_CONFIG) --shared-mode),static)
+    LIBS += $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
     LIBS += $(shell $(LLVM_CONFIG) --system-libs $(LLVM_CONFIG_LIB_COMPONENTS))
     LIBS += -lstdc++
+  else
+    # Don't link with libLLVM.so. "prog dump jited" loads it with dlopen().
+    LIBLLVM_SO := $(shell $(LLVM_CONFIG) --libfiles $(LLVM_CONFIG_LIB_COMPONENTS))
+    CFLAGS += -DLIBLLVM_SO='"$(notdir $(realpath $(LIBLLVM_SO)))"'
+    LIBS += -ldl
   endif
   LDFLAGS += $(shell $(LLVM_CONFIG) --ldflags)
 else
diff --git a/tools/bpf/bpftool/jit_disasm.c b/tools/bpf/bpftool/jit_disasm.c
index 0454115..cca02cd 100644
--- a/tools/bpf/bpftool/jit_disasm.c
+++ b/tools/bpf/bpftool/jit_disasm.c
@@ -25,6 +25,7 @@
 #include <bpf/libbpf.h>

 #ifdef HAVE_LLVM_SUPPORT
+#include <dlfcn.h>
 #include <llvm-c/Core.h>
 #include <llvm-c/Disassembler.h>
 #include <llvm-c/Target.h>
@@ -47,6 +48,88 @@ static int oper_count;

 typedef LLVMDisasmContextRef disasm_ctx_t;

+#ifdef LIBLLVM_SO
+/*
+ * libLLVM.so is big and only "prog dump jited" needs it. bpftool is not linked
+ * with it. disasm_init() loads it.
+ */
+#define LLVM_FUNCS(F)			\
+	F(LLVMCreateDisasm)		\
+	F(LLVMCreateDisasmCPUFeatures)	\
+	F(LLVMDisasmDispose)		\
+	F(LLVMDisasmInstruction)	\
+	F(LLVMDisposeMessage)		\
+	F(LLVMGetDefaultTargetTriple)	\
+	F(LLVMNormalizeTargetTriple)
+
+#define F(fn) static __typeof__(&fn) p_##fn;
+LLVM_FUNCS(F)
+#undef F
+
+static const struct {
+	const char *name;
+	void *ptr;
+} llvm_funcs[] = {
+#define F(fn) { #fn, &p_##fn },
+	LLVM_FUNCS(F)
+#undef F
+};
+
+/* The functions that LLVMInitializeAll*() in llvm-c/Target.h call */
+static const char * const llvm_init_funcs[] = {
+#define LLVM_TARGET(t) "LLVMInitialize" #t "TargetInfo", "LLVMInitialize" #t "TargetMC",
+#include <llvm/Config/Targets.def>
+#define LLVM_DISASSEMBLER(t) "LLVMInitialize" #t "Disassembler",
+#include <llvm/Config/Disassemblers.def>
+};
+
+#define LLVMCreateDisasm		p_LLVMCreateDisasm
+#define LLVMCreateDisasmCPUFeatures	p_LLVMCreateDisasmCPUFeatures
+#define LLVMDisasmDispose		p_LLVMDisasmDispose
+#define LLVMDisasmInstruction		p_LLVMDisasmInstruction
+#define LLVMDisposeMessage		p_LLVMDisposeMessage
+#define LLVMGetDefaultTargetTriple	p_LLVMGetDefaultTargetTriple
+#define LLVMNormalizeTargetTriple	p_LLVMNormalizeTargetTriple
+
+static void *llvm_sym(void *lib, const char *name)
+{
+	void *sym = dlsym(lib, name);
+
+	if (!sym)
+		p_err("%s is not in %s", name, LIBLLVM_SO);
+	return sym;
+}
+
+int disasm_init(void)
+{
+	void (*init)(void);
+	void *lib, *sym;
+	size_t i;
+
+	lib = dlopen(LIBLLVM_SO, RTLD_NOW);
+	if (!lib) {
+		p_err("failed to load %s: %s", LIBLLVM_SO,
+		      dlerror() ?: "unknown error");
+		return -1;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(llvm_funcs); i++) {
+		sym = llvm_sym(lib, llvm_funcs[i].name);
+		if (!sym)
+			return -1;
+		*(void **)llvm_funcs[i].ptr = sym;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(llvm_init_funcs); i++) {
+		init = llvm_sym(lib, llvm_init_funcs[i]);
+		if (!init)
+			return -1;
+		init();
+	}
+	return 0;
+}
+#endif /* LIBLLVM_SO */
+
 static int printf_json(char *s)
 {
 	s = strtok(s, " \t");
@@ -135,6 +218,7 @@ disassemble_insn(disasm_ctx_t *ctx, unsigned char *image, ssize_t len, int pc,
 	return count;
 }

+#ifndef LIBLLVM_SO
 int disasm_init(void)
 {
 	LLVMInitializeAllTargetInfos();
@@ -142,6 +226,7 @@ int disasm_init(void)
 	LLVMInitializeAllDisassemblers();
 	return 0;
 }
+#endif
 #endif /* HAVE_LLVM_SUPPORT */

 #ifdef HAVE_LIBBFD_SUPPORT

Keep patch 1.

Also pls don't respin in 22 minutes only because a bot said something.

pw-bot: cr

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-29 15:24 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-29 13:05 [PATCH bpf-next v3 0/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
2026-10-29 13:05 ` [PATCH bpf-next v3 1/2] bpftool: Fix disposal of the LLVM disassembler context Mark Korenberg via B4 Relay
2026-10-29 13:05 ` [PATCH bpf-next v3 2/2] bpftool: Make the LLVM disassembler an optional runtime dependency Mark Korenberg via B4 Relay
2026-10-29 13:46   ` bot+bpf-ci
2026-10-29 15:24   ` Alexei Starovoitov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®