diff --git a/Cargo.toml b/Cargo.toml
index f765597f..f3cee151 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
     "aya-log-parser",
     "aya-obj",
     "aya-tool",
+    "ebpf-panic",
     "test-distro",
     "test/integration-common",
     "test/integration-test",
@@ -33,6 +34,7 @@ default-members = [
     "aya-log-parser",
     "aya-obj",
     "aya-tool",
+    "ebpf-panic",
     "test-distro",
     "test/integration-common",
     # test/integration-test is omitted; including it in this list causes `cargo test` to run its
diff --git a/clippy.sh b/clippy.sh
index b690f988..62de2bb5 100755
--- a/clippy.sh
+++ b/clippy.sh
@@ -2,11 +2,6 @@
 
 set -eux
 
-# We cannot run clippy over the whole workspace at once due to feature unification. Since both
-# integration-test and integration-ebpf depend on integration-common and integration-test activates
-# integration-common's aya dependency, we end up trying to compile the panic handler twice: once
-# from the bpf program, and again from std via aya.
-# 
 # `-C panic=abort` because "unwinding panics are not supported without std"; integration-ebpf
 # contains `#[no_std]` binaries.
 # 
@@ -16,5 +11,4 @@ set -eux
 # unwinding behavior.
 # 
 # `+nightly` because "the option `Z` is only accepted on the nightly compiler".
-cargo +nightly hack clippy "$@" --exclude integration-ebpf --all-targets --feature-powerset --workspace -- --deny warnings
-cargo +nightly hack clippy "$@" --package integration-ebpf --all-targets --feature-powerset -- --deny warnings -C panic=abort -Zpanic_abort_tests
+cargo +nightly hack clippy "$@" --all-targets --feature-powerset -- --deny warnings -C panic=abort -Zpanic_abort_tests
diff --git a/ebpf-panic/Cargo.toml b/ebpf-panic/Cargo.toml
new file mode 100644
index 00000000..eb78e6f2
--- /dev/null
+++ b/ebpf-panic/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "ebpf-panic"
+publish = false
+version = "1.0.0"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
diff --git a/ebpf-panic/src/lib.rs b/ebpf-panic/src/lib.rs
new file mode 100644
index 00000000..4b4d86e2
--- /dev/null
+++ b/ebpf-panic/src/lib.rs
@@ -0,0 +1,33 @@
+//! A panic handler for eBPF rust targets.
+//!
+//! Panics are not supported in the eBPF rust targets however since crates for
+//! the eBPF targets are no_std they must provide a panic handler. This crate
+//! provides a panic handler that loops forever. Such a function, if called,
+//! will cause the program to be rejected by the eBPF verifier with an error
+//! message similar to:
+//!
+//! ```text
+//! last insn is not an exit or jmp
+//! ```
+//!
+//! # Example
+//!
+//! ```ignore
+//! #![no_std]
+//!
+//! use aya_ebpf::{macros::tracepoint, programs::TracePointContext};
+//! #[cfg(not(test))]
+//! extern crate ebpf_panic;
+//!
+//! #[tracepoint]
+//! pub fn test_tracepoint_one(_ctx: TracePointContext) -> u32 {
+//!     0
+//! }
+//! ```
+#![no_std]
+
+#[cfg(not(test))]
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml
index ed16b476..8f3eb3de 100644
--- a/test/integration-ebpf/Cargo.toml
+++ b/test/integration-ebpf/Cargo.toml
@@ -16,6 +16,7 @@ workspace = true
 [dependencies]
 aya-ebpf = { path = "../../ebpf/aya-ebpf" }
 aya-log-ebpf = { path = "../../ebpf/aya-log-ebpf" }
+ebpf-panic = { path = "../../ebpf-panic" }
 integration-common = { path = "../integration-common" }
 network-types = { workspace = true }
 
diff --git a/test/integration-ebpf/src/bpf_probe_read.rs b/test/integration-ebpf/src/bpf_probe_read.rs
index a721163c..2a622a25 100644
--- a/test/integration-ebpf/src/bpf_probe_read.rs
+++ b/test/integration-ebpf/src/bpf_probe_read.rs
@@ -8,6 +8,8 @@ use aya_ebpf::{
     programs::ProbeContext,
 };
 use integration_common::bpf_probe_read::{RESULT_BUF_LEN, TestResult};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 fn read_str_bytes(
     fun: unsafe fn(*const u8, &mut [u8]) -> Result<&[u8], i64>,
@@ -71,9 +73,3 @@ pub fn test_bpf_probe_read_kernel_str_bytes(ctx: ProbeContext) {
         ctx.arg::<usize>(0),
     );
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/log.rs b/test/integration-ebpf/src/log.rs
index b6471f3f..67228b53 100644
--- a/test/integration-ebpf/src/log.rs
+++ b/test/integration-ebpf/src/log.rs
@@ -5,6 +5,8 @@ use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 
 use aya_ebpf::{macros::uprobe, programs::ProbeContext};
 use aya_log_ebpf::{debug, error, info, trace, warn};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[uprobe]
 pub fn test_log(ctx: ProbeContext) {
@@ -81,9 +83,3 @@ pub fn test_log(ctx: ProbeContext) {
         debug!(&ctx, "{:x}", no_copy.consume());
     }
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/map_test.rs b/test/integration-ebpf/src/map_test.rs
index e53b2f85..6eaf4123 100644
--- a/test/integration-ebpf/src/map_test.rs
+++ b/test/integration-ebpf/src/map_test.rs
@@ -9,6 +9,8 @@ use aya_ebpf::{
     maps::{Array, HashMap},
     programs::SkBuffContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 // Introduced in kernel v3.19.
 #[map]
@@ -35,9 +37,3 @@ pub fn simple_prog(_ctx: SkBuffContext) -> i64 {
 
     0
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/memmove_test.rs b/test/integration-ebpf/src/memmove_test.rs
index f9002131..afadfc09 100644
--- a/test/integration-ebpf/src/memmove_test.rs
+++ b/test/integration-ebpf/src/memmove_test.rs
@@ -9,6 +9,8 @@ use aya_ebpf::{
     maps::HashMap,
     programs::XdpContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 use network_types::{
     eth::{EthHdr, EtherType},
     ip::Ipv6Hdr,
@@ -53,9 +55,3 @@ fn try_do_dnat(ctx: XdpContext) -> Result<u32, ()> {
     }
     Ok(xdp_action::XDP_PASS)
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/name_test.rs b/test/integration-ebpf/src/name_test.rs
index d1b48950..12d0d3f2 100644
--- a/test/integration-ebpf/src/name_test.rs
+++ b/test/integration-ebpf/src/name_test.rs
@@ -2,6 +2,8 @@
 #![no_main]
 
 use aya_ebpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[xdp]
 pub fn ihaveaverylongname(ctx: XdpContext) -> u32 {
@@ -14,9 +16,3 @@ pub fn ihaveaverylongname(ctx: XdpContext) -> u32 {
 unsafe fn try_pass(_ctx: XdpContext) -> Result<u32, u32> {
     Ok(xdp_action::XDP_PASS)
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/pass.rs b/test/integration-ebpf/src/pass.rs
index 795d82b0..63afc45d 100644
--- a/test/integration-ebpf/src/pass.rs
+++ b/test/integration-ebpf/src/pass.rs
@@ -2,6 +2,8 @@
 #![no_main]
 
 use aya_ebpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 // Note: the `frags` attribute causes this probe to be incompatible with kernel versions < 5.18.0.
 // See https://github.com/torvalds/linux/commit/c2f2cdb.
@@ -16,9 +18,3 @@ pub fn pass(ctx: XdpContext) -> u32 {
 unsafe fn try_pass(_ctx: XdpContext) -> Result<u32, u32> {
     Ok(xdp_action::XDP_PASS)
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/raw_tracepoint.rs b/test/integration-ebpf/src/raw_tracepoint.rs
index 1db9a21b..513d4ab5 100644
--- a/test/integration-ebpf/src/raw_tracepoint.rs
+++ b/test/integration-ebpf/src/raw_tracepoint.rs
@@ -6,6 +6,8 @@ use aya_ebpf::{
     maps::Array,
     programs::RawTracePointContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 use integration_common::raw_tracepoint::SysEnterEvent;
 
 #[map]
@@ -25,9 +27,3 @@ pub fn sys_enter(ctx: RawTracePointContext) -> i32 {
 
     0
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/redirect.rs b/test/integration-ebpf/src/redirect.rs
index eb32f999..b559c06d 100644
--- a/test/integration-ebpf/src/redirect.rs
+++ b/test/integration-ebpf/src/redirect.rs
@@ -7,6 +7,8 @@ use aya_ebpf::{
     maps::{Array, CpuMap, DevMap, DevMapHash, XskMap},
     programs::XdpContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[map]
 static SOCKS: XskMap = XskMap::with_max_entries(1, 0);
@@ -74,9 +76,3 @@ fn inc_hit(index: u32) {
         unsafe { *hit += 1 };
     }
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/relocations.rs b/test/integration-ebpf/src/relocations.rs
index dc9e71ca..03b0d342 100644
--- a/test/integration-ebpf/src/relocations.rs
+++ b/test/integration-ebpf/src/relocations.rs
@@ -8,6 +8,8 @@ use aya_ebpf::{
     maps::Array,
     programs::ProbeContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[map]
 static RESULTS: Array<u64> = Array::with_max_entries(3, 0);
@@ -38,9 +40,3 @@ fn set_result(index: u32, value: u64) {
 fn set_result_backward(index: u32, value: u64) {
     set_result(index, value);
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/ring_buf.rs b/test/integration-ebpf/src/ring_buf.rs
index 6674f5e6..e8825a8c 100644
--- a/test/integration-ebpf/src/ring_buf.rs
+++ b/test/integration-ebpf/src/ring_buf.rs
@@ -7,6 +7,8 @@ use aya_ebpf::{
     programs::ProbeContext,
 };
 use integration_common::ring_buf::Registers;
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[map]
 static RING_BUF: RingBuf = RingBuf::with_byte_size(0, 0);
@@ -45,9 +47,3 @@ pub fn ring_buf_test(ctx: ProbeContext) {
         entry.discard(0);
     }
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/simple_prog.rs b/test/integration-ebpf/src/simple_prog.rs
index 98725dce..ccb2e24e 100644
--- a/test/integration-ebpf/src/simple_prog.rs
+++ b/test/integration-ebpf/src/simple_prog.rs
@@ -5,15 +5,11 @@
 #![no_main]
 
 use aya_ebpf::{macros::socket_filter, programs::SkBuffContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 // Introduced in kernel v3.19.
 #[socket_filter]
 pub fn simple_prog(_ctx: SkBuffContext) -> i64 {
     0
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/strncmp.rs b/test/integration-ebpf/src/strncmp.rs
index a19b10d2..9b831157 100644
--- a/test/integration-ebpf/src/strncmp.rs
+++ b/test/integration-ebpf/src/strncmp.rs
@@ -9,6 +9,8 @@ use aya_ebpf::{
     programs::ProbeContext,
 };
 use integration_common::strncmp::TestResult;
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[map]
 static RESULT: Array<TestResult> = Array::with_max_entries(1, 0);
@@ -26,9 +28,3 @@ pub fn test_bpf_strncmp(ctx: ProbeContext) -> Result<(), c_long> {
 
     Ok(())
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/tcx.rs b/test/integration-ebpf/src/tcx.rs
index 5ed3c211..0d173cd5 100644
--- a/test/integration-ebpf/src/tcx.rs
+++ b/test/integration-ebpf/src/tcx.rs
@@ -2,14 +2,10 @@
 #![no_main]
 
 use aya_ebpf::{bindings::tcx_action_base::TCX_NEXT, macros::classifier, programs::TcContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[classifier]
 pub fn tcx_next(_ctx: TcContext) -> i32 {
     TCX_NEXT
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/test.rs b/test/integration-ebpf/src/test.rs
index 417da0de..2dc736d3 100644
--- a/test/integration-ebpf/src/test.rs
+++ b/test/integration-ebpf/src/test.rs
@@ -8,6 +8,8 @@ use aya_ebpf::{
         FlowDissectorContext, ProbeContext, RetProbeContext, TracePointContext, XdpContext,
     },
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[xdp]
 pub fn pass(ctx: XdpContext) -> u32 {
@@ -52,9 +54,3 @@ pub fn test_flow(_ctx: FlowDissectorContext) -> u32 {
     // Linux kernel for inspiration.
     bpf_ret_code::BPF_FLOW_DISSECTOR_CONTINUE
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/two_progs.rs b/test/integration-ebpf/src/two_progs.rs
index 17d08168..98da3fce 100644
--- a/test/integration-ebpf/src/two_progs.rs
+++ b/test/integration-ebpf/src/two_progs.rs
@@ -4,6 +4,8 @@
 #![no_main]
 
 use aya_ebpf::{macros::tracepoint, programs::TracePointContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[tracepoint]
 pub fn test_tracepoint_one(_ctx: TracePointContext) -> u32 {
@@ -13,9 +15,3 @@ pub fn test_tracepoint_one(_ctx: TracePointContext) -> u32 {
 pub fn test_tracepoint_two(_ctx: TracePointContext) -> u32 {
     0
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/uprobe_cookie.rs b/test/integration-ebpf/src/uprobe_cookie.rs
index 19713f85..56961aff 100644
--- a/test/integration-ebpf/src/uprobe_cookie.rs
+++ b/test/integration-ebpf/src/uprobe_cookie.rs
@@ -7,6 +7,8 @@ use aya_ebpf::{
     maps::RingBuf,
     programs::ProbeContext,
 };
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 #[map]
 static RING_BUF: RingBuf = RingBuf::with_byte_size(0, 0);
@@ -17,9 +19,3 @@ pub fn uprobe_cookie(ctx: ProbeContext) {
     let cookie_bytes = cookie.to_le_bytes();
     let _res = RING_BUF.output(&cookie_bytes, 0);
 }
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
diff --git a/test/integration-ebpf/src/xdp_sec.rs b/test/integration-ebpf/src/xdp_sec.rs
index c9eed920..56fd586f 100644
--- a/test/integration-ebpf/src/xdp_sec.rs
+++ b/test/integration-ebpf/src/xdp_sec.rs
@@ -2,6 +2,8 @@
 #![no_main]
 
 use aya_ebpf::{bindings::xdp_action::XDP_PASS, macros::xdp, programs::XdpContext};
+#[cfg(not(test))]
+extern crate ebpf_panic;
 
 macro_rules! probe {
     ($name:ident, ($($arg:ident $(= $value:literal)?),*) ) => {
@@ -18,9 +20,3 @@ probe!(xdp_cpumap, (map = "cpumap"));
 probe!(xdp_devmap, (map = "devmap"));
 probe!(xdp_frags_cpumap, (frags, map = "cpumap"));
 probe!(xdp_frags_devmap, (frags, map = "devmap"));
-
-#[cfg(not(test))]
-#[panic_handler]
-fn panic(_info: &core::panic::PanicInfo) -> ! {
-    loop {}
-}