diff --git a/include/linux/btf.h b/include/linux/btf.h
index 4c200f5..688786a 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -214,6 +214,14 @@ static inline const struct btf_var_secinfo *btf_type_var_secinfo(
 const char *btf_name_by_offset(const struct btf *btf, u32 offset);
 struct btf *btf_parse_vmlinux(void);
 struct btf *bpf_prog_get_target_btf(const struct bpf_prog *prog);
+#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
+struct btf *bpf_get_btf_module(__u32 obj_id);
+#else
+static inline struct btf *bpf_get_btf_module(__u32 obj_id)
+{
+	return ERR_PTR(-ENOTSUPP);
+}
+#endif
 #else
 static inline const struct btf_type *btf_type_by_id(const struct btf *btf,
 						    u32 type_id)
@@ -225,6 +233,10 @@ static inline const char *btf_name_by_offset(const struct btf *btf,
 {
 	return NULL;
 }
+static inline struct btf *bpf_get_btf_module(__u32 obj_id)
+{
+	return ERR_PTR(-ENOTSUPP);
+}
 #endif
 
 #endif
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 1233f14..ccb75299 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -3641,7 +3641,9 @@ struct bpf_stack_build_id {
  *		the pointer data is carried out to avoid kernel crashes during
  *		operation.  Smaller types can use string space on the stack;
  *		larger programs can use map data to store the string
- *		representation.
+ *		representation.  Module-specific data structures can be
+ *		displayed if the module BTF object id is supplied in the
+ *		*ptr*->obj_id field.
  *
  *		The string can be subsequently shared with userspace via
  *		bpf_perf_event_output() or ring buffer interfaces.
@@ -5115,15 +5117,14 @@ struct bpf_sk_lookup {
 /*
  * struct btf_ptr is used for typed pointer representation; the
  * type id is used to render the pointer data as the appropriate type
- * via the bpf_snprintf_btf() helper described above.  A flags field -
- * potentially to specify additional details about the BTF pointer
- * (rather than its mode of display) - is included for future use.
- * Display flags - BTF_F_* - are passed to bpf_snprintf_btf separately.
+ * via the bpf_snprintf_btf() helper described above.  The obj_id
+ * is used to specify an object id (such as a module); if unset
+ * a core vmlinux type id is assumed.
  */
 struct btf_ptr {
 	void *ptr;
 	__u32 type_id;
-	__u32 flags;		/* BTF ptr flags; unused at present. */
+	__u32 obj_id;		/* BTF object; vmlinux if 0 */
 };
 
 /*
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 333f41c..8ee691e 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -5777,6 +5777,24 @@ struct btf_module {
 	return len;
 }
 
+struct btf *bpf_get_btf_module(__u32 obj_id)
+{
+	struct btf *btf = ERR_PTR(-ENOENT);
+	struct btf_module *btf_mod;
+
+	rcu_read_lock();
+	list_for_each_entry_rcu(btf_mod, &btf_modules, list) {
+		if (!btf_mod->btf || obj_id != btf_mod->btf->id)
+			continue;
+
+		refcount_inc(&btf_mod->btf->refcnt);
+		btf = btf_mod->btf;
+		break;
+	}
+	rcu_read_unlock();
+	return btf;
+}
+
 static void btf_module_free(struct rcu_head *rcu)
 {
 	struct btf_module *btf_mod = container_of(rcu, struct btf_module, rcu);
diff --git a/kernel/trace/bpf_trace.c b/kernel/trace/bpf_trace.c
index 23a390a..66d4120 100644
--- a/kernel/trace/bpf_trace.c
+++ b/kernel/trace/bpf_trace.c
@@ -75,8 +75,8 @@ static struct bpf_raw_event_map *bpf_get_raw_tracepoint_module(const char *name)
 u64 bpf_get_stack(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5);
 
 static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
-				  u64 flags, const struct btf **btf,
-				  s32 *btf_id);
+				  u64 flags, struct btf **btf,
+				  bool *btf_is_vmlinux, s32 *btf_id);
 
 /**
  * trace_call_bpf - invoke BPF program
@@ -786,15 +786,22 @@ struct bpf_seq_printf_buf {
 BPF_CALL_4(bpf_seq_printf_btf, struct seq_file *, m, struct btf_ptr *, ptr,
 	   u32, btf_ptr_size, u64, flags)
 {
-	const struct btf *btf;
+	bool btf_is_vmlinux;
+	struct btf *btf;
 	s32 btf_id;
 	int ret;
 
-	ret = bpf_btf_printf_prepare(ptr, btf_ptr_size, flags, &btf, &btf_id);
+	ret = bpf_btf_printf_prepare(ptr, btf_ptr_size, flags, &btf,
+				     &btf_is_vmlinux, &btf_id);
 	if (ret)
 		return ret;
 
-	return btf_type_seq_show_flags(btf, btf_id, ptr->ptr, m, flags);
+	ret = btf_type_seq_show_flags(btf, btf_id, ptr->ptr, m, flags);
+	/* modules refcount their BTF */
+	if (!btf_is_vmlinux)
+		btf_put(btf);
+
+	return ret;
 }
 
 static const struct bpf_func_proto bpf_seq_printf_btf_proto = {
@@ -1205,7 +1212,8 @@ static bool bpf_d_path_allowed(const struct bpf_prog *prog)
 			 BTF_F_PTR_RAW | BTF_F_ZERO)
 
 static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
-				  u64 flags, const struct btf **btf,
+				  u64 flags, struct btf **btf,
+				  bool *btf_is_vmlinux,
 				  s32 *btf_id)
 {
 	const struct btf_type *t;
@@ -1216,7 +1224,14 @@ static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
 	if (btf_ptr_size != sizeof(struct btf_ptr))
 		return -EINVAL;
 
-	*btf = bpf_get_btf_vmlinux();
+	*btf_is_vmlinux = false;
+
+	if (ptr->obj_id > 0)
+		*btf = bpf_get_btf_module(ptr->obj_id);
+	if (ptr->obj_id == 0 || IS_ERR(*btf)) {
+		*btf = bpf_get_btf_vmlinux();
+		*btf_is_vmlinux = true;
+	}
 
 	if (IS_ERR_OR_NULL(*btf))
 		return PTR_ERR(*btf);
@@ -1237,16 +1252,23 @@ static int bpf_btf_printf_prepare(struct btf_ptr *ptr, u32 btf_ptr_size,
 BPF_CALL_5(bpf_snprintf_btf, char *, str, u32, str_size, struct btf_ptr *, ptr,
 	   u32, btf_ptr_size, u64, flags)
 {
-	const struct btf *btf;
+	bool btf_is_vmlinux;
+	struct btf *btf;
 	s32 btf_id;
 	int ret;
 
-	ret = bpf_btf_printf_prepare(ptr, btf_ptr_size, flags, &btf, &btf_id);
+	ret = bpf_btf_printf_prepare(ptr, btf_ptr_size, flags, &btf,
+				     &btf_is_vmlinux, &btf_id);
 	if (ret)
 		return ret;
 
-	return btf_type_snprintf_show(btf, btf_id, ptr->ptr, str, str_size,
-				      flags);
+	ret = btf_type_snprintf_show(btf, btf_id, ptr->ptr, str, str_size,
+				     flags);
+	/* modules refcount their BTF */
+	if (!btf_is_vmlinux)
+		btf_put(btf);
+
+	return ret;
 }
 
 const struct bpf_func_proto bpf_snprintf_btf_proto = {
diff --git a/tools/include/uapi/linux/bpf.h b/tools/include/uapi/linux/bpf.h
index 1233f14..ccb75299 100644
--- a/tools/include/uapi/linux/bpf.h
+++ b/tools/include/uapi/linux/bpf.h
@@ -3641,7 +3641,9 @@ struct bpf_stack_build_id {
  *		the pointer data is carried out to avoid kernel crashes during
  *		operation.  Smaller types can use string space on the stack;
  *		larger programs can use map data to store the string
- *		representation.
+ *		representation.  Module-specific data structures can be
+ *		displayed if the module BTF object id is supplied in the
+ *		*ptr*->obj_id field.
  *
  *		The string can be subsequently shared with userspace via
  *		bpf_perf_event_output() or ring buffer interfaces.
@@ -5115,15 +5117,14 @@ struct bpf_sk_lookup {
 /*
  * struct btf_ptr is used for typed pointer representation; the
  * type id is used to render the pointer data as the appropriate type
- * via the bpf_snprintf_btf() helper described above.  A flags field -
- * potentially to specify additional details about the BTF pointer
- * (rather than its mode of display) - is included for future use.
- * Display flags - BTF_F_* - are passed to bpf_snprintf_btf separately.
+ * via the bpf_snprintf_btf() helper described above.  The obj_id
+ * is used to specify an object id (such as a module); if unset
+ * a core vmlinux type id is assumed.
  */
 struct btf_ptr {
 	void *ptr;
 	__u32 type_id;
-	__u32 flags;		/* BTF ptr flags; unused at present. */
+	__u32 obj_id;		/* BTF object; vmlinux if 0 */
 };
 
 /*
