Skip to content

Harden Stratum V1 input parsing and work generation - #1849

Open
Distortions81 wants to merge 3 commits into
bitaxeorg:masterfrom
Distortions81:fix/stratum-v1-input-hardening
Open

Harden Stratum V1 input parsing and work generation#1849
Distortions81 wants to merge 3 commits into
bitaxeorg:masterfrom
Distortions81:fix/stratum-v1-input-hardening

Conversation

@Distortions81

@Distortions81 Distortions81 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Treat Stratum V1 pool messages and derived coinbase/work data as untrusted input through receive, parse, and job generation.

  • Preserve fragmented and consecutive lines while bounding JSON-RPC messages at 16 KiB.
  • Strictly validate message shape, numeric/string/hex widths, extranonce, and Merkle counts.
  • Make coinbase decoding and varints length-aware, including exact lock-time framing.
  • Check all work-size arithmetic and allocation failures.
  • Do not update counters/state or queue work after decode failure.
  • Cover malformed, truncated, oversized, embedded-NUL, and exact-boundary inputs.

Why

Pool-controlled data previously reached conversions, allocations, and transaction decoding with limited structural validation. This could cause invalid reads, unsafe conversions, null dereferences, or excessive allocation. The trusted-pool/network design is unchanged; no authentication is added.

Review follow-up

Incorporates @johnny9's findings: require exactly four lock-time bytes, gate every work side effect on decode success, and add the proposed maximum-line/successor, embedded-NUL, 32-byte extranonce, and NULL/empty-input regressions.

Related work

Related to #1844, which takes a smaller receive/parser-focused approach. This draft also hardens coinbase decoding, binary Merkle handling, work arithmetic, cleanup, and task failure propagation.

Validation

  • ESP-IDF 6.0.2 full firmware and ESP32-S3 test images built
  • Exact CI QEMU environment: 84 passed, 0 failed
  • Every review-added regression confirmed executed
  • AxeOS production build passed with Node.js 22
  • git diff --check

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Test Results

  2 files    2 suites   0s ⏱️
132 tests 132 ✅ 0 💤 0 ❌
134 runs  134 ✅ 0 💤 0 ❌

Results for commit 19ac92e.

♻️ This comment has been updated with latest results.

@Distortions81
Distortions81 force-pushed the fix/stratum-v1-input-hardening branch from 27d6388 to 0bcd791 Compare August 7, 2026 17:52

@johnny9 johnny9 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 0bcd791 with the PR-required ESP-IDF 6.0.2 toolchain. I found two work-validation defects and one boundary-coverage gap. The missing-locktime regression fails on the PR head (84 tests, 1 failure); the three sequential proposal patches pass 86/86 ESP32-S3 QEMU tests and a fresh full ESP-IDF 6.0.2 firmware build.

Comment thread components/stratum/coinbase_decoder.c Outdated
if (offset + 4 <= coinbase_2_len) {
for (int i = 0; i < 4; i++) {
nLockTime |= ((uint32_t)coinbase_2_bin[offset + i]) << (i * 8);
if (offset <= coinbase_2_len && coinbase_2_len - offset >= 4U) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: nLockTime is treated as optional and trailing transaction bytes are ignored. A coinbase_2 with no locktime, or with bytes after the locktime, reaches ESP_OK, so the decoder accepts a serialization that is not exactly one complete transaction. I reproduced the missing-locktime case. The proposal reply requires exactly four bytes after the parsed outputs and adds missing/trailing regressions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposed patch. It applies sequentially to the exact reviewed head and was validated in the combined 86/86 ESP32-S3 QEMU run plus a fresh full ESP-IDF 6.0.2 firmware build:

diff --git a/components/stratum/coinbase_decoder.c b/components/stratum/coinbase_decoder.c
index 25b9b5a..60ffbc0 100644
--- a/components/stratum/coinbase_decoder.c
+++ b/components/stratum/coinbase_decoder.c
@@ -404,11 +404,13 @@ esp_err_t coinbase_process_notification(const mining_notify *notification,
     }
     
     // Read nLockTime (4 bytes at the end of the transaction) for BIP-54 detection
+    if (offset > coinbase_2_len || coinbase_2_len - offset != 4U) {
+        goto invalid_coinbase_2;
+    }
+
     uint32_t nLockTime = 0;
-    if (offset <= coinbase_2_len && coinbase_2_len - offset >= 4U) {
-        for (size_t i = 0; i < 4; i++) {
-            nLockTime |= ((uint32_t)coinbase_2_bin[offset + i]) << (i * 8U);
-        }
+    for (size_t i = 0; i < 4; i++) {
+        nLockTime |= ((uint32_t)coinbase_2_bin[offset + i]) << (i * 8U);
     }
     
     // Detect BIP-54 signaling: nLockTime = block_height - 1 AND nSequence != 0xffffffff
diff --git a/components/stratum/test/test_coinbase_decoder.c b/components/stratum/test/test_coinbase_decoder.c
index 56824d8..e09cb30 100644
--- a/components/stratum/test/test_coinbase_decoder.c
+++ b/components/stratum/test/test_coinbase_decoder.c
@@ -234,6 +234,48 @@ TEST_CASE("Decode regtest P2WPKH address", "[coinbase_decoder]")
 // integration-level — the detection logic is tested implicitly through
 // the address prefix matching in the full processing pipeline.
 
+TEST_CASE("Coinbase decoder requires exactly one locktime", "[coinbase_decoder][security]")
+{
+    const char *coinbase_1 =
+        "0100000001000000000000000000000000000000000000000000000000000000"
+        "0000000000ffffffff4b03a5020cfabe6d6d379ae882651f6469f2ed6b8b40a4"
+        "f9a4b41fd838a3ad6de8cba775f4e8f1d3080100000000000000";
+    const char *valid_coinbase_2 =
+        "41903d4c1b2f736c7573682f0000000003ca890d27000000001976a9147c154e"
+        "d1dc59609e3d26abb2df2ea3d587cd8c4188ac00000000000000002c6a4c2952"
+        "534b424c4f434b3a4cb4cb2ddfc37c41baf5ef6b6b4899e3253a8f1dfc7e5dd"
+        "68a5b5b27005014ef0000000000000000266a24aa21a9ed5caa249f1af9fbf71"
+        "c986fea8e076ca34ae3514fb2f86400561b28c7b15949bf00000000";
+    mining_notify notify = {
+        .version = 0x20000000,
+        .coinbase_1 = (char *)coinbase_1,
+    };
+    mining_notification_result_t result = { 0 };
+
+    char *missing_locktime = strdup(valid_coinbase_2);
+    TEST_ASSERT_NOT_NULL(missing_locktime);
+    missing_locktime[strlen(missing_locktime) - 8U] = '\0';
+    notify.coinbase_2 = missing_locktime;
+    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
+                      coinbase_process_notification(
+                          &notify, "01020304050607", 8, "", true, &result));
+    free(result.scriptsig);
+    free(missing_locktime);
+
+    size_t valid_length = strlen(valid_coinbase_2);
+    char *trailing_byte = malloc(valid_length + 3U);
+    TEST_ASSERT_NOT_NULL(trailing_byte);
+    memcpy(trailing_byte, valid_coinbase_2, valid_length);
+    memcpy(trailing_byte + valid_length, "00", 3U);
+    notify.coinbase_2 = trailing_byte;
+    memset(&result, 0, sizeof(result));
+    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
+                      coinbase_process_notification(
+                          &notify, "01020304050607", 8, "", true, &result));
+    free(result.scriptsig);
+    free(trailing_byte);
+}
+
 TEST_CASE("BIP-110 signaling not detected", "[coinbase_decoder]")
 {
     // Create a mining_notify without BIP-110 bit set

Comment thread components/stratum/coinbase_decoder.c
Comment thread components/stratum/test/test_stratum_json.c
@Distortions81
Distortions81 marked this pull request as ready for review August 7, 2026 22:30
@johnny9

johnny9 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Physical Bitaxe 602 smoke test passed on head 19ac92ec010f57b768a0ddc354ae37dd2eb51c5d.

The application-only OTA reported the expected firmware version, the device returned to healthy mining with three stable API samples, zero fault indication, and the configured pool intact. An independent authorized Stratum V1 probe received a fresh mining.notify, exercising the hardened Stratum V1 path.

Full test result and artifacts: https://mining-qa-status.vercel.app/results/4e29d544-6585-43ad-8006-da95691ff1a5

Test harness: johnny9/miner-testcode@b3694f6b01617baa8b71ed84c28300baf4cdab61.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants