Merge branch 'avb-test-suite' into 'master'

module-avb: fix heap corruption in server_destroy_descriptors

See merge request pipewire/pipewire!2778
This commit is contained in:
Christian Fredrik Kalager Schaller 2026-04-08 19:26:28 +00:00
commit 3d0545bf3f
14 changed files with 4784 additions and 31 deletions

View file

@ -82,7 +82,7 @@ static void pending_free(struct acmp *acmp, struct pending *p)
static void pending_destroy(struct acmp *acmp) static void pending_destroy(struct acmp *acmp)
{ {
struct pending *p, *t; struct pending *p, *t;
for (uint32_t list_id = 0; list_id < PENDING_CONTROLLER; list_id++) { for (uint32_t list_id = 0; list_id <= PENDING_CONTROLLER; list_id++) {
spa_list_for_each_safe(p, t, &acmp->pending[list_id], link) { spa_list_for_each_safe(p, t, &acmp->pending[list_id], link) {
pending_free(acmp, p); pending_free(acmp, p);
} }
@ -174,13 +174,14 @@ static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void
return 0; return 0;
memcpy(buf, m, len); memcpy(buf, m, len);
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id));
if (stream == NULL) { if (stream == NULL) {
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
goto done; goto done;
} }
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
reply->stream_id = htobe64(stream->id); reply->stream_id = htobe64(stream->id);
stream_activate(stream, ntohs(reply->talker_unique_id), now); stream_activate(stream, ntohs(reply->talker_unique_id), now);
@ -251,14 +252,14 @@ static int handle_disconnect_tx_command(struct acmp *acmp, uint64_t now, const v
return 0; return 0;
memcpy(buf, m, len); memcpy(buf, m, len);
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id)); stream = find_stream(server, SPA_DIRECTION_OUTPUT, ntohs(reply->talker_unique_id));
if (stream == NULL) { if (stream == NULL) {
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX; status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
goto done; goto done;
} }
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
stream_deactivate(stream, now); stream_deactivate(stream, now);
done: done:

View file

@ -99,7 +99,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void
desc = server_find_descriptor(server, desc_type, desc_id); desc = server_find_descriptor(server, desc_type, desc_id);
if (desc == NULL) if (desc == NULL)
return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, p, len); return reply_status(aecp, AVB_AECP_AEM_STATUS_NO_SUCH_DESCRIPTOR, m, len);
entity_state = desc->ptr; entity_state = desc->ptr;
lock = &entity_state->state.lock_state; lock = &entity_state->state.lock_state;
@ -148,7 +148,7 @@ int handle_cmd_lock_entity_milan_v12(struct aecp *aecp, int64_t now, const void
// If the lock is taken again by device // If the lock is taken again by device
if (ctrler_id == lock->locked_id) { if (ctrler_id == lock->locked_id) {
lock->base_info.expire_timeout += lock->base_info.expire_timeout +=
AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND; AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC;
lock->is_locked = true; lock->is_locked = true;
} else { } else {

View file

@ -27,7 +27,8 @@ static int handle_acquire_entity_avb_legacy(struct aecp *aecp, int64_t now,
const void *m, int len) const void *m, int len)
{ {
struct server *server = aecp->server; struct server *server = aecp->server;
const struct avb_packet_aecp_aem *p = m; const struct avb_ethernet_header *h = m;
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
const struct avb_packet_aecp_aem_acquire *ae; const struct avb_packet_aecp_aem_acquire *ae;
const struct descriptor *desc; const struct descriptor *desc;
uint16_t desc_type, desc_id; uint16_t desc_type, desc_id;
@ -53,7 +54,8 @@ static int handle_lock_entity_avb_legacy(struct aecp *aecp, int64_t now,
const void *m, int len) const void *m, int len)
{ {
struct server *server = aecp->server; struct server *server = aecp->server;
const struct avb_packet_aecp_aem *p = m; const struct avb_ethernet_header *h = m;
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
const struct avb_packet_aecp_aem_acquire *ae; const struct avb_packet_aecp_aem_acquire *ae;
const struct descriptor *desc; const struct descriptor *desc;
uint16_t desc_type, desc_id; uint16_t desc_type, desc_id;

View file

@ -0,0 +1,149 @@
/* AVB support */
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
/* SPDX-License-Identifier: MIT */
#ifndef AVB_TRANSPORT_LOOPBACK_H
#define AVB_TRANSPORT_LOOPBACK_H
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include "internal.h"
#include "packets.h"
#define AVB_LOOPBACK_MAX_PACKETS 64
#define AVB_LOOPBACK_MAX_PACKET_SIZE 2048
struct avb_loopback_packet {
uint8_t dest[6];
uint16_t type;
size_t size;
uint8_t data[AVB_LOOPBACK_MAX_PACKET_SIZE];
};
struct avb_loopback_transport {
struct avb_loopback_packet packets[AVB_LOOPBACK_MAX_PACKETS];
int packet_count;
int packet_read;
};
static inline int avb_loopback_setup(struct server *server)
{
struct avb_loopback_transport *t;
static const uint8_t test_mac[6] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 };
t = calloc(1, sizeof(*t));
if (t == NULL)
return -errno;
server->transport_data = t;
memcpy(server->mac_addr, test_mac, 6);
server->ifindex = 1;
server->entity_id = (uint64_t)server->mac_addr[0] << 56 |
(uint64_t)server->mac_addr[1] << 48 |
(uint64_t)server->mac_addr[2] << 40 |
(uint64_t)0xff << 32 |
(uint64_t)0xfe << 24 |
(uint64_t)server->mac_addr[3] << 16 |
(uint64_t)server->mac_addr[4] << 8 |
(uint64_t)server->mac_addr[5];
return 0;
}
static inline int avb_loopback_send_packet(struct server *server,
const uint8_t dest[6], uint16_t type, void *data, size_t size)
{
struct avb_loopback_transport *t = server->transport_data;
struct avb_loopback_packet *pkt;
struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
if (t->packet_count >= AVB_LOOPBACK_MAX_PACKETS)
return -ENOSPC;
if (size > AVB_LOOPBACK_MAX_PACKET_SIZE)
return -EMSGSIZE;
/* Fill in the ethernet header like the raw transport does */
memcpy(hdr->dest, dest, 6);
memcpy(hdr->src, server->mac_addr, 6);
hdr->type = htons(type);
pkt = &t->packets[t->packet_count % AVB_LOOPBACK_MAX_PACKETS];
memcpy(pkt->dest, dest, 6);
pkt->type = type;
pkt->size = size;
memcpy(pkt->data, data, size);
t->packet_count++;
return 0;
}
/**
* Return a dummy fd for protocol handlers that create their own sockets.
* Uses eventfd so pw_loop_add_io() has a valid fd to work with.
*/
static inline int avb_loopback_make_socket(struct server *server,
uint16_t type, const uint8_t mac[6])
{
int fd;
fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd < 0)
return -errno;
return fd;
}
static inline void avb_loopback_destroy(struct server *server)
{
free(server->transport_data);
server->transport_data = NULL;
}
static const struct avb_transport_ops avb_transport_loopback = {
.setup = avb_loopback_setup,
.send_packet = avb_loopback_send_packet,
.make_socket = avb_loopback_make_socket,
.destroy = avb_loopback_destroy,
};
/** Get the number of captured sent packets */
static inline int avb_loopback_get_packet_count(struct server *server)
{
struct avb_loopback_transport *t = server->transport_data;
return t->packet_count - t->packet_read;
}
/** Read the next captured sent packet, returns packet size or -1 */
static inline int avb_loopback_get_packet(struct server *server,
void *buf, size_t bufsize)
{
struct avb_loopback_transport *t = server->transport_data;
struct avb_loopback_packet *pkt;
if (t->packet_read >= t->packet_count)
return -1;
pkt = &t->packets[t->packet_read % AVB_LOOPBACK_MAX_PACKETS];
t->packet_read++;
if (pkt->size > bufsize)
return -1;
memcpy(buf, pkt->data, pkt->size);
return pkt->size;
}
/** Clear all captured packets */
static inline void avb_loopback_clear_packets(struct server *server)
{
struct avb_loopback_transport *t = server->transport_data;
t->packet_count = 0;
t->packet_read = 0;
}
#endif /* AVB_TRANSPORT_LOOPBACK_H */

View file

@ -84,7 +84,7 @@ static void on_socket_data(void *data, int fd, uint32_t mask)
} }
} }
int avb_server_send_packet(struct server *server, const uint8_t dest[6], static int raw_send_packet(struct server *server, const uint8_t dest[6],
uint16_t type, void *data, size_t size) uint16_t type, void *data, size_t size)
{ {
struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data; struct avb_ethernet_header *hdr = (struct avb_ethernet_header*)data;
@ -101,6 +101,12 @@ int avb_server_send_packet(struct server *server, const uint8_t dest[6],
return res; return res;
} }
int avb_server_send_packet(struct server *server, const uint8_t dest[6],
uint16_t type, void *data, size_t size)
{
return server->transport->send_packet(server, dest, type, data, size);
}
static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6]) static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_t mac[6])
{ {
struct sock_fprog filter; struct sock_fprog filter;
@ -136,7 +142,7 @@ static int load_filter(int fd, uint16_t eth, const uint8_t dest[6], const uint8_
return 0; return 0;
} }
int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]) static int raw_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
{ {
int fd, res; int fd, res;
struct ifreq req; struct ifreq req;
@ -209,13 +215,20 @@ error_close:
return res; return res;
} }
static int setup_socket(struct server *server) int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6])
{
if (server->transport && server->transport->make_socket)
return server->transport->make_socket(server, type, mac);
return raw_make_socket(server, type, mac);
}
static int raw_transport_setup(struct server *server)
{ {
struct impl *impl = server->impl; struct impl *impl = server->impl;
int fd, res; int fd, res;
static const uint8_t bmac[6] = AVB_BROADCAST_MAC; static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
fd = avb_server_make_socket(server, AVB_TSN_ETH, bmac); fd = raw_make_socket(server, AVB_TSN_ETH, bmac);
if (fd < 0) if (fd < 0)
return fd; return fd;
@ -244,6 +257,21 @@ error_no_source:
return res; return res;
} }
static void raw_transport_destroy(struct server *server)
{
struct impl *impl = server->impl;
if (server->source)
pw_loop_destroy_source(impl->loop, server->source);
server->source = NULL;
}
const struct avb_transport_ops avb_transport_raw = {
.setup = raw_transport_setup,
.send_packet = raw_send_packet,
.make_socket = raw_make_socket,
.destroy = raw_transport_destroy,
};
struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props) struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
{ {
struct server *server; struct server *server;
@ -269,7 +297,10 @@ struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
server->debug_messages = false; server->debug_messages = false;
if ((res = setup_socket(server)) < 0) if (server->transport == NULL)
server->transport = &avb_transport_raw;
if ((res = server->transport->setup(server)) < 0)
goto error_free; goto error_free;
@ -315,12 +346,10 @@ void avdecc_server_add_listener(struct server *server, struct spa_hook *listener
void avdecc_server_free(struct server *server) void avdecc_server_free(struct server *server)
{ {
struct impl *impl = server->impl;
server_destroy_descriptors(server); server_destroy_descriptors(server);
spa_list_remove(&server->link); spa_list_remove(&server->link);
if (server->source) if (server->transport)
pw_loop_destroy_source(impl->loop, server->source); server->transport->destroy(server);
pw_timer_queue_cancel(&server->timer); pw_timer_queue_cancel(&server->timer);
spa_hook_list_clean(&server->listener_list); spa_hook_list_clean(&server->listener_list);
free(server->ifname); free(server->ifname);

View file

@ -17,6 +17,15 @@ struct avb_mrp;
#define AVB_TSN_ETH 0x22f0 #define AVB_TSN_ETH 0x22f0
#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }; #define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
struct avb_transport_ops {
int (*setup)(struct server *server);
int (*send_packet)(struct server *server, const uint8_t dest[6],
uint16_t type, void *data, size_t size);
int (*make_socket)(struct server *server, uint16_t type,
const uint8_t mac[6]);
void (*destroy)(struct server *server);
};
struct impl { struct impl {
struct pw_loop *loop; struct pw_loop *loop;
struct pw_timer_queue *timer_queue; struct pw_timer_queue *timer_queue;
@ -77,6 +86,9 @@ struct server {
uint64_t entity_id; uint64_t entity_id;
int ifindex; int ifindex;
const struct avb_transport_ops *transport;
void *transport_data;
struct spa_source *source; struct spa_source *source;
struct pw_timer timer; struct pw_timer timer;
@ -102,7 +114,6 @@ static inline void server_destroy_descriptors(struct server *server)
struct descriptor *d, *t; struct descriptor *d, *t;
spa_list_for_each_safe(d, t, &server->descriptors, link) { spa_list_for_each_safe(d, t, &server->descriptors, link) {
free(d->ptr);
spa_list_remove(&d->link); spa_list_remove(&d->link);
free(d); free(d);
} }
@ -145,6 +156,8 @@ void avdecc_server_free(struct server *server);
void avdecc_server_add_listener(struct server *server, struct spa_hook *listener, void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
const struct server_events *events, void *data); const struct server_events *events, void *data);
extern const struct avb_transport_ops avb_transport_raw;
int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]); int avb_server_make_socket(struct server *server, uint16_t type, const uint8_t mac[6]);
int avb_server_send_packet(struct server *server, const uint8_t dest[6], int avb_server_send_packet(struct server *server, const uint8_t dest[6],

View file

@ -302,6 +302,8 @@ const char *avb_mrp_notify_name(uint8_t notify)
const char *avb_mrp_send_name(uint8_t send) const char *avb_mrp_send_name(uint8_t send)
{ {
switch(send) { switch(send) {
case 0:
return "none";
case AVB_MRP_SEND_NEW: case AVB_MRP_SEND_NEW:
return "new"; return "new";
case AVB_MRP_SEND_JOININ: case AVB_MRP_SEND_JOININ:

View file

@ -88,13 +88,13 @@ struct avb_packet_mrp_footer {
#define AVB_MRP_ATTRIBUTE_EVENT_LV 5 #define AVB_MRP_ATTRIBUTE_EVENT_LV 5
#define AVB_MRP_ATTRIBUTE_EVENT_LVA 6 #define AVB_MRP_ATTRIBUTE_EVENT_LVA 6
#define AVB_MRP_SEND_NEW 0 #define AVB_MRP_SEND_NEW 1
#define AVB_MRP_SEND_JOININ 1 #define AVB_MRP_SEND_JOININ 2
#define AVB_MRP_SEND_IN 2 #define AVB_MRP_SEND_IN 3
#define AVB_MRP_SEND_JOINMT 3 #define AVB_MRP_SEND_JOINMT 4
#define AVB_MRP_SEND_MT 4 #define AVB_MRP_SEND_MT 5
#define AVB_MRP_SEND_LV 5 #define AVB_MRP_SEND_LV 6
#define AVB_MRP_SEND_LVA 6 #define AVB_MRP_SEND_LVA 7
#define AVB_MRP_NOTIFY_NEW 1 #define AVB_MRP_NOTIFY_NEW 1
#define AVB_MRP_NOTIFY_JOIN 2 #define AVB_MRP_NOTIFY_JOIN 2

View file

@ -91,7 +91,7 @@ static int encode_talker(struct msrp *msrp, struct attr *a, void *m)
*t = a->attr.attr.talker; *t = a->attr.attr.talker;
ev = SPA_PTROFF(t, sizeof(*t), uint8_t); ev = SPA_PTROFF(t, sizeof(*t), uint8_t);
*ev = a->attr.mrp->pending_send * 6 * 6; *ev = (a->attr.mrp->pending_send - 1) * 6 * 6;
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
f->end_mark = 0; f->end_mark = 0;
@ -170,7 +170,7 @@ static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
*l = a->attr.attr.listener; *l = a->attr.attr.listener;
ev = SPA_PTROFF(l, sizeof(*l), uint8_t); ev = SPA_PTROFF(l, sizeof(*l), uint8_t);
*ev = a->attr.mrp->pending_send * 6 * 6; *ev = (a->attr.mrp->pending_send - 1) * 6 * 6;
ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t); ev = SPA_PTROFF(ev, sizeof(*ev), uint8_t);
*ev = a->attr.param * 4 * 4 * 4; *ev = a->attr.param * 4 * 4 * 4;
@ -226,7 +226,7 @@ static int encode_domain(struct msrp *msrp, struct attr *a, void *m)
*d = a->attr.attr.domain; *d = a->attr.attr.domain;
ev = SPA_PTROFF(d, sizeof(*d), uint8_t); ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
*ev = a->attr.mrp->pending_send * 36; *ev = (a->attr.mrp->pending_send - 1) * 36;
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
f->end_mark = 0; f->end_mark = 0;
@ -332,7 +332,8 @@ static void msrp_notify(void *data, uint64_t now, uint8_t notify)
{ {
struct attr *a = data; struct attr *a = data;
struct msrp *msrp = a->msrp; struct msrp *msrp = a->msrp;
return dispatch[a->attr.type].notify(msrp, now, a, notify); if (dispatch[a->attr.type].notify)
dispatch[a->attr.type].notify(msrp, now, a, notify);
} }
static const struct avb_mrp_attribute_events mrp_attr_events = { static const struct avb_mrp_attribute_events mrp_attr_events = {

View file

@ -84,7 +84,7 @@ static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
*d = a->attr.attr.vid; *d = a->attr.attr.vid;
ev = SPA_PTROFF(d, sizeof(*d), uint8_t); ev = SPA_PTROFF(d, sizeof(*d), uint8_t);
*ev = a->attr.mrp->pending_send * 36; *ev = (a->attr.mrp->pending_send - 1) * 36;
f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer); f = SPA_PTROFF(ev, sizeof(*ev), struct avb_packet_mrp_footer);
f->end_mark = 0; f->end_mark = 0;
@ -171,7 +171,8 @@ static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
{ {
struct attr *a = data; struct attr *a = data;
struct mvrp *mvrp = a->mvrp; struct mvrp *mvrp = a->mvrp;
return dispatch[a->attr.type].notify(mvrp, now, a, notify); if (dispatch[a->attr.type].notify)
dispatch[a->attr.type].notify(mvrp, now, a, notify);
} }
static const struct avb_mrp_attribute_events mrp_attr_events = { static const struct avb_mrp_attribute_events mrp_attr_events = {

143
test/avb-bugs.md Normal file
View file

@ -0,0 +1,143 @@
# AVB Module Bugs Found via Test Suite
The following bugs were discovered by building a software test harness
for the AVB protocol stack. All have been fixed in the accompanying
patch series.
## 1. Heap corruption in server_destroy_descriptors
**File:** `src/modules/module-avb/internal.h`
**Commit:** `69c721006`
`server_destroy_descriptors()` called `free(d->ptr)` followed by
`free(d)`, but `d->ptr` points into the same allocation as `d`
(set via `SPA_PTROFF(d, sizeof(struct descriptor), void)` in
`server_add_descriptor()`). This is a double-free / heap corruption
that could cause crashes or memory corruption when tearing down an
AVB server.
**Fix:** Remove the erroneous `free(d->ptr)` call.
## 2. NULL pointer dereference in MSRP notify dispatch
**File:** `src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c`
**Commit:** `b056e9f85`
`msrp_notify()` unconditionally calls `dispatch[a->attr.type].notify()`
but the dispatch table entry for `AVB_MSRP_ATTRIBUTE_TYPE_TALKER_FAILED`
has `notify = NULL`. If a talker-failed attribute receives a registrar
state change (e.g., `RX_NEW` triggers `NOTIFY_NEW`), this crashes with
a NULL pointer dereference. The same unguarded pattern exists in
`mvrp_notify()`.
**Fix:** Add `if (dispatch[a->attr.type].notify)` NULL check before
calling, matching the defensive pattern already used in the encode path.
## 3. MRP NEW messages never transmitted
**File:** `src/modules/module-avb/mrp.h`, `src/modules/module-avb/mrp.c`,
`src/modules/module-avb/msrp.c`, `src/modules/module-avb/mvrp.c`
**Commit:** `bc2c41daa`
`AVB_MRP_SEND_NEW` was defined as `0`. The MSRP and MVRP event handlers
skip attributes with `if (!a->attr.mrp->pending_send)`, treating `0` as
"no pending send". Since the MRP state machine sets `pending_send` to
`AVB_MRP_SEND_NEW` (0) when an attribute in state VN or AN receives a
TX event, NEW messages were silently dropped instead of being
transmitted. This violates IEEE 802.1Q which requires NEW messages to
be sent when an attribute is first declared.
In practice, the attribute would cycle through VN -> AN -> AA over
successive TX events, eventually sending a JOINMT instead of the
initial NEW. The protocol still functioned because JOINMT also
registers the attribute, but the initial declaration was lost.
**Fix:** Shift all `AVB_MRP_SEND_*` values to start at 1, so that 0
unambiguously means "no send pending". Update MSRP and MVRP encoders
to subtract 1 when encoding to the IEEE 802.1Q wire format.
## 4. ACMP error responses sent with wrong message type
**File:** `src/modules/module-avb/acmp.c`
**Commit:** `9f4147104`
In `handle_connect_tx_command()` and `handle_disconnect_tx_command()`,
`AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` is called after the `goto done`
jump target. When `find_stream()` fails (returns NULL), the code jumps
to `done:` without setting the message type, so the error response is
sent with the original command message type (e.g.,
`CONNECT_TX_COMMAND = 0`) instead of the correct response type
(`CONNECT_TX_RESPONSE = 1`).
A controller receiving this malformed response would not recognize it
as a response to its command and would eventually time out.
**Fix:** Move `AVB_PACKET_ACMP_SET_MESSAGE_TYPE()` before the
`find_stream()` call so the response type is always set correctly.
## 5. ACMP pending_destroy skips controller cleanup
**File:** `src/modules/module-avb/acmp.c`
**Commit:** `9f4147104`
`pending_destroy()` iterates with `list_id < PENDING_CONTROLLER`
(where `PENDING_CONTROLLER = 2`), which only cleans up
`PENDING_TALKER` (0) and `PENDING_LISTENER` (1) lists, skipping
`PENDING_CONTROLLER` (2). Any pending controller requests leak on
shutdown.
**Fix:** Change `<` to `<=` to include the controller list.
## 6. Legacy AECP handlers read payload at wrong offset
**File:** `src/modules/module-avb/aecp-aem.c`
`handle_acquire_entity_avb_legacy()` and `handle_lock_entity_avb_legacy()`
assign `const struct avb_packet_aecp_aem *p = m;` where `m` is the full
ethernet frame (starting with `struct avb_ethernet_header`). The handlers
then access `p->payload` to read the acquire/lock fields, but this reads
from `m + offsetof(avb_packet_aecp_aem, payload)` instead of the correct
`m + sizeof(avb_ethernet_header) + offsetof(avb_packet_aecp_aem, payload)`.
This causes `descriptor_type` and `descriptor_id` to be read from the
wrong position, leading to incorrect descriptor lookups.
All other AEM command handlers (e.g., `handle_read_descriptor_common`)
correctly derive `p` via `SPA_PTROFF(h, sizeof(*h), void)`.
**Fix:** Change `const struct avb_packet_aecp_aem *p = m;` to properly
skip the ethernet header:
```c
const struct avb_ethernet_header *h = m;
const struct avb_packet_aecp_aem *p = SPA_PTROFF(h, sizeof(*h), void);
```
## 7. Milan LOCK_ENTITY error response uses wrong packet pointer
**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c`
In `handle_cmd_lock_entity_milan_v12()`, when `server_find_descriptor()`
returns NULL, `reply_status()` is called with `p` (the AEM packet pointer
past the ethernet header) instead of `m` (the full ethernet frame).
`reply_status()` assumes its third argument is the full frame and casts
it as `struct avb_ethernet_header *`. With the wrong pointer, the
response ethernet header (including destination MAC) is corrupted.
**Fix:** Change `reply_status(aecp, ..., p, len)` to
`reply_status(aecp, ..., m, len)`.
## 8. Lock entity re-lock timeout uses wrong units
**File:** `src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c`
When a controller that already holds the lock sends another lock request
(to refresh it), the expire timeout is extended by:
```c
lock->base_info.expire_timeout +=
AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND;
```
`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND` is `60` (raw seconds),
but `expire_timeout` is in nanoseconds. This adds only 60 nanoseconds
instead of 60 seconds. The initial lock correctly uses
`AECP_AEM_LOCK_ENTITY_EXPIRE_TIMEOUT_SECOND * SPA_NSEC_PER_SEC`.
**Fix:** Multiply by `SPA_NSEC_PER_SEC` to match the nanosecond units.

View file

@ -163,3 +163,42 @@ if valgrind.found()
env : valgrind_env, env : valgrind_env,
timeout_multiplier : 3) timeout_multiplier : 3)
endif endif
if build_module_avb
avb_test_inc = [pwtest_inc, include_directories('../src/modules')]
avb_module_sources = [
'../src/modules/module-avb/avb.c',
'../src/modules/module-avb/adp.c',
'../src/modules/module-avb/acmp.c',
'../src/modules/module-avb/aecp.c',
'../src/modules/module-avb/aecp-aem.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-available.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c',
'../src/modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c',
'../src/modules/module-avb/es-builder.c',
'../src/modules/module-avb/avdecc.c',
'../src/modules/module-avb/descriptors.c',
'../src/modules/module-avb/maap.c',
'../src/modules/module-avb/mmrp.c',
'../src/modules/module-avb/mrp.c',
'../src/modules/module-avb/msrp.c',
'../src/modules/module-avb/mvrp.c',
'../src/modules/module-avb/srp.c',
'../src/modules/module-avb/stream.c',
]
test('test-avb',
executable('test-avb',
['test-avb.c'] + avb_module_sources,
include_directories: avb_test_inc,
dependencies: [spa_dep, pipewire_dep, mathlib, dl_lib, rt_lib],
link_with: pwtest_lib)
)
endif

356
test/test-avb-utils.h Normal file
View file

@ -0,0 +1,356 @@
/* AVB test utilities */
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
/* SPDX-License-Identifier: MIT */
#ifndef TEST_AVB_UTILS_H
#define TEST_AVB_UTILS_H
#include <pipewire/pipewire.h>
#include "module-avb/internal.h"
#include "module-avb/packets.h"
#include "module-avb/adp.h"
#include "module-avb/acmp.h"
#include "module-avb/mrp.h"
#include "module-avb/msrp.h"
#include "module-avb/mvrp.h"
#include "module-avb/mmrp.h"
#include "module-avb/maap.h"
#include "module-avb/aecp.h"
#include "module-avb/aecp-aem.h"
#include "module-avb/aecp-aem-descriptors.h"
#include "module-avb/aecp-aem-state.h"
#include "module-avb/iec61883.h"
#include "module-avb/aaf.h"
#include "module-avb/stream.h"
#include "module-avb/descriptors.h"
#include "module-avb/avb-transport-loopback.h"
#define server_emit_message(s,n,m,l) \
spa_hook_list_call(&(s)->listener_list, struct server_events, message, 0, n, m, l)
#define server_emit_periodic(s,n) \
spa_hook_list_call(&(s)->listener_list, struct server_events, periodic, 0, n)
/**
* Create a test AVB server with loopback transport.
* All protocol handlers are registered. No network access required.
*/
static inline struct server *avb_test_server_new(struct impl *impl)
{
struct server *server;
server = calloc(1, sizeof(*server));
if (server == NULL)
return NULL;
server->impl = impl;
server->ifname = strdup("test0");
server->avb_mode = AVB_MODE_LEGACY;
server->transport = &avb_transport_loopback;
spa_list_append(&impl->servers, &server->link);
spa_hook_list_init(&server->listener_list);
spa_list_init(&server->descriptors);
spa_list_init(&server->streams);
if (server->transport->setup(server) < 0)
goto error;
server->mrp = avb_mrp_new(server);
if (server->mrp == NULL)
goto error;
avb_aecp_register(server);
server->maap = avb_maap_register(server);
server->mmrp = avb_mmrp_register(server);
server->msrp = avb_msrp_register(server);
server->mvrp = avb_mvrp_register(server);
avb_adp_register(server);
avb_acmp_register(server);
server->domain_attr = avb_msrp_attribute_new(server->msrp,
AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
/* Add a minimal entity descriptor so ADP can advertise.
* We skip init_descriptors() because it creates streams that
* need a pw_core connection. */
{
struct avb_aem_desc_entity entity;
memset(&entity, 0, sizeof(entity));
entity.entity_id = htobe64(server->entity_id);
entity.entity_model_id = htobe64(0x0001000000000001ULL);
entity.entity_capabilities = htonl(
AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED);
entity.talker_stream_sources = htons(1);
entity.talker_capabilities = htons(
AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE);
entity.listener_stream_sinks = htons(1);
entity.listener_capabilities = htons(
AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK);
entity.configurations_count = htons(1);
server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
sizeof(entity), &entity);
}
return server;
error:
free(server->ifname);
free(server);
return NULL;
}
static inline void avb_test_server_free(struct server *server)
{
avdecc_server_free(server);
}
/**
* Inject a raw packet into the server's protocol dispatch.
* This simulates receiving a packet from the network.
*/
static inline void avb_test_inject_packet(struct server *server,
uint64_t now, const void *data, int len)
{
server_emit_message(server, now, data, len);
}
/**
* Trigger the periodic callback with a given timestamp.
* Use this to advance time and test timeout/readvertise logic.
*/
static inline void avb_test_tick(struct server *server, uint64_t now)
{
server_emit_periodic(server, now);
}
/**
* Build an ADP entity available packet.
* Returns the packet size, or -1 on error.
*/
static inline int avb_test_build_adp_entity_available(
uint8_t *buf, size_t bufsize,
const uint8_t src_mac[6],
uint64_t entity_id,
int valid_time)
{
struct avb_ethernet_header *h;
struct avb_packet_adp *p;
size_t len = sizeof(*h) + sizeof(*p);
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
if (bufsize < len)
return -1;
memset(buf, 0, len);
h = (struct avb_ethernet_header *)buf;
memcpy(h->dest, bmac, 6);
memcpy(h->src, src_mac, 6);
h->type = htons(AVB_TSN_ETH);
p = (struct avb_packet_adp *)(buf + sizeof(*h));
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_AVAILABLE);
AVB_PACKET_ADP_SET_VALID_TIME(p, valid_time);
p->entity_id = htobe64(entity_id);
return len;
}
/**
* Build an ADP entity departing packet.
*/
static inline int avb_test_build_adp_entity_departing(
uint8_t *buf, size_t bufsize,
const uint8_t src_mac[6],
uint64_t entity_id)
{
struct avb_ethernet_header *h;
struct avb_packet_adp *p;
size_t len = sizeof(*h) + sizeof(*p);
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
if (bufsize < len)
return -1;
memset(buf, 0, len);
h = (struct avb_ethernet_header *)buf;
memcpy(h->dest, bmac, 6);
memcpy(h->src, src_mac, 6);
h->type = htons(AVB_TSN_ETH);
p = (struct avb_packet_adp *)(buf + sizeof(*h));
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DEPARTING);
p->entity_id = htobe64(entity_id);
return len;
}
/**
* Build an ADP entity discover packet.
*/
static inline int avb_test_build_adp_entity_discover(
uint8_t *buf, size_t bufsize,
const uint8_t src_mac[6],
uint64_t entity_id)
{
struct avb_ethernet_header *h;
struct avb_packet_adp *p;
size_t len = sizeof(*h) + sizeof(*p);
static const uint8_t bmac[6] = AVB_BROADCAST_MAC;
if (bufsize < len)
return -1;
memset(buf, 0, len);
h = (struct avb_ethernet_header *)buf;
memcpy(h->dest, bmac, 6);
memcpy(h->src, src_mac, 6);
h->type = htons(AVB_TSN_ETH);
p = (struct avb_packet_adp *)(buf + sizeof(*h));
AVB_PACKET_SET_SUBTYPE(&p->hdr, AVB_SUBTYPE_ADP);
AVB_PACKET_SET_LENGTH(&p->hdr, AVB_ADP_CONTROL_DATA_LENGTH);
AVB_PACKET_ADP_SET_MESSAGE_TYPE(p, AVB_ADP_MESSAGE_TYPE_ENTITY_DISCOVER);
p->entity_id = htobe64(entity_id);
return len;
}
/**
* Build an AECP AEM command packet for injection.
* Returns packet size, or -1 on error.
*/
static inline int avb_test_build_aecp_aem(
uint8_t *buf, size_t bufsize,
const uint8_t src_mac[6],
uint64_t target_guid,
uint64_t controller_guid,
uint16_t sequence_id,
uint16_t command_type,
const void *payload, size_t payload_size)
{
struct avb_ethernet_header *h;
struct avb_packet_aecp_aem *p;
size_t len = sizeof(*h) + sizeof(*p) + payload_size;
if (bufsize < len)
return -1;
memset(buf, 0, len);
h = (struct avb_ethernet_header *)buf;
memcpy(h->dest, (uint8_t[]){ 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 }, 6);
memcpy(h->src, src_mac, 6);
h->type = htons(AVB_TSN_ETH);
p = SPA_PTROFF(h, sizeof(*h), void);
AVB_PACKET_SET_SUBTYPE(&p->aecp.hdr, AVB_SUBTYPE_AECP);
AVB_PACKET_AECP_SET_MESSAGE_TYPE(&p->aecp, AVB_AECP_MESSAGE_TYPE_AEM_COMMAND);
AVB_PACKET_AECP_SET_STATUS(&p->aecp, 0);
AVB_PACKET_SET_LENGTH(&p->aecp.hdr, payload_size + 12);
p->aecp.target_guid = htobe64(target_guid);
p->aecp.controller_guid = htobe64(controller_guid);
p->aecp.sequence_id = htons(sequence_id);
AVB_PACKET_AEM_SET_COMMAND_TYPE(p, command_type);
if (payload && payload_size > 0)
memcpy(p->payload, payload, payload_size);
return len;
}
/**
* Create a test AVB server in Milan v1.2 mode with loopback transport.
* The entity descriptor is properly sized for Milan state (lock, unsol).
*/
static inline struct server *avb_test_server_new_milan(struct impl *impl)
{
struct server *server;
server = calloc(1, sizeof(*server));
if (server == NULL)
return NULL;
server->impl = impl;
server->ifname = strdup("test0");
server->avb_mode = AVB_MODE_MILAN_V12;
server->transport = &avb_transport_loopback;
spa_list_append(&impl->servers, &server->link);
spa_hook_list_init(&server->listener_list);
spa_list_init(&server->descriptors);
spa_list_init(&server->streams);
if (server->transport->setup(server) < 0)
goto error;
server->mrp = avb_mrp_new(server);
if (server->mrp == NULL)
goto error;
avb_aecp_register(server);
server->maap = avb_maap_register(server);
server->mmrp = avb_mmrp_register(server);
server->msrp = avb_msrp_register(server);
server->mvrp = avb_mvrp_register(server);
avb_adp_register(server);
avb_acmp_register(server);
server->domain_attr = avb_msrp_attribute_new(server->msrp,
AVB_MSRP_ATTRIBUTE_TYPE_DOMAIN);
server->domain_attr->attr.domain.sr_class_id = AVB_MSRP_CLASS_ID_DEFAULT;
server->domain_attr->attr.domain.sr_class_priority = AVB_MSRP_PRIORITY_DEFAULT;
server->domain_attr->attr.domain.sr_class_vid = htons(AVB_DEFAULT_VLAN);
avb_mrp_attribute_begin(server->domain_attr->mrp, 0);
avb_mrp_attribute_join(server->domain_attr->mrp, 0, true);
/* Add Milan-sized entity descriptor with lock/unsol state */
{
struct aecp_aem_entity_milan_state entity_state;
memset(&entity_state, 0, sizeof(entity_state));
entity_state.state.desc.entity_id = htobe64(server->entity_id);
entity_state.state.desc.entity_model_id = htobe64(0x0001000000000001ULL);
entity_state.state.desc.entity_capabilities = htonl(
AVB_ADP_ENTITY_CAPABILITY_AEM_SUPPORTED |
AVB_ADP_ENTITY_CAPABILITY_CLASS_A_SUPPORTED |
AVB_ADP_ENTITY_CAPABILITY_GPTP_SUPPORTED);
entity_state.state.desc.talker_stream_sources = htons(1);
entity_state.state.desc.talker_capabilities = htons(
AVB_ADP_TALKER_CAPABILITY_IMPLEMENTED |
AVB_ADP_TALKER_CAPABILITY_AUDIO_SOURCE);
entity_state.state.desc.listener_stream_sinks = htons(1);
entity_state.state.desc.listener_capabilities = htons(
AVB_ADP_LISTENER_CAPABILITY_IMPLEMENTED |
AVB_ADP_LISTENER_CAPABILITY_AUDIO_SINK);
entity_state.state.desc.configurations_count = htons(1);
server_add_descriptor(server, AVB_AEM_DESC_ENTITY, 0,
sizeof(entity_state), &entity_state);
}
return server;
error:
free(server->ifname);
free(server);
return NULL;
}
#endif /* TEST_AVB_UTILS_H */

4017
test/test-avb.c Normal file

File diff suppressed because it is too large Load diff