Harden Stratum V1 input parsing and work generation - #1849
Conversation
Test Results 2 files 2 suites 0s ⏱️ Results for commit 19ac92e. ♻️ This comment has been updated with latest results. |
27d6388 to
0bcd791
Compare
johnny9
left a comment
There was a problem hiding this comment.
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.
| 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(
+ ¬ify, "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(
+ ¬ify, "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
|
Physical Bitaxe 602 smoke test passed on head 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 Full test result and artifacts: https://mining-qa-status.vercel.app/results/4e29d544-6585-43ad-8006-da95691ff1a5 Test harness: |
Summary
Treat Stratum V1 pool messages and derived coinbase/work data as untrusted input through receive, parse, and job generation.
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
git diff --check