libibnetdisc: keep cache loadable after port timeouts
[ Upstream commit de83d1c208f9550ec1024a132ad982dcba9d8c06 ]
ibnetdiscover --cache writes each node's port references and each
port's remote-port reference from the in-memory fabric. If a PortInfo
query times out during discovery, the port can remain as a shell in
node->ports[] or as a remoteport without ever being inserted into the
fabric port table. Such a port has no cache port record, but references
to it were still written. ibnd_load_fabric() then rejected the cache with
"Cache invalid: cannot find port".
Make the cache writer emit only references to ports that are actually
serialized. A port is serialized only when it is reachable from its
owning node, and the same predicate is used for node port references,
remote-port references, and port records. References are matched against
a sorted set of serialized ports, avoiding repeated port-table walks on
large fabrics. Keep nodes with no ports array as zero-port node records
so fabric->from_node remains resolvable.
Also tolerate old incomplete caches when reading: missing node port keys
and missing remote ports are skipped instead of aborting the load. While
doing that, keep malformed caches from creating unsafe fabric state by
bounds-checking cached port numbers before indexing node->ports[] (an
out-of-bounds write) and by skipping unclaimed port records before they
are linked into the fabric and then freed (a use-after-free).
Fixes: 01b8045f28f6 ("add libibnetdisc caching to libibnetdiscover")
Signed-off-by: Jonathan Süssemilch Poulain <jpoulain@coreweave.com>
Signed-off-by: Nicolas Morey <nmorey@suse.com>
diff --git a/libibnetdisc/ibnetdisc_cache.c b/libibnetdisc/ibnetdisc_cache.c
index 10de714..1001d47 100644
--- a/libibnetdisc/ibnetdisc_cache.c
+++ b/libibnetdisc/ibnetdisc_cache.c
@@ -496,8 +496,10 @@
ibnd_port_cache_t *port_cache;
if (!(port_cache = _find_port(fabric_cache, port_cache_key))) {
- IBND_DEBUG("Cache invalid: cannot find port\n");
- return -1;
+ IBND_DEBUG("Cache incomplete: cannot find port 0x%016" PRIx64
+ ":%u\n", port_cache_key->guid,
+ (unsigned int) port_cache_key->portnum);
+ return 0;
}
if (port_cache->port_stored_to_fabric) {
@@ -505,6 +507,20 @@
return -1;
}
+ /* node->ports was allocated for node->numports + 1 entries; a cached
+ * portnum outside that range (corrupt/incompatible cache) would index
+ * out of bounds, so treat it as an incomplete record and skip it.
+ */
+ if (port_cache->port->portnum < 0 ||
+ port_cache->port->portnum > node->numports) {
+ IBND_DEBUG("Cache incomplete: port 0x%016" PRIx64
+ " portnum %d out of range for node 0x%016" PRIx64
+ " (numports %d)\n",
+ port_cache->port->guid, port_cache->port->portnum,
+ node->guid, node->numports);
+ return 0;
+ }
+
node->ports[port_cache->port->portnum] = port_cache->port;
port_cache->port_stored_to_fabric++;
@@ -587,6 +603,21 @@
port_cache_next = port_cache->next;
+ /* Skip port records that no node claimed during
+ * _rebuild_nodes(). _destroy_ibnd_fabric_cache() frees such
+ * records, so they must not be linked into the fabric (LID
+ * hash or another port's remoteport) or we would leave
+ * dangling pointers behind.
+ */
+ if (!port_cache->port_stored_to_fabric) {
+ IBND_DEBUG("Cache incomplete: unreferenced port record "
+ "0x%016" PRIx64 ":%d, skipping\n",
+ port_cache->port->guid,
+ port_cache->port->portnum);
+ port_cache = port_cache_next;
+ continue;
+ }
+
port = port_cache->port;
if (!(node_cache =
@@ -598,17 +629,22 @@
port->node = node_cache->node;
if (port_cache->remoteport_flag) {
- if (!(remoteport_cache = _find_port(fabric_cache,
- &port_cache->remoteport_cache_key)))
- {
- IBND_DEBUG
- ("Cache invalid: cannot find remote port\n");
- return -1;
+ remoteport_cache =
+ _find_port(fabric_cache,
+ &port_cache->remoteport_cache_key);
+ if (remoteport_cache &&
+ remoteport_cache->port_stored_to_fabric) {
+ port->remoteport = remoteport_cache->port;
+ } else {
+ IBND_DEBUG("Cache incomplete: cannot find remote port "
+ "0x%016" PRIx64 ":%u\n",
+ port_cache->remoteport_cache_key.guid,
+ (unsigned int) port_cache->remoteport_cache_key.portnum);
+ port->remoteport = NULL;
}
-
- port->remoteport = remoteport_cache->port;
- } else
+ } else {
port->remoteport = NULL;
+ }
add_to_portlid_hash(port, fabric_cache->f_int);
port_cache = port_cache_next;
@@ -803,7 +839,89 @@
return 0;
}
-static int _cache_node(int fd, ibnd_node_t * node)
+/* Shell ports may remain in node->ports[] after discovery timeouts, but only
+ * ports present in portstbl have cache records. Sorted set of those ports so
+ * references to shell ports can be dropped while caching.
+ */
+typedef struct {
+ ibnd_port_t **ports;
+ size_t count;
+} port_set_t;
+
+/* Cacheable ports must be reachable from their owning node so references can
+ * be resolved on load.
+ */
+static int _port_cacheable(ibnd_port_t *port)
+{
+ return port && port->node && port->node->ports &&
+ port->portnum >= 0 && port->portnum <= port->node->numports &&
+ port->node->ports[port->portnum] == port;
+}
+
+static int _port_ptr_cmp(const void *a, const void *b)
+{
+ uintptr_t pa = (uintptr_t) *(ibnd_port_t * const *) a;
+ uintptr_t pb = (uintptr_t) *(ibnd_port_t * const *) b;
+
+ if (pa < pb)
+ return -1;
+ if (pa > pb)
+ return 1;
+ return 0;
+}
+
+static int _port_set_build(port_set_t *set, ibnd_fabric_t *fabric)
+{
+ size_t count = 0;
+ ibnd_port_t *port;
+ int i;
+
+ set->ports = NULL;
+ set->count = 0;
+
+ for (i = 0; i < HTSZ; i++)
+ for (port = fabric->portstbl[i]; port; port = port->htnext)
+ if (_port_cacheable(port))
+ count++;
+
+ if (!count)
+ return 0;
+
+ set->ports = malloc(count * sizeof(*set->ports));
+ if (!set->ports) {
+ IBND_DEBUG("OOM: port set\n");
+ return -1;
+ }
+
+ for (i = 0; i < HTSZ; i++)
+ for (port = fabric->portstbl[i]; port; port = port->htnext)
+ if (_port_cacheable(port))
+ set->ports[set->count++] = port;
+
+ qsort(set->ports, set->count, sizeof(*set->ports), _port_ptr_cmp);
+ return 0;
+}
+
+static void _port_set_destroy(port_set_t *set)
+{
+ free(set->ports);
+ set->ports = NULL;
+ set->count = 0;
+}
+
+/* Pointer-identity test: a shell port may share a guid with a real port, so
+ * ibnd_find_port_guid() is not enough.
+ */
+static int _port_set_contains(const port_set_t *set, ibnd_port_t *port)
+{
+ if (!port || !set->count)
+ return 0;
+
+ return bsearch(&port, set->ports, set->count, sizeof(*set->ports),
+ _port_ptr_cmp) != NULL;
+}
+
+static int _cache_node(int fd, const port_set_t *set, ibnd_node_t *node)
{
uint8_t buf[IBND_FABRIC_CACHE_BUFLEN];
size_t offset = 0;
@@ -828,14 +946,24 @@
ports_stored_offset = offset;
offset += sizeof(uint8_t);
- for (i = 0; i <= node->numports; i++) {
- if (node->ports[i]) {
- offset += _marshall64(buf + offset,
- node->ports[i]->guid);
- offset += _marshall8(buf + offset,
- (uint8_t) node->ports[i]->portnum);
- ports_stored_count++;
+ /* Emit even a node with no ports array (with zero ports) so that
+ * fabric->from_node, whose guid is already in the header, stays
+ * loadable.
+ */
+ if (node->ports) {
+ for (i = 0; i <= node->numports; i++) {
+ if (node->ports[i] &&
+ _port_set_contains(set, node->ports[i])) {
+ offset += _marshall64(buf + offset,
+ node->ports[i]->guid);
+ offset += _marshall8(buf + offset,
+ (uint8_t) node->ports[i]->portnum);
+ ports_stored_count++;
+ }
}
+ } else {
+ IBND_DEBUG("Cache incomplete: node 0x%016" PRIx64
+ " has no ports array\n", node->guid);
}
/* go back and store number of port keys stored */
@@ -844,14 +972,25 @@
if (ibnd_write(fd, buf, offset) < 0)
return -1;
- return 0;
+ return 1;
}
-static int _cache_port(int fd, ibnd_port_t * port)
+/* Returns 1 if written, 0 if skipped, -1 on error. */
+static int _cache_port(int fd, const port_set_t *set, ibnd_port_t *port)
{
uint8_t buf[IBND_FABRIC_CACHE_BUFLEN];
size_t offset = 0;
+ /* Same predicate port_set is filtered by, so written references and
+ * written records stay in lock-step.
+ */
+ if (!_port_cacheable(port)) {
+ IBND_DEBUG("Cache incomplete: port 0x%016" PRIx64
+ ":%d not reachable from its node, skipping\n",
+ port->guid, port->portnum);
+ return 0;
+ }
+
offset += _marshall64(buf + offset, port->guid);
offset += _marshall8(buf + offset, (uint8_t) port->portnum);
offset += _marshall8(buf + offset, (uint8_t) port->ext_portnum);
@@ -859,10 +998,12 @@
offset += _marshall8(buf + offset, port->lmc);
offset += _marshall_buf(buf + offset, port->info, IB_SMP_DATA_SIZE);
offset += _marshall64(buf + offset, port->node->guid);
- if (port->remoteport) {
+ if (port->remoteport &&
+ _port_set_contains(set, port->remoteport)) {
offset += _marshall8(buf + offset, 1);
offset += _marshall64(buf + offset, port->remoteport->guid);
- offset += _marshall8(buf + offset, (uint8_t) port->remoteport->portnum);
+ offset += _marshall8(buf + offset,
+ (uint8_t) port->remoteport->portnum);
} else {
offset += _marshall8(buf + offset, 0);
offset += _marshall64(buf + offset, 0);
@@ -872,7 +1013,7 @@
if (ibnd_write(fd, buf, offset) < 0)
return -1;
- return 0;
+ return 1;
}
int ibnd_cache_fabric(ibnd_fabric_t * fabric, const char *file,
@@ -885,8 +1026,10 @@
ibnd_port_t *port = NULL;
ibnd_port_t *port_next = NULL;
unsigned int port_count = 0;
+ port_set_t port_set = { NULL, 0 };
int fd;
int i;
+ int rc;
if (!fabric) {
IBND_DEBUG("fabric parameter NULL\n");
@@ -898,6 +1041,11 @@
return -1;
}
+ if (!fabric->from_node) {
+ IBND_DEBUG("cannot cache fabric: missing from node\n");
+ return -1;
+ }
+
if (!(flags & IBND_CACHE_FABRIC_FLAG_NO_OVERWRITE)) {
if (!stat(file, &statbuf)) {
if (unlink(file) < 0) {
@@ -922,14 +1070,19 @@
if (_cache_header_info(fd, fabric) < 0)
goto cleanup;
+ if (_port_set_build(&port_set, fabric) < 0)
+ goto cleanup;
+
node = fabric->nodes;
while (node) {
node_next = node->next;
- if (_cache_node(fd, node) < 0)
+ rc = _cache_node(fd, &port_set, node);
+ if (rc < 0)
goto cleanup;
+ if (rc)
+ node_count++;
- node_count++;
node = node_next;
}
@@ -938,10 +1091,12 @@
while (port) {
port_next = port->htnext;
- if (_cache_port(fd, port) < 0)
+ rc = _cache_port(fd, &port_set, port);
+ if (rc < 0)
goto cleanup;
+ if (rc)
+ port_count++;
- port_count++;
port = port_next;
}
}
@@ -949,14 +1104,21 @@
if (_cache_header_counts(fd, node_count, port_count) < 0)
goto cleanup;
+ IBND_DEBUG("cached fabric: %u nodes, %u ports\n",
+ node_count, port_count);
+
+ _port_set_destroy(&port_set);
+
if (close(fd) < 0) {
IBND_DEBUG("close: %s\n", strerror(errno));
- goto cleanup;
+ unlink(file);
+ return -1;
}
return 0;
cleanup:
+ _port_set_destroy(&port_set);
unlink(file);
close(fd);
return -1;