diff --git a/include/linux/btf.h b/include/linux/btf.h
index 456bd8f..ef66d2e 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -177,4 +177,14 @@ static inline const char *btf_name_by_offset(const struct btf *btf,
 }
 #endif
 
+/* Following function used for testing BTF-based printk-family support */
+#ifdef CONFIG_BTF_PRINTF
+const char *btf_vmlinux_next_type_name(u8 kind, s32 *id);
+#else
+static inline const char *btf_vmlinux_next_type_name(u8 kind, s32 *id)
+{
+	return NULL;
+}
+#endif /* CONFIG_BTF_PRINTF */
+
 #endif
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index ae453f0..0703d1d 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -4867,3 +4867,38 @@ u32 btf_id(const struct btf *btf)
 {
 	return btf->id;
 }
+
+#ifdef CONFIG_BTF_PRINTF
+/*
+ * btf_vmlinux_next_type_name():  used in test_printf.c to
+ * iterate over types for testing.
+ * Exported as test_printf can be built as a module.
+ *
+ * @kind: BTF_KIND_* value
+ * @id: pointer to last id; value/result argument. When next
+ *      type name is found, we set *id to associated id.
+ * Returns:
+ *	Next type name, sets *id to associated id.
+ */
+const char *btf_vmlinux_next_type_name(u8 kind, s32 *id)
+{
+	const struct btf *btf = bpf_get_btf_vmlinux();
+	const struct btf_type *t;
+	const char *name;
+
+	if (!btf || !id)
+		return NULL;
+
+	for ((*id)++; *id <= btf->nr_types; (*id)++) {
+		t = btf->types[*id];
+		if (BTF_INFO_KIND(t->info) != kind)
+			continue;
+		name = btf_name_by_offset(btf, t->name_off);
+		if (name && strlen(name) > 0)
+			return name;
+	}
+
+	return NULL;
+}
+EXPORT_SYMBOL_GPL(btf_vmlinux_next_type_name);
+#endif /* CONFIG_BTF_PRINTF */
diff --git a/lib/test_printf.c b/lib/test_printf.c
index 2d9f520..9743e96 100644
--- a/lib/test_printf.c
+++ b/lib/test_printf.c
@@ -23,6 +23,9 @@
 #include <linux/mm.h>
 
 #include <linux/property.h>
+#include <linux/bpf.h>
+#include <linux/btf.h>
+#include <linux/skbuff.h>
 
 #include "../tools/testing/selftests/kselftest_module.h"
 
@@ -644,6 +647,120 @@ static void __init fwnode_pointer(void)
 #endif
 }
 
+#define	__TEST_BTF(type, var, expected)	test(expected, "%pT<"#type">", &var)
+
+#define TEST_BTF(type, var, ...)					\
+	do {								\
+		type var = __VA_ARGS__;					\
+		pr_debug("type %s: %pT<" #type ">", #type, &var);	\
+		__TEST_BTF(type, var, #__VA_ARGS__);			\
+	} while (0)
+
+#define	BTF_MAX_DATA_SIZE	8192
+#define	BTF_MAX_BUF_SIZE	(BTF_MAX_DATA_SIZE * 8)
+
+static void __init
+btf_print_kind(u8 kind, const char *kind_name)
+{
+	char fmt1[256], fmt2[256];
+	int res1, res2, res3;
+	const char *name;
+	u64 *dummy_data;
+	s32 id = 0;
+	char *buf;
+
+	dummy_data = kzalloc(BTF_MAX_DATA_SIZE, GFP_KERNEL);
+	buf = kzalloc(BTF_MAX_BUF_SIZE, GFP_KERNEL);
+	for (;;) {
+		name = btf_vmlinux_next_type_name(kind, &id);
+		if (!name)
+			break;
+
+		total_tests++;
+
+		strncpy(fmt1, "%pT<", sizeof(fmt1));
+		strncat(fmt1, kind_name, sizeof(fmt1));
+		strncat(fmt1, name, sizeof(fmt1));
+		strncat(fmt1, ">", sizeof(fmt1));
+
+		strncpy(fmt2, "%pTN<", sizeof(fmt2));
+		strncat(fmt2, kind_name, sizeof(fmt2));
+		strncat(fmt2, name, sizeof(fmt2));
+		strncat(fmt2, ">", sizeof(fmt2));
+
+		res1 = snprintf(buf, BTF_MAX_BUF_SIZE, fmt1, dummy_data);
+		res2 = snprintf(buf, 0, fmt1, dummy_data);
+		res3 = snprintf(buf, BTF_MAX_BUF_SIZE, fmt2, dummy_data);
+
+		/*
+		 * Ensure return value is > 0 and identical irrespective
+		 * of whether we pass in a big enough buffer;
+		 * also ensure that printing names always results in as
+		 * long/longer buffer length.
+		 */
+		if (res1 <= 0 || res2 <= 0 || res3 <= 0) {
+			pr_warn("snprintf(%s%s); %d <= 0",
+				kind_name, name,
+				res1 <= 0 ? res1 : res2 <= 0 ? res2 : res3);
+			failed_tests++;
+		} else if (res1 != res2) {
+			pr_warn("snprintf(%s%s): %d != %d",
+				kind_name, name, res1, res2);
+			failed_tests++;
+		} else if (res3 < res2) {
+			pr_warn("snprintf(%s%s); %d < %d",
+				kind_name, name, res3, res2);
+			failed_tests++;
+		} else {
+			pr_debug("Printed %s%s (%d bytes, %d bytes with names)",
+				 kind_name, name, res1, res3);
+		}
+	}
+	kfree(dummy_data);
+	kfree(buf);
+}
+
+static void __init
+btf_pointer(void)
+{
+	struct sk_buff *skb = alloc_skb(64, GFP_KERNEL);
+#ifdef CONFIG_BTF_PRINTF
+	TEST_BTF(int, testint, 0);
+	TEST_BTF(int, testint, 1234);
+	TEST_BTF(int, testint, -4567);
+	TEST_BTF(bool, testbool, 0);
+	TEST_BTF(bool, testbool, 1);
+	TEST_BTF(int64_t, testint64, 0);
+	TEST_BTF(int64_t, testint64, 1234);
+	TEST_BTF(int64_t, testint64, -4567);
+	TEST_BTF(char, testchar, 100);
+	TEST_BTF(enum bpf_arg_type, testenum, ARG_CONST_MAP_PTR);
+#endif /* CONFIG_BTF_PRINTF */
+
+	/*
+	 * Iterate every instance of each kind, printing each associated type.
+	 * This constitutes around 10k tests.
+	 */
+	btf_print_kind(BTF_KIND_STRUCT, "struct ");
+	btf_print_kind(BTF_KIND_UNION, "union ");
+	btf_print_kind(BTF_KIND_ENUM, "enum ");
+	btf_print_kind(BTF_KIND_TYPEDEF, "");
+
+	/* verify unknown type falls back to hashed pointer display */
+	test_hashed("%pT<unknown_type>", NULL);
+	test_hashed("%pT<unknown_type>", skb);
+
+	/* verify use of unknown modifier X returns error string */
+	test("(%pT?)<unknown_type>", "%pTX<unknown_type>", skb);
+
+	/* No space separation is allowed other than for struct|enum|union */
+	test("(%pT?)", "%pT<invalid format>", skb);
+	/* Missing ">" format error */
+	test("(%pT?)", "%pT<struct sk_buff", skb);
+
+	kfree_skb(skb);
+}
+
 static void __init
 test_pointer(void)
 {
@@ -668,6 +785,7 @@ static void __init fwnode_pointer(void)
 	flags();
 	errptr();
 	fwnode_pointer();
+	btf_pointer();
 }
 
 static void __init selftest(void)
