Merge branch 'avb-virtual-tool' into 'master'

module-avb: fix heap corruption in server_destroy_descriptors

See merge request pipewire/pipewire!2779
This commit is contained in:
Christian Fredrik Kalager Schaller 2026-04-09 07:43:25 +00:00
commit 696e7c5d99
18 changed files with 5286 additions and 108 deletions

View file

@ -174,13 +174,14 @@ static int handle_connect_tx_command(struct acmp *acmp, uint64_t now, const void
return 0;
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));
if (stream == NULL) {
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
goto done;
}
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_CONNECT_TX_RESPONSE);
reply->stream_id = htobe64(stream->id);
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;
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));
if (stream == NULL) {
status = AVB_ACMP_STATUS_TALKER_NO_STREAM_INDEX;
goto done;
}
AVB_PACKET_ACMP_SET_MESSAGE_TYPE(reply, AVB_ACMP_MESSAGE_TYPE_DISCONNECT_TX_RESPONSE);
stream_deactivate(stream, now);
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);
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;
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 (ctrler_id == lock->locked_id) {
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;
} 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)
{
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 descriptor *desc;
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)
{
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 descriptor *desc;
uint16_t desc_type, desc_id;

View file

@ -0,0 +1,184 @@
/* 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 <linux/if_ether.h>
#include <linux/if_packet.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;
}
/**
* Create a dummy stream socket using eventfd.
* No AF_PACKET, no ioctls, no privileges needed.
*/
static inline int avb_loopback_stream_setup_socket(struct server *server,
struct stream *stream)
{
int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (fd < 0)
return -errno;
spa_zero(stream->sock_addr);
stream->sock_addr.sll_family = AF_PACKET;
stream->sock_addr.sll_halen = ETH_ALEN;
return fd;
}
/**
* No-op stream send pretend the send succeeded.
* Audio data is consumed from the ringbuffer but goes nowhere.
*/
static inline ssize_t avb_loopback_stream_send(struct server *server,
struct stream *stream, struct msghdr *msg, int flags)
{
ssize_t total = 0;
for (size_t i = 0; i < msg->msg_iovlen; i++)
total += msg->msg_iov[i].iov_len;
return total;
}
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,
.stream_setup_socket = avb_loopback_stream_setup_socket,
.stream_send = avb_loopback_stream_send,
};
/** 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)
{
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;
}
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])
{
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;
}
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;
struct ifreq req;
@ -209,13 +215,20 @@ error_close:
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;
int fd, res;
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)
return fd;
@ -244,6 +257,119 @@ error_no_source:
return res;
}
static int raw_stream_setup_socket(struct server *server, struct stream *stream)
{
int fd, res;
char buf[128];
struct ifreq req;
fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
if (fd < 0) {
pw_log_error("socket() failed: %m");
return -errno;
}
spa_zero(req);
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
res = ioctl(fd, SIOCGIFINDEX, &req);
if (res < 0) {
pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
res = -errno;
goto error_close;
}
spa_zero(stream->sock_addr);
stream->sock_addr.sll_family = AF_PACKET;
stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
stream->sock_addr.sll_ifindex = req.ifr_ifindex;
if (stream->direction == SPA_DIRECTION_OUTPUT) {
struct sock_txtime txtime_cfg;
res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
sizeof(stream->prio));
if (res < 0) {
pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
res = -errno;
goto error_close;
}
txtime_cfg.clockid = CLOCK_TAI;
txtime_cfg.flags = 0;
res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
sizeof(txtime_cfg));
if (res < 0) {
pw_log_error("setsockopt(SO_TXTIME) failed: %m");
res = -errno;
goto error_close;
}
} else {
struct packet_mreq mreq;
res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
if (res < 0) {
pw_log_error("bind() failed: %m");
res = -errno;
goto error_close;
}
spa_zero(mreq);
mreq.mr_ifindex = req.ifr_ifindex;
mreq.mr_type = PACKET_MR_MULTICAST;
mreq.mr_alen = ETH_ALEN;
memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
&mreq, sizeof(struct packet_mreq));
pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
if (res < 0) {
pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
res = -errno;
goto error_close;
}
}
return fd;
error_close:
close(fd);
return res;
}
static ssize_t raw_stream_send(struct server *server, struct stream *stream,
struct msghdr *msg, int flags)
{
return sendmsg(stream->source->fd, msg, flags);
}
int avb_server_stream_setup_socket(struct server *server, struct stream *stream)
{
return server->transport->stream_setup_socket(server, stream);
}
ssize_t avb_server_stream_send(struct server *server, struct stream *stream,
struct msghdr *msg, int flags)
{
return server->transport->stream_send(server, stream, msg, flags);
}
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,
.stream_setup_socket = raw_stream_setup_socket,
.stream_send = raw_stream_send,
};
struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
{
struct server *server;
@ -266,10 +392,14 @@ struct server *avdecc_server_new(struct impl *impl, struct spa_dict *props)
spa_hook_list_init(&server->listener_list);
spa_list_init(&server->descriptors);
spa_list_init(&server->streams);
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;
@ -315,12 +445,10 @@ void avdecc_server_add_listener(struct server *server, struct spa_hook *listener
void avdecc_server_free(struct server *server)
{
struct impl *impl = server->impl;
server_destroy_descriptors(server);
spa_list_remove(&server->link);
if (server->source)
pw_loop_destroy_source(impl->loop, server->source);
if (server->transport)
server->transport->destroy(server);
pw_timer_queue_cancel(&server->timer);
spa_hook_list_clean(&server->listener_list);
free(server->ifname);

View file

@ -5,6 +5,8 @@
#ifndef AVB_INTERNAL_H
#define AVB_INTERNAL_H
#include <sys/socket.h>
#include <pipewire/pipewire.h>
#ifdef __cplusplus
@ -17,6 +19,22 @@ struct avb_mrp;
#define AVB_TSN_ETH 0x22f0
#define AVB_BROADCAST_MAC { 0x91, 0xe0, 0xf0, 0x01, 0x00, 0x00 };
struct stream;
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);
/* stream data plane ops */
int (*stream_setup_socket)(struct server *server, struct stream *stream);
ssize_t (*stream_send)(struct server *server, struct stream *stream,
struct msghdr *msg, int flags);
};
struct impl {
struct pw_loop *loop;
struct pw_timer_queue *timer_queue;
@ -77,12 +95,16 @@ struct server {
uint64_t entity_id;
int ifindex;
const struct avb_transport_ops *transport;
void *transport_data;
struct spa_source *source;
struct pw_timer timer;
struct spa_hook_list listener_list;
struct spa_list descriptors;
struct spa_list streams;
unsigned debug_messages:1;
@ -102,7 +124,6 @@ static inline void server_destroy_descriptors(struct server *server)
struct descriptor *d, *t;
spa_list_for_each_safe(d, t, &server->descriptors, link) {
free(d->ptr);
spa_list_remove(&d->link);
free(d);
}
@ -145,11 +166,17 @@ void avdecc_server_free(struct server *server);
void avdecc_server_add_listener(struct server *server, struct spa_hook *listener,
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_send_packet(struct server *server, const uint8_t dest[6],
uint16_t type, void *data, size_t size);
int avb_server_stream_setup_socket(struct server *server, struct stream *stream);
ssize_t avb_server_stream_send(struct server *server, struct stream *stream,
struct msghdr *msg, int flags);
struct aecp {
struct server *server;
struct spa_hook server_listener;

View file

@ -302,6 +302,8 @@ const char *avb_mrp_notify_name(uint8_t notify)
const char *avb_mrp_send_name(uint8_t send)
{
switch(send) {
case 0:
return "none";
case AVB_MRP_SEND_NEW:
return "new";
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_LVA 6
#define AVB_MRP_SEND_NEW 0
#define AVB_MRP_SEND_JOININ 1
#define AVB_MRP_SEND_IN 2
#define AVB_MRP_SEND_JOINMT 3
#define AVB_MRP_SEND_MT 4
#define AVB_MRP_SEND_LV 5
#define AVB_MRP_SEND_LVA 6
#define AVB_MRP_SEND_NEW 1
#define AVB_MRP_SEND_JOININ 2
#define AVB_MRP_SEND_IN 3
#define AVB_MRP_SEND_JOINMT 4
#define AVB_MRP_SEND_MT 5
#define AVB_MRP_SEND_LV 6
#define AVB_MRP_SEND_LVA 7
#define AVB_MRP_NOTIFY_NEW 1
#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;
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->end_mark = 0;
@ -170,7 +170,7 @@ static int encode_listener(struct msrp *msrp, struct attr *a, void *m)
*l = a->attr.attr.listener;
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 = 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;
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->end_mark = 0;
@ -332,7 +332,8 @@ static void msrp_notify(void *data, uint64_t now, uint8_t notify)
{
struct attr *a = data;
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 = {

View file

@ -84,7 +84,7 @@ static int encode_vid(struct mvrp *mvrp, struct attr *a, void *m)
*d = a->attr.attr.vid;
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->end_mark = 0;
@ -171,7 +171,8 @@ static void mvrp_notify(void *data, uint64_t now, uint8_t notify)
{
struct attr *a = data;
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 = {

View file

@ -116,9 +116,10 @@ static int flush_write(struct stream *stream, uint64_t current_time)
p->timestamp = ptime;
p->dbc = dbc;
n = sendmsg(stream->source->fd, &stream->msg, MSG_NOSIGNAL);
n = avb_server_stream_send(stream->server, stream,
&stream->msg, MSG_NOSIGNAL);
if (n < 0 || n != (ssize_t)stream->pdu_size) {
pw_log_error("sendmsg() failed %zd != %zd: %m",
pw_log_error("stream send failed %zd != %zd: %m",
n, stream->pdu_size);
}
txtime += stream->pdu_period;
@ -331,6 +332,8 @@ struct stream *server_create_stream(struct server *server, struct stream *stream
stream->talker_attr->attr.talker.rank = AVB_MSRP_RANK_DEFAULT;
stream->talker_attr->attr.talker.accumulated_latency = htonl(95);
spa_list_append(&server->streams, &stream->link);
return stream;
error_free_stream:
@ -348,82 +351,7 @@ void stream_destroy(struct stream *stream)
static int setup_socket(struct stream *stream)
{
struct server *server = stream->server;
int fd, res;
char buf[128];
struct ifreq req;
fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ALL));
if (fd < 0) {
pw_log_error("socket() failed: %m");
return -errno;
}
spa_zero(req);
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", server->ifname);
res = ioctl(fd, SIOCGIFINDEX, &req);
if (res < 0) {
pw_log_error("SIOCGIFINDEX %s failed: %m", server->ifname);
res = -errno;
goto error_close;
}
spa_zero(stream->sock_addr);
stream->sock_addr.sll_family = AF_PACKET;
stream->sock_addr.sll_protocol = htons(ETH_P_TSN);
stream->sock_addr.sll_ifindex = req.ifr_ifindex;
if (stream->direction == SPA_DIRECTION_OUTPUT) {
struct sock_txtime txtime_cfg;
res = setsockopt(fd, SOL_SOCKET, SO_PRIORITY, &stream->prio,
sizeof(stream->prio));
if (res < 0) {
pw_log_error("setsockopt(SO_PRIORITY %d) failed: %m", stream->prio);
res = -errno;
goto error_close;
}
txtime_cfg.clockid = CLOCK_TAI;
txtime_cfg.flags = 0;
res = setsockopt(fd, SOL_SOCKET, SO_TXTIME, &txtime_cfg,
sizeof(txtime_cfg));
if (res < 0) {
pw_log_error("setsockopt(SO_TXTIME) failed: %m");
res = -errno;
goto error_close;
}
} else {
struct packet_mreq mreq;
res = bind(fd, (struct sockaddr *) &stream->sock_addr, sizeof(stream->sock_addr));
if (res < 0) {
pw_log_error("bind() failed: %m");
res = -errno;
goto error_close;
}
spa_zero(mreq);
mreq.mr_ifindex = req.ifr_ifindex;
mreq.mr_type = PACKET_MR_MULTICAST;
mreq.mr_alen = ETH_ALEN;
memcpy(&mreq.mr_address, stream->addr, ETH_ALEN);
res = setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP,
&mreq, sizeof(struct packet_mreq));
pw_log_info("join %s", avb_utils_format_addr(buf, 128, stream->addr));
if (res < 0) {
pw_log_error("setsockopt(ADD_MEMBERSHIP) failed: %m");
res = -errno;
goto error_close;
}
}
return fd;
error_close:
close(fd);
return res;
return avb_server_stream_setup_socket(stream->server, stream);
}
static void handle_iec61883_packet(struct stream *stream,
@ -548,3 +476,24 @@ int stream_deactivate(struct stream *stream, uint64_t now)
}
return 0;
}
int stream_activate_virtual(struct stream *stream, uint16_t index)
{
struct server *server = stream->server;
int fd;
if (stream->source == NULL) {
fd = setup_socket(stream);
if (fd < 0)
return fd;
stream->source = pw_loop_add_io(server->impl->loop, fd,
SPA_IO_IN, true, on_socket_data, stream);
if (stream->source == NULL) {
close(fd);
return -errno;
}
}
pw_stream_set_active(stream->stream, true);
return 0;
}

View file

@ -78,5 +78,6 @@ void stream_destroy(struct stream *stream);
int stream_activate(struct stream *stream, uint16_t index, uint64_t now);
int stream_deactivate(struct stream *stream, uint64_t now);
int stream_activate_virtual(struct stream *stream, uint16_t index);
#endif /* AVB_STREAM_H */

View file

@ -95,6 +95,50 @@ if build_pw_cat
summary({'Build pw-cat with FFmpeg integration': build_pw_cat_with_ffmpeg}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump tool')
endif
build_avb_virtual = get_option('avb').require(
host_machine.system() == 'linux',
error_message: 'AVB support is only available on Linux'
).allowed()
if build_avb_virtual
avb_tool_inc = include_directories('../modules')
avb_tool_sources = [
'pw-avb-virtual.c',
'../modules/module-avb/avb.c',
'../modules/module-avb/adp.c',
'../modules/module-avb/acmp.c',
'../modules/module-avb/aecp.c',
'../modules/module-avb/aecp-aem.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-available.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-control.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-name.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-clock-source.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-sampling-rate.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-deregister-unsolicited-notifications.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-register-unsolicited-notifications.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-stream-format.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-lock-entity.c',
'../modules/module-avb/aecp-aem-cmds-resps/cmd-get-set-configuration.c',
'../modules/module-avb/aecp-aem-cmds-resps/reply-unsol-helpers.c',
'../modules/module-avb/es-builder.c',
'../modules/module-avb/avdecc.c',
'../modules/module-avb/descriptors.c',
'../modules/module-avb/maap.c',
'../modules/module-avb/mmrp.c',
'../modules/module-avb/mrp.c',
'../modules/module-avb/msrp.c',
'../modules/module-avb/mvrp.c',
'../modules/module-avb/srp.c',
'../modules/module-avb/stream.c',
]
executable('pw-avb-virtual',
avb_tool_sources,
install: true,
include_directories: [configinc, avb_tool_inc],
dependencies: [mathlib, dl_lib, rt_lib, pipewire_dep],
)
endif
if dbus_dep.found()
executable('pw-reserve',
'reserve.h',

283
src/tools/pw-avb-virtual.c Normal file
View file

@ -0,0 +1,283 @@
/* PipeWire */
/* SPDX-FileCopyrightText: Copyright © 2026 PipeWire contributors */
/* SPDX-License-Identifier: MIT */
/**
* pw-avb-virtual: Create virtual AVB audio devices in the PipeWire graph.
*
* This tool creates virtual AVB talker/listener endpoints that appear
* as Audio/Source and Audio/Sink nodes in the PipeWire graph (visible
* in tools like Helvum). No AVB hardware or network access is needed
* the loopback transport is used for all protocol and stream operations.
*
* The sink node consumes audio silently (data goes nowhere).
* The source node produces silence (no network data to receive).
*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <getopt.h>
#include <locale.h>
#include <spa/utils/result.h>
#include <pipewire/pipewire.h>
#include "module-avb/internal.h"
#include "module-avb/stream.h"
#include "module-avb/avb-transport-loopback.h"
#include "module-avb/descriptors.h"
#include "module-avb/mrp.h"
#include "module-avb/adp.h"
#include "module-avb/acmp.h"
#include "module-avb/aecp.h"
#include "module-avb/maap.h"
#include "module-avb/mmrp.h"
#include "module-avb/msrp.h"
#include "module-avb/mvrp.h"
struct data {
struct pw_main_loop *loop;
struct pw_context *context;
struct pw_core *core;
struct spa_hook core_listener;
struct impl impl;
struct server *server;
const char *opt_remote;
const char *opt_name;
bool opt_milan;
};
static void do_quit(void *data, int signal_number)
{
struct data *d = data;
pw_main_loop_quit(d->loop);
}
static void on_core_error(void *data, uint32_t id, int seq,
int res, const char *message)
{
struct data *d = data;
pw_log_error("error id:%u seq:%d res:%d (%s): %s",
id, seq, res, spa_strerror(res), message);
if (id == PW_ID_CORE && res == -EPIPE)
pw_main_loop_quit(d->loop);
}
static const struct pw_core_events core_events = {
PW_VERSION_CORE_EVENTS,
.error = on_core_error,
};
static struct server *create_virtual_server(struct data *data)
{
struct impl *impl = &data->impl;
struct server *server;
struct stream *stream;
uint16_t idx;
char name_buf[256];
server = calloc(1, sizeof(*server));
if (server == NULL)
return NULL;
server->impl = impl;
server->ifname = strdup("virtual0");
server->avb_mode = data->opt_milan ? AVB_MODE_MILAN_V12 : 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_maap_reserve(server->maap, 1);
init_descriptors(server);
/* Update stream properties and activate */
idx = 0;
spa_list_for_each(stream, &server->streams, link) {
if (stream->direction == SPA_DIRECTION_INPUT) {
snprintf(name_buf, sizeof(name_buf), "%s.source.%u",
data->opt_name, idx);
pw_stream_update_properties(stream->stream,
&SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) {
{ PW_KEY_NODE_NAME, name_buf },
{ PW_KEY_NODE_DESCRIPTION, "AVB Virtual Source" },
{ PW_KEY_NODE_VIRTUAL, "true" },
})));
} else {
snprintf(name_buf, sizeof(name_buf), "%s.sink.%u",
data->opt_name, idx);
pw_stream_update_properties(stream->stream,
&SPA_DICT_INIT_ARRAY(((struct spa_dict_item[]) {
{ PW_KEY_NODE_NAME, name_buf },
{ PW_KEY_NODE_DESCRIPTION, "AVB Virtual Sink" },
{ PW_KEY_NODE_VIRTUAL, "true" },
})));
}
if (stream_activate_virtual(stream, idx) < 0)
pw_log_warn("failed to activate stream %u", idx);
idx++;
}
return server;
error:
spa_list_remove(&server->link);
free(server->ifname);
free(server);
return NULL;
}
static void show_help(const char *name)
{
printf("%s [options]\n"
" -h, --help Show this help\n"
" --version Show version\n"
" -r, --remote NAME Remote daemon name\n"
" -n, --name PREFIX Node name prefix (default: avb-virtual)\n"
" -m, --milan Use Milan v1.2 mode (default: legacy)\n",
name);
}
int main(int argc, char *argv[])
{
struct data data = { 0 };
struct pw_loop *l;
int res = -1;
static const struct option long_options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ "remote", required_argument, NULL, 'r' },
{ "name", required_argument, NULL, 'n' },
{ "milan", no_argument, NULL, 'm' },
{ NULL, 0, NULL, 0 }
};
int c;
setlocale(LC_ALL, "");
pw_init(&argc, &argv);
data.opt_name = "avb-virtual";
while ((c = getopt_long(argc, argv, "hVr:n:m", long_options, NULL)) != -1) {
switch (c) {
case 'h':
show_help(argv[0]);
return 0;
case 'V':
printf("%s\n"
"Compiled with libpipewire %s\n"
"Linked with libpipewire %s\n",
argv[0],
pw_get_headers_version(),
pw_get_library_version());
return 0;
case 'r':
data.opt_remote = optarg;
break;
case 'n':
data.opt_name = optarg;
break;
case 'm':
data.opt_milan = true;
break;
default:
show_help(argv[0]);
return -1;
}
}
data.loop = pw_main_loop_new(NULL);
if (data.loop == NULL) {
fprintf(stderr, "can't create main loop: %m\n");
goto exit;
}
l = pw_main_loop_get_loop(data.loop);
pw_loop_add_signal(l, SIGINT, do_quit, &data);
pw_loop_add_signal(l, SIGTERM, do_quit, &data);
data.context = pw_context_new(l, NULL, 0);
if (data.context == NULL) {
fprintf(stderr, "can't create context: %m\n");
goto exit;
}
data.core = pw_context_connect(data.context,
pw_properties_new(
PW_KEY_REMOTE_NAME, data.opt_remote,
NULL),
0);
if (data.core == NULL) {
fprintf(stderr, "can't connect to PipeWire: %m\n");
goto exit;
}
pw_core_add_listener(data.core, &data.core_listener,
&core_events, &data);
/* Initialize the AVB impl */
data.impl.loop = l;
data.impl.timer_queue = pw_context_get_timer_queue(data.context);
data.impl.context = data.context;
data.impl.core = data.core;
spa_list_init(&data.impl.servers);
/* Create the virtual AVB server with streams */
data.server = create_virtual_server(&data);
if (data.server == NULL) {
fprintf(stderr, "can't create virtual AVB server: %m\n");
goto exit;
}
fprintf(stdout, "Virtual AVB device running (%s mode). Press Ctrl-C to stop.\n",
data.opt_milan ? "Milan v1.2" : "legacy");
pw_main_loop_run(data.loop);
res = 0;
exit:
if (data.server)
avdecc_server_free(data.server);
if (data.core)
pw_core_disconnect(data.core);
if (data.context)
pw_context_destroy(data.context);
if (data.loop)
pw_main_loop_destroy(data.loop);
pw_deinit();
return res;
}

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,
timeout_multiplier : 3)
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