pw stress tests
diff --git a/.bazelrc b/.bazelrc
index dd92fd3..ab8b222 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -83,4 +83,20 @@
 # Mirrors k_common's test_tag_filters and adds -hardware (physical-board-only tests).
 # Combined into one flag to avoid the Bazel 'expanded from multiple configs' warning
 # that fires when --test_tag_filters is set by both k_common and this config.
-test:virt_ast10x0 --test_tag_filters=-integration,-do_not_build,-do_not_run_test,-kernel_doc_test,-hardware
\ No newline at end of file
+test:virt_ast10x0 --test_tag_filters=-integration,-do_not_build,-do_not_run_test,-kernel_doc_test,-hardware
+
+# Stress test configs — identical to the base configs but with --no-timeout in
+# --run_under and +stress in test_tag_filters so only stress targets are run.
+common:stress_virt_ast10x0 --platforms=//target/ast10x0
+common:stress_virt_ast10x0 --//target/ast10x0:qemu=true
+build:stress_virt_ast10x0  --build_tag_filters=-do_not_build,-kernel_doc_test
+run:stress_virt_ast10x0    --run_under="//target/ast10x0/harness:qemu_runner \
+  --no-timeout --cpu cortex-m4 --machine ast1030-evb --image "
+test:stress_virt_ast10x0   --run_under="//target/ast10x0/harness:qemu_runner \
+  --no-timeout --cpu cortex-m4 --machine ast1030-evb --image "
+common:stress_k_ast1060_evb --platforms=//target/ast10x0
+build:stress_k_ast1060_evb  --build_tag_filters=-do_not_build,-kernel_doc_test
+test:stress_k_ast1060_evb   --local_test_jobs=1
+test:stress_k_ast1060_evb   --run_under="//target/ast10x0/harness:test_runner \
+  --timeout 0 "
+test:stress_k_ast1060_evb   --test_env=AST1060_EVB_PI_HOST
diff --git a/target/ast10x0/harness/qemu_runner.py b/target/ast10x0/harness/qemu_runner.py
index 10fe61c..0ea4e46 100644
--- a/target/ast10x0/harness/qemu_runner.py
+++ b/target/ast10x0/harness/qemu_runner.py
@@ -55,6 +55,11 @@
     parser.add_argument(
         "--qemu-args", nargs="*", help="Extra arguments to pass to qemu"
     )
+    parser.add_argument(
+        "--no-timeout",
+        action="store_true",
+        help="Run indefinitely; for stress tests that never emit PASS",
+    )
     return parser.parse_args()
 
 
@@ -138,7 +143,7 @@
     result = [None]  # 0 = pass, 1 = fail, None = no sentinel found
 
     with tempfile.NamedTemporaryFile() as f:
-        with subprocess.Popen(args=qemu_args, stdout=f) as proc:
+        with subprocess.Popen(args=qemu_args, stdout=f, stdin=subprocess.DEVNULL) as proc:
             qemu_finished = threading.Event()
             sentinel_thread = threading.Thread(
                 target=_sentinel_watcher,
@@ -154,7 +159,13 @@
             stdout_thread.start()
 
             try:
-                proc.wait(timeout=TIMEOUT_SECONDS)
+                if args.no_timeout:
+                    proc.wait()
+                else:
+                    proc.wait(timeout=TIMEOUT_SECONDS)
+            except KeyboardInterrupt:
+                proc.kill()
+                proc.wait()
             except subprocess.TimeoutExpired:
                 _LOG.error(
                     "Test timed out after %ds — no sentinel detected",
diff --git a/target/ast10x0/harness/stress_image_test.bzl b/target/ast10x0/harness/stress_image_test.bzl
new file mode 100644
index 0000000..0e60d76
--- /dev/null
+++ b/target/ast10x0/harness/stress_image_test.bzl
@@ -0,0 +1,40 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+"""Bazel rule for stress test images.
+
+Stress tests loop forever and only exit on TEST_RESULT:FAIL. This rule
+produces a test target that relies on --run_under to supply the runner,
+exactly like system_image_test. Use the stress_virt_ast10x0 or
+stress_k_ast1060_evb configs which pass --no-timeout to the runner.
+"""
+
+load("@pigweed//pw_kernel/tooling:system_image.bzl", "SystemImageInfo")
+
+def _stress_image_test_impl(ctx):
+    executable_symlink = ctx.actions.declare_file(ctx.label.name)
+    ctx.actions.symlink(
+        output = executable_symlink,
+        target_file = ctx.attr.image[SystemImageInfo].elf,
+    )
+
+    return [
+        DefaultInfo(
+            executable = executable_symlink,
+            runfiles = ctx.attr.image[DefaultInfo].default_runfiles,
+        ),
+    ]
+
+stress_image_test = rule(
+    implementation = _stress_image_test_impl,
+    test = True,
+    attrs = {
+        "image": attr.label(
+            doc = "The system_image target to test.",
+            mandatory = True,
+            providers = [DefaultInfo, SystemImageInfo],
+            executable = True,
+            cfg = "target",
+        ),
+    },
+    doc = "Defines a stress test for a system_image. Use with stress_virt_ast10x0 or stress_k_ast1060_evb config.",
+)
diff --git a/target/ast10x0/tests/stress/README.md b/target/ast10x0/tests/stress/README.md
new file mode 100644
index 0000000..510e3bf
--- /dev/null
+++ b/target/ast10x0/tests/stress/README.md
@@ -0,0 +1,37 @@
+# AST10x0 Stress Tests
+
+Stress tests run forever and only exit on `TEST_RESULT:FAIL`. They are excluded
+from CI and must be run manually using one of the configs below.
+
+## Tests
+
+| Target | What it stresses |
+|---|---|
+| `stress/mutex/kernel:mutex_stress_test` | Mutex contention across three kernel threads |
+| `stress/ipc/user:ipc_stress_test` | IPC channel between initiator and handler processes |
+| `stress/process_termination/user:process_termination_stress_test` | Repeated process termination and restart |
+
+## Running on QEMU
+
+```
+# Open-ended soak — runs until Ctrl-C or FAIL, no timeout.
+# Target the system_image directly, not the _stress_test rule.
+bazel run --config=stress_virt_ast10x0 //target/ast10x0/tests/stress/mutex/kernel:mutex
+
+# Timed run — Bazel kills after 3600s if no FAIL detected.
+bazel test --config=stress_virt_ast10x0 //target/ast10x0/tests/stress/<test>:*_stress_test
+```
+
+Use `bazel run` on the `system_image` target (`:mutex`, `:ipc`, `:process_termination`)
+for open-ended soak testing — this runs QEMU directly with no wrappers.
+`bazel test` targets the `_stress_test` rule and is bounded by Bazel's 3600s ceiling.
+
+## Running on hardware (EVB via Pi fixture)
+
+```
+AST1060_EVB_PI_HOST=<pi-hostname> bazel test --config=stress_k_ast1060_evb //target/ast10x0/tests/stress/<test>
+```
+
+`bazel run` is not supported for EVB — the Pi fixture is only wired into
+`bazel test`. Tests run one at a time (`--local_test_jobs=1` is inherited from
+`k_ast1060_evb`) and have no runner timeout.
diff --git a/target/ast10x0/tests/stress/ipc/user/BUILD.bazel b/target/ast10x0/tests/stress/ipc/user/BUILD.bazel
index 2510ac1..8d8b284 100644
--- a/target/ast10x0/tests/stress/ipc/user/BUILD.bazel
+++ b/target/ast10x0/tests/stress/ipc/user/BUILD.bazel
@@ -7,6 +7,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:stress_image_test.bzl", "stress_image_test")
 
 system_image(
     name = "ipc",
@@ -27,6 +28,17 @@
     tags = ["kernel"],
 )
 
+stress_image_test(
+    name = "ipc_stress_test",
+    image = ":ipc",
+    tags = [
+        "kernel",
+        "do_not_run_test",
+    ],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    timeout = "eternal",
+)
+
 filegroup(
     name = "system_config",
     srcs = ["system.json5"],
diff --git a/target/ast10x0/tests/stress/mutex/kernel/BUILD.bazel b/target/ast10x0/tests/stress/mutex/kernel/BUILD.bazel
index adc3068..a0bddda 100644
--- a/target/ast10x0/tests/stress/mutex/kernel/BUILD.bazel
+++ b/target/ast10x0/tests/stress/mutex/kernel/BUILD.bazel
@@ -6,6 +6,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:stress_image_test.bzl", "stress_image_test")
 
 system_image(
     name = "mutex",
@@ -23,6 +24,17 @@
     tags = ["kernel"],
 )
 
+stress_image_test(
+    name = "mutex_stress_test",
+    image = ":mutex",
+    tags = [
+        "kernel",
+        "do_not_run_test",
+    ],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    timeout = "eternal",
+)
+
 filegroup(
     name = "system_config",
     srcs = ["system.json5"],
diff --git a/target/ast10x0/tests/stress/process_termination/user/BUILD.bazel b/target/ast10x0/tests/stress/process_termination/user/BUILD.bazel
index 0a30fba..d32e72a 100644
--- a/target/ast10x0/tests/stress/process_termination/user/BUILD.bazel
+++ b/target/ast10x0/tests/stress/process_termination/user/BUILD.bazel
@@ -7,6 +7,7 @@
 load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
 load("@rules_rust//rust:defs.bzl", "rust_binary")
 load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target/ast10x0/harness:stress_image_test.bzl", "stress_image_test")
 
 system_image(
     name = "process_termination",
@@ -27,6 +28,17 @@
     tags = ["kernel"],
 )
 
+stress_image_test(
+    name = "process_termination_stress_test",
+    image = ":process_termination",
+    tags = [
+        "kernel",
+        "do_not_run_test",
+    ],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    timeout = "eternal",
+)
+
 filegroup(
     name = "system_config",
     srcs = ["system.json5"],
diff --git a/workflows.json b/workflows.json
index d1f1919..5d9ca2a 100644
--- a/workflows.json
+++ b/workflows.json
@@ -129,7 +129,7 @@
         "args": [
           "--keep_going",
           "--config=virt_ast10x0",
-          "--test_tag_filters=-hardware",
+          "--test_tag_filters=-hardware,-do_not_run_test",
           "--test_output=streamed"
         ],
         "driver_options": {