Skip to content

Commit f763283

Browse files
committed
Merge #18512: Improve asmap checks and add sanity check
7489776 Add asmap_direct fuzzer that tests Interpreter directly (Pieter Wuille) 7cf97fd Make asmap Interpreter errors fatal and fuzz test it (Pieter Wuille) c81aefc Add additional effiency checks to sanity checker (Pieter Wuille) fffd8dc Add asmap sanity checker (Pieter Wuille) 5feefbe Improve asmap Interpret checks and document failures (Pieter Wuille) 2b3dbfa Deal with decoding failures explicitly in asmap Interpret (Pieter Wuille) 1479007 Introduce Instruction enum in asmap (Pieter Wuille) Pull request description: This improves/documents the failure cases inside the asmap interpreter. None of the changes are bug fixes (they only change behavior for corrupted asmap files), but they may make things easier to follow. In a second step, a sanity checker is added that effectively executes every potential code path through the asmap file, checking the same failure cases as the interpreter, and more. It takes around 30 ms to run for me for a 1.2 MB asmap file. I've verified that this accepts asmap files constructed by https://github.com/sipa/asmap/blob/master/buildmap.py with a large dataset, and no longer accepts it with 1 bit changed in it. ACKs for top commit: practicalswift: ACK 7489776 modulo feedback below. jonatack: ACK 7489776 code review, regular build/tests/ran bitcoin with -asmap, fuzz build/ran both fuzzers overnight. fjahr: ACK 7489776 Tree-SHA512: d876df3859735795c857c83e7155ba6851ce839bdfa10c18ce2698022cc493ce024b5578c1828e2a94bcdf2552c2f46c392a251ed086691b41959e62a6970821
2 parents 88b2652 + 7489776 commit f763283

File tree

8 files changed

+198
-26
lines changed

8 files changed

+198
-26
lines changed

src/Makefile.test.include

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ FUZZ_TARGETS = \
99
test/fuzz/address_deserialize \
1010
test/fuzz/addrman_deserialize \
1111
test/fuzz/asmap \
12+
test/fuzz/asmap_direct \
1213
test/fuzz/banentry_deserialize \
1314
test/fuzz/base_encode_decode \
1415
test/fuzz/bech32 \
@@ -332,6 +333,12 @@ test_fuzz_asmap_LDADD = $(FUZZ_SUITE_LD_COMMON)
332333
test_fuzz_asmap_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
333334
test_fuzz_asmap_SOURCES = test/fuzz/asmap.cpp
334335

336+
test_fuzz_asmap_direct_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
337+
test_fuzz_asmap_direct_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
338+
test_fuzz_asmap_direct_LDADD = $(FUZZ_SUITE_LD_COMMON)
339+
test_fuzz_asmap_direct_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
340+
test_fuzz_asmap_direct_SOURCES = test/fuzz/asmap_direct.cpp
341+
335342
test_fuzz_banentry_deserialize_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) -DBANENTRY_DESERIALIZE=1
336343
test_fuzz_banentry_deserialize_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
337344
test_fuzz_banentry_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON)

src/addrman.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,5 +644,9 @@ std::vector<bool> CAddrMan::DecodeAsmap(fs::path path)
644644
bits.push_back((cur_byte >> bit) & 1);
645645
}
646646
}
647+
if (!SanityCheckASMap(bits)) {
648+
LogPrintf("Sanity check of asmap file %s failed\n", path);
649+
return {};
650+
}
647651
return bits;
648652
}

src/netaddress.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,3 +894,8 @@ bool operator<(const CSubNet& a, const CSubNet& b)
894894
{
895895
return (a.network < b.network || (a.network == b.network && memcmp(a.netmask, b.netmask, 16) < 0));
896896
}
897+
898+
bool SanityCheckASMap(const std::vector<bool>& asmap)
899+
{
900+
return SanityCheckASMap(asmap, 128); // For IP address lookups, the input is 128 bits
901+
}

src/netaddress.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,6 @@ class CService : public CNetAddr
180180
}
181181
};
182182

183+
bool SanityCheckASMap(const std::vector<bool>& asmap);
184+
183185
#endif // BITCOIN_NETADDRESS_H

src/test/fuzz/asmap.cpp

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,47 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#include <netaddress.h>
6-
#include <test/fuzz/FuzzedDataProvider.h>
76
#include <test/fuzz/fuzz.h>
87

98
#include <cstdint>
109
#include <vector>
1110

11+
//! asmap code that consumes nothing
12+
static const std::vector<bool> IPV6_PREFIX_ASMAP = {};
13+
14+
//! asmap code that consumes the 96 prefix bits of ::ffff:0/96 (IPv4-in-IPv6 map)
15+
static const std::vector<bool> IPV4_PREFIX_ASMAP = {
16+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
17+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
18+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
19+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
20+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
21+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
22+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
23+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
24+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
25+
true, true, false, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false, // Match 0x00
26+
true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, // Match 0xFF
27+
true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true // Match 0xFF
28+
};
29+
1230
void test_one_input(const std::vector<uint8_t>& buffer)
1331
{
14-
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
15-
const Network network = fuzzed_data_provider.PickValueInArray({NET_IPV4, NET_IPV6});
16-
if (fuzzed_data_provider.remaining_bytes() < 16) {
17-
return;
18-
}
19-
CNetAddr net_addr;
20-
net_addr.SetRaw(network, fuzzed_data_provider.ConsumeBytes<uint8_t>(16).data());
21-
std::vector<bool> asmap;
22-
for (const char cur_byte : fuzzed_data_provider.ConsumeRemainingBytes<char>()) {
23-
for (int bit = 0; bit < 8; ++bit) {
24-
asmap.push_back((cur_byte >> bit) & 1);
32+
// Encoding: [7 bits: asmap size] [1 bit: ipv6?] [3-130 bytes: asmap] [4 or 16 bytes: addr]
33+
if (buffer.size() < 1 + 3 + 4) return;
34+
int asmap_size = 3 + (buffer[0] & 127);
35+
bool ipv6 = buffer[0] & 128;
36+
int addr_size = ipv6 ? 16 : 4;
37+
if (buffer.size() < size_t(1 + asmap_size + addr_size)) return;
38+
std::vector<bool> asmap = ipv6 ? IPV6_PREFIX_ASMAP : IPV4_PREFIX_ASMAP;
39+
asmap.reserve(asmap.size() + 8 * asmap_size);
40+
for (int i = 0; i < asmap_size; ++i) {
41+
for (int j = 0; j < 8; ++j) {
42+
asmap.push_back((buffer[1 + i] >> j) & 1);
2543
}
2644
}
45+
if (!SanityCheckASMap(asmap)) return;
46+
CNetAddr net_addr;
47+
net_addr.SetRaw(ipv6 ? NET_IPV6 : NET_IPV4, buffer.data() + 1 + asmap_size);
2748
(void)net_addr.GetMappedAS(asmap);
2849
}

src/test/fuzz/asmap_direct.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2020 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <util/asmap.h>
6+
#include <test/fuzz/fuzz.h>
7+
8+
#include <cstdint>
9+
#include <vector>
10+
11+
#include <assert.h>
12+
13+
void test_one_input(const std::vector<uint8_t>& buffer)
14+
{
15+
// Encoding: [asmap using 1 bit / byte] 0xFF [addr using 1 bit / byte]
16+
bool have_sep = false;
17+
size_t sep_pos;
18+
for (size_t pos = 0; pos < buffer.size(); ++pos) {
19+
uint8_t x = buffer[pos];
20+
if ((x & 0xFE) == 0) continue;
21+
if (x == 0xFF) {
22+
if (have_sep) return;
23+
have_sep = true;
24+
sep_pos = pos;
25+
} else {
26+
return;
27+
}
28+
}
29+
if (!have_sep) return; // Needs exactly 1 separator
30+
if (buffer.size() - sep_pos - 1 > 128) return; // At most 128 bits in IP address
31+
32+
// Checks on asmap
33+
std::vector<bool> asmap(buffer.begin(), buffer.begin() + sep_pos);
34+
if (SanityCheckASMap(asmap, buffer.size() - 1 - sep_pos)) {
35+
// Verify that for valid asmaps, no prefix (except up to 7 zero padding bits) is valid.
36+
std::vector<bool> asmap_prefix = asmap;
37+
while (!asmap_prefix.empty() && asmap_prefix.size() + 7 > asmap.size() && asmap_prefix.back() == false) asmap_prefix.pop_back();
38+
while (!asmap_prefix.empty()) {
39+
asmap_prefix.pop_back();
40+
assert(!SanityCheckASMap(asmap_prefix, buffer.size() - 1 - sep_pos));
41+
}
42+
// No address input should trigger assertions in interpreter
43+
std::vector<bool> addr(buffer.begin() + sep_pos + 1, buffer.end());
44+
(void)Interpret(asmap, addr);
45+
}
46+
}

src/util/asmap.cpp

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <map>
56
#include <vector>
67
#include <assert.h>
78
#include <crypto/common.h>
89

910
namespace {
1011

12+
constexpr uint32_t INVALID = 0xFFFFFFFF;
13+
1114
uint32_t DecodeBits(std::vector<bool>::const_iterator& bitpos, const std::vector<bool>::const_iterator& endpos, uint8_t minval, const std::vector<uint8_t> &bit_sizes)
1215
{
1316
uint32_t val = minval;
@@ -25,21 +28,29 @@ uint32_t DecodeBits(std::vector<bool>::const_iterator& bitpos, const std::vector
2528
val += (1 << *bit_sizes_it);
2629
} else {
2730
for (int b = 0; b < *bit_sizes_it; b++) {
28-
if (bitpos == endpos) break;
31+
if (bitpos == endpos) return INVALID; // Reached EOF in mantissa
2932
bit = *bitpos;
3033
bitpos++;
3134
val += bit << (*bit_sizes_it - 1 - b);
3235
}
3336
return val;
3437
}
3538
}
36-
return -1;
39+
return INVALID; // Reached EOF in exponent
3740
}
3841

42+
enum class Instruction : uint32_t
43+
{
44+
RETURN = 0,
45+
JUMP = 1,
46+
MATCH = 2,
47+
DEFAULT = 3,
48+
};
49+
3950
const std::vector<uint8_t> TYPE_BIT_SIZES{0, 0, 1};
40-
uint32_t DecodeType(std::vector<bool>::const_iterator& bitpos, const std::vector<bool>::const_iterator& endpos)
51+
Instruction DecodeType(std::vector<bool>::const_iterator& bitpos, const std::vector<bool>::const_iterator& endpos)
4152
{
42-
return DecodeBits(bitpos, endpos, 0, TYPE_BIT_SIZES);
53+
return Instruction(DecodeBits(bitpos, endpos, 0, TYPE_BIT_SIZES));
4354
}
4455

4556
const std::vector<uint8_t> ASN_BIT_SIZES{15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
@@ -70,34 +81,105 @@ uint32_t Interpret(const std::vector<bool> &asmap, const std::vector<bool> &ip)
7081
const std::vector<bool>::const_iterator endpos = asmap.end();
7182
uint8_t bits = ip.size();
7283
uint32_t default_asn = 0;
73-
uint32_t opcode, jump, match, matchlen;
84+
uint32_t jump, match, matchlen;
85+
Instruction opcode;
7486
while (pos != endpos) {
7587
opcode = DecodeType(pos, endpos);
76-
if (opcode == 0) {
77-
return DecodeASN(pos, endpos);
78-
} else if (opcode == 1) {
88+
if (opcode == Instruction::RETURN) {
89+
default_asn = DecodeASN(pos, endpos);
90+
if (default_asn == INVALID) break; // ASN straddles EOF
91+
return default_asn;
92+
} else if (opcode == Instruction::JUMP) {
7993
jump = DecodeJump(pos, endpos);
80-
if (bits == 0) break;
94+
if (jump == INVALID) break; // Jump offset straddles EOF
95+
if (bits == 0) break; // No input bits left
96+
if (jump >= endpos - pos) break; // Jumping past EOF
8197
if (ip[ip.size() - bits]) {
82-
if (jump >= endpos - pos) break;
8398
pos += jump;
8499
}
85100
bits--;
86-
} else if (opcode == 2) {
101+
} else if (opcode == Instruction::MATCH) {
87102
match = DecodeMatch(pos, endpos);
103+
if (match == INVALID) break; // Match bits straddle EOF
88104
matchlen = CountBits(match) - 1;
105+
if (bits < matchlen) break; // Not enough input bits
89106
for (uint32_t bit = 0; bit < matchlen; bit++) {
90-
if (bits == 0) break;
91107
if ((ip[ip.size() - bits]) != ((match >> (matchlen - 1 - bit)) & 1)) {
92108
return default_asn;
93109
}
94110
bits--;
95111
}
96-
} else if (opcode == 3) {
112+
} else if (opcode == Instruction::DEFAULT) {
97113
default_asn = DecodeASN(pos, endpos);
114+
if (default_asn == INVALID) break; // ASN straddles EOF
98115
} else {
99-
break;
116+
break; // Instruction straddles EOF
100117
}
101118
}
119+
assert(false); // Reached EOF without RETURN, or aborted (see any of the breaks above) - should have been caught by SanityCheckASMap below
102120
return 0; // 0 is not a valid ASN
103121
}
122+
123+
bool SanityCheckASMap(const std::vector<bool>& asmap, int bits)
124+
{
125+
const std::vector<bool>::const_iterator begin = asmap.begin(), endpos = asmap.end();
126+
std::vector<bool>::const_iterator pos = begin;
127+
std::vector<std::pair<uint32_t, int>> jumps; // All future positions we may jump to (bit offset in asmap -> bits to consume left)
128+
jumps.reserve(bits);
129+
Instruction prevopcode = Instruction::JUMP;
130+
bool had_incomplete_match = false;
131+
while (pos != endpos) {
132+
uint32_t offset = pos - begin;
133+
if (!jumps.empty() && offset >= jumps.back().first) return false; // There was a jump into the middle of the previous instruction
134+
Instruction opcode = DecodeType(pos, endpos);
135+
if (opcode == Instruction::RETURN) {
136+
if (prevopcode == Instruction::DEFAULT) return false; // There should not be any RETURN immediately after a DEFAULT (could be combined into just RETURN)
137+
uint32_t asn = DecodeASN(pos, endpos);
138+
if (asn == INVALID) return false; // ASN straddles EOF
139+
if (jumps.empty()) {
140+
// Nothing to execute anymore
141+
if (endpos - pos > 7) return false; // Excessive padding
142+
while (pos != endpos) {
143+
if (*pos) return false; // Nonzero padding bit
144+
++pos;
145+
}
146+
return true; // Sanely reached EOF
147+
} else {
148+
// Continue by pretending we jumped to the next instruction
149+
offset = pos - begin;
150+
if (offset != jumps.back().first) return false; // Unreachable code
151+
bits = jumps.back().second; // Restore the number of bits we would have had left after this jump
152+
jumps.pop_back();
153+
prevopcode = Instruction::JUMP;
154+
}
155+
} else if (opcode == Instruction::JUMP) {
156+
uint32_t jump = DecodeJump(pos, endpos);
157+
if (jump == INVALID) return false; // Jump offset straddles EOF
158+
if (jump > endpos - pos) return false; // Jump out of range
159+
if (bits == 0) return false; // Consuming bits past the end of the input
160+
--bits;
161+
uint32_t jump_offset = pos - begin + jump;
162+
if (!jumps.empty() && jump_offset >= jumps.back().first) return false; // Intersecting jumps
163+
jumps.emplace_back(jump_offset, bits);
164+
prevopcode = Instruction::JUMP;
165+
} else if (opcode == Instruction::MATCH) {
166+
uint32_t match = DecodeMatch(pos, endpos);
167+
if (match == INVALID) return false; // Match bits straddle EOF
168+
int matchlen = CountBits(match) - 1;
169+
if (prevopcode != Instruction::MATCH) had_incomplete_match = false;
170+
if (matchlen < 8 && had_incomplete_match) return false; // Within a sequence of matches only at most one should be incomplete
171+
had_incomplete_match = (matchlen < 8);
172+
if (bits < matchlen) return false; // Consuming bits past the end of the input
173+
bits -= matchlen;
174+
prevopcode = Instruction::MATCH;
175+
} else if (opcode == Instruction::DEFAULT) {
176+
if (prevopcode == Instruction::DEFAULT) return false; // There should not be two successive DEFAULTs (they could be combined into one)
177+
uint32_t asn = DecodeASN(pos, endpos);
178+
if (asn == INVALID) return false; // ASN straddles EOF
179+
prevopcode = Instruction::DEFAULT;
180+
} else {
181+
return false; // Instruction straddles EOF
182+
}
183+
}
184+
return false; // Reached EOF without RETURN instruction
185+
}

src/util/asmap.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
#ifndef BITCOIN_UTIL_ASMAP_H
66
#define BITCOIN_UTIL_ASMAP_H
77

8+
#include <stdint.h>
9+
#include <vector>
10+
811
uint32_t Interpret(const std::vector<bool> &asmap, const std::vector<bool> &ip);
912

13+
bool SanityCheckASMap(const std::vector<bool>& asmap, int bits);
14+
1015
#endif // BITCOIN_UTIL_ASMAP_H

0 commit comments

Comments
 (0)