OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / test_bootstrap_ownership.cpp

131 lines · cpp ref:
// Each peer's DHT bootstrap list belongs to that peer.
//
// Build: ./build_tests.sh && ./test_bootstrap_ownership
// (SAN=address is the interesting run: the defect this covers was a read of
// freed memory, and only a sanitizer names it before it becomes a wrong answer.)
//
// WHY THIS EXISTS. start_dht does not use the bootstrap list — it queues
// CMD_DHT_INIT and returns, and _init_dht parses the strings later, on the UDX
// thread. The transport used to keep the caller's `const char **`, and join_dht
// kept the strings alive in vectors that were `static`: one list shared by every
// HyperswarmMultiplayerPeer in the process. A second peer's join_dht cleared and
// refilled them, so the first peer's transport was left pointing into a vector
// that had been emptied and reallocated underneath it — while it was still
// waiting to read it on another thread.
//
// A second defect sat inside the same lines: the pointers were taken in the same
// pass that filled the strings. A vector of SHORT strings stores them inside its
// own element block, so each growth frees the block that every c_str() taken so
// far points into. At one bootstrap node the vector never grows and nothing goes
// wrong, which is exactly what the integration suite passes; at two it is a
// heap-use-after-free.
//
// So the lists here are THREE nodes long, and they are built in a scope that ends
// before anything is asserted: a transport that still needed the caller's memory
// would be reading a dead vector, which is the whole claim.
//
// NOTE ON REDNESS: this gate asserts the ownership contract that the fix
// introduces, and could not have been run against the previous shape — there was
// no per-transport list to ask about, only a borrowed pointer. The defect itself
// was measured separately, as a standalone ASan repro of the same-pass pointer
// capture.

#include "udx_transport.h"

#include <sodium.h>

#include <chrono>
#include <cstdio>
#include <string>
#include <thread>
#include <vector>

static int tests_run = 0;
static int tests_passed = 0;

static void check(const char *name, bool ok) {
	tests_run++;
	if (ok) {
		tests_passed++;
		printf("  PASS  %s\n", name);
	} else {
		printf("  FAIL  %s\n", name);
	}
}

static void sleep_ms(int ms) {
	std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

// Start a transport the way a caller does, from buffers that DIE when this
// returns. Unroutable addresses, so nothing leaves the machine.
static void start_with(UdxTransport &t, const std::vector<std::string> &want) {
	Ed25519KeyPair kp;
	crypto_sign_keypair(kp.public_key, kp.secret_key);

	std::vector<std::string> strs(want.begin(), want.end());
	std::vector<const char *> ptrs;
	ptrs.reserve(strs.size());
	for (const std::string &s : strs) ptrs.push_back(s.c_str());
	t.start_dht(kp, ptrs.data(), (int)ptrs.size());
}

static bool same(const std::vector<std::string> &a, const std::vector<std::string> &b) {
	if (a.size() != b.size()) return false;
	for (size_t i = 0; i < a.size(); i++)
		if (a[i] != b[i]) return false;
	return true;
}

int main() {
	printf("=== DHT bootstrap ownership ===\n");

	if (sodium_init() < 0) {
		printf("  FAIL  sodium_init\n");
		return 1;
	}

	// Deliberately not the same list, so "A has B's" is distinguishable from
	// "both have the right one". Port 1 answers nothing.
	const std::vector<std::string> want_a = {
		"127.0.0.1:1", "127.0.0.2:2", "bootstrap-a.invalid.example:49737"};
	const std::vector<std::string> want_b = {
		"127.0.0.3:3", "127.0.0.4:4", "bootstrap-b.invalid.example:49737"};

	UdxTransport a;
	UdxTransport b;

	start_with(a, want_a);
	check("A kept the list after the caller's buffers died", same(a.bootstrap_nodes(), want_a));

	// THE SECOND JOIN IS THE TEST. This is what used to reach into the first
	// peer's list, because there was only ever one.
	start_with(b, want_b);
	check("B has its own list", same(b.bootstrap_nodes(), want_b));
	check("and A's is untouched by B starting", same(a.bootstrap_nodes(), want_a));

	// Churn the heap: anything still borrowing freed memory is now reading
	// somebody else's bytes rather than a lucky copy of its own.
	{
		std::vector<std::string> noise;
		for (int i = 0; i < 256; i++) noise.push_back("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	}
	check("A still, after the heap was churned", same(a.bootstrap_nodes(), want_a));
	check("B still", same(b.bootstrap_nodes(), want_b));

	// And the copy is what actually gets used: _init_dht runs on the UDX thread
	// and parses this list. If it read freed memory, this is where it would.
	sleep_ms(300);
	check("A's DHT initialised from it", a.is_dht_active());
	check("B's DHT initialised from it", b.is_dht_active());
	check("the two bound different ports", a.bound_port() > 0 && b.bound_port() > 0 &&
			a.bound_port() != b.bound_port());

	a.stop();
	b.stop();
	check("both stopped clean", !a.is_running() && !b.is_running());

	printf("\n=== %d/%d passed ===\n", tests_passed, tests_run);
	return tests_passed == tests_run ? 0 : 1;
}