What changed, and why it matters
This commit adds a new Bitcoin Core RPC command called exportasmap. It lets a user with RPC access write a copy of the built-in network-mapping database (the ASMap file) to a file on the node's disk. The file path is relative to the node's data directory, cannot escape it, and any existing file at that path is overwritten. It is a straightforward export feature with no obvious code-level security bug, but it does add another way for an authenticated RPC user to write files on the server, which is normally a capability reserved for trusted administrators.
No immediate action required. Operators should ensure RPC access is limited to trusted users, as with all Bitcoin Core RPC commands. Reviewers may want to confirm that fsbridge::AbsPathJoin correctly prevents path traversal outside the datadir and that the overwrite behavior is acceptable.
Security signals we found
New RPC that writes to filesystem under datadir
Existing file at target path is overwritten
Path is restricted to datadir via fsbridge::AbsPathJoin
No input sanitization beyond filesystem abstraction
Fuzz harness explicitly excludes command as disk-write unsafe
Evidence from the diff
The patch introduces exportasmap in src/rpc/net.cpp. When ENABLE_EMBEDDED_ASMAP is defined, it validates the embedded ip_asn blob with CheckStandardAsmap, resolves the supplied path under the network-specific datadir via fsbridge::AbsPathJoin, writes the blob with AutoFile in wb mode, and returns the absolute path, byte count, and SHA256 hash. The fuzz harness excludes exportasmap to avoid disk writes. Functional tests verify relative-path resolution, byte count, and a known SHA256 for the embedded data.
Changed components
src/rpc/net.cppsrc/test/fuzz/rpc.cpptest/functional/feature_asmap.pyInspect captured patch +96 / −7
diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp
index cf5e5864..5b19de10 100644
--- a/src/rpc/net.cpp
+++ b/src/rpc/net.cpp
@@ -9,12 +9,17 @@
#include <banman.h>
#include <chainparams.h>
#include <clientversion.h>
+#include <common/args.h>
#include <core_io.h>
+#include <hash.h>
#include <net_permissions.h>
#include <net_processing.h>
#include <net_types.h>
#include <netbase.h>
#include <node/context.h>
+#ifdef ENABLE_EMBEDDED_ASMAP
+#include <node/data/ip_asn.dat.h>
+#endif
#include <node/protocol_version.h>
#include <node/warnings.h>
#include <policy/settings.h>
@@ -25,6 +30,7 @@
#include <rpc/util.h>
#include <sync.h>
#include <univalue.h>
+#include <util/asmap.h>
#include <util/chaintype.h>
#include <util/strencodings.h>
#include <util/string.h>
@@ -1116,6 +1122,59 @@ static RPCMethod getaddrmaninfo()
};
}
+static RPCMethod exportasmap()
+{
+ return RPCMethod{
+ "exportasmap",
+ "Export the embedded ASMap data to a file. Any existing file at the path will be overwritten.\n",
+ {
+ {"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
+ },
+ RPCResult{
+ RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::STR, "path", "the absolute path that the ASMap data was written to"},
+ {RPCResult::Type::NUM, "bytes_written", "the number of bytes written to the file"},
+ {RPCResult::Type::STR_HEX, "file_hash", "the SHA256 hash of the exported ASMap data"},
+ }
+ },
+ RPCExamples{
+ HelpExampleCli("exportasmap", "\"asmap.dat\"") + HelpExampleRpc("exportasmap", "\"asmap.dat\"")},
+ [&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue {
+#ifndef ENABLE_EMBEDDED_ASMAP
+ throw JSONRPCError(RPC_MISC_ERROR, "No embedded ASMap data available");
+#else
+ if (node::data::ip_asn.empty() || !CheckStandardAsmap(node::data::ip_asn)) {
+ throw JSONRPCError(RPC_MISC_ERROR, "Embedded ASMap data appears to be corrupted");
+ }
+
+ const ArgsManager& args{EnsureAnyArgsman(request.context)};
+ const fs::path export_path{fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(self.Arg<std::string_view>("path")))};
+
+ AutoFile file{fsbridge::fopen(export_path, "wb")};
+ if (file.IsNull()) {
+ throw JSONRPCError(RPC_MISC_ERROR, strprintf("Failed to open asmap file: %s", fs::PathToString(export_path)));
+ }
+
+ file << node::data::ip_asn;
+
+ if (file.fclose() != 0) {
+ throw JSONRPCError(RPC_MISC_ERROR, strprintf("Failed to close asmap file: %s", fs::PathToString(export_path)));
+ }
+
+ HashWriter hasher;
+ hasher.write(node::data::ip_asn);
+
+ UniValue result(UniValue::VOBJ);
+ result.pushKV("path", export_path.utf8string());
+ result.pushKV("bytes_written", (uint64_t)node::data::ip_asn.size());
+ result.pushKV("file_hash", HexStr(hasher.GetSHA256()));
+ return result;
+#endif
+ },
+ };
+}
+
UniValue AddrmanEntryToJSON(const AddrInfo& info, const CConnman& connman)
{
UniValue ret(UniValue::VOBJ);
@@ -1210,6 +1269,7 @@ void RegisterNetRPCCommands(CRPCTable& t)
{"network", &setnetworkactive},
{"network", &getnodeaddresses},
{"network", &getaddrmaninfo},
+ {"network", &exportasmap},
{"hidden", &addconnection},
{"hidden", &addpeeraddress},
{"hidden", &sendmsgtopeer},
diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp
index cd7f4987..f0362db2 100644
--- a/src/test/fuzz/rpc.cpp
+++ b/src/test/fuzz/rpc.cpp
@@ -78,15 +78,16 @@ const std::vector<std::string> RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{
"dumptxoutset", // avoid writing to disk
"enumeratesigners",
"echoipc", // avoid assertion failure (Assertion `"EnsureAnyNodeContext(request.context).init" && check' failed.)
+ "exportasmap", // avoid writing to disk
"generatetoaddress", // avoid prohibitively slow execution (when `num_blocks` is large)
"generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large)
"gettxoutproof", // avoid prohibitively slow execution
- "importmempool", // avoid reading from disk
- "loadtxoutset", // avoid reading from disk
- "loadwallet", // avoid reading from disk
- "savemempool", // disabled as a precautionary measure: may take a file path argument in the future
- "setban", // avoid DNS lookups
- "stop", // avoid shutdown state
+ "importmempool", // avoid reading from disk
+ "loadtxoutset", // avoid reading from disk
+ "loadwallet", // avoid reading from disk
+ "savemempool", // disabled as a precautionary measure: may take a file path argument in the future
+ "setban", // avoid DNS lookups
+ "stop", // avoid shutdown state
};
// RPC commands which are safe for fuzzing.
diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py
index 310788f2..5debdfbe 100755
--- a/test/functional/feature_asmap.py
+++ b/test/functional/feature_asmap.py
@@ -11,11 +11,15 @@ with missing and unparseable files.
The tests are order-independent.
"""
+import hashlib
import os
import shutil
from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal
+from test_framework.util import (
+ assert_equal,
+ assert_raises_rpc_error,
+)
ASMAP = 'src/test/data/asmap.raw' # path to unit test skeleton asmap
VERSION = 'bafc9da308f45179443bd1d22325400ac9104f741522d003e3fac86700f68895'
@@ -124,6 +128,29 @@ class AsmapTest(BitcoinTestFramework):
asns.append(asn)
assert_equal(len(asns), 3)
+ def test_export_embedded_asmap(self):
+ self.log.info('Test exportasmap RPC')
+ export_path = os.path.join(self.datadir, "asmap.dat")
+
+ if not self.is_embedded_asmap_compiled():
+ assert_raises_rpc_error(-1, "No embedded ASMap data available", self.node.exportasmap, export_path)
+ return
+
+ # Relative paths are resolved against the datadir.
+ result = self.node.exportasmap("asmap.dat")
+ assert_equal(result["path"], export_path)
+
+ with open(export_path, 'rb') as f:
+ data = f.read()
+ assert_equal(result["bytes_written"], len(data))
+
+ # Added in https://github.com/bitcoin/bitcoin/pull/34696
+ expected_hash = "478d61986c59365cf86cd244485bbbe76a9ca0c630864717286dd19949879074"
+ assert_equal(hashlib.sha256(data).hexdigest(), expected_hash)
+ assert_equal(result["file_hash"], expected_hash)
+
+ os.remove(export_path)
+
def run_test(self):
self.node = self.nodes[0]
self.datadir = self.node.chain_path
@@ -139,6 +166,7 @@ class AsmapTest(BitcoinTestFramework):
self.test_asmap_with_missing_file()
self.test_empty_asmap()
self.test_asmap_health_check()
+ self.test_export_embedded_asmap()
if __name__ == '__main__':
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.