Files
freebsd/sys/netlink/netlink_message_writer.c
R. Christian McDonald 5180fce1cf netlink: Make room for the unfinished message when growing the buffer
When a message outgrows the writer's buffer, _nlmsg_refill_buffer()
allocates a larger one and moves the unfinished message there.  The new
buffer was sized for the data about to be added alone, so with enough of
the message already written the caller wrote past its end and left it
with datalen > buflen.  A 20-byte message plus an attribute of 8176
bytes got a buffer of 8192 bytes for 8200 bytes of data, and reading it
panicked an INVARIANTS kernel in nl_soreceive().

Size the new buffer for both, and add a ktest for it.

Reviewed by:		glebius
Fixes:			7e5bf68495 ("netlink: add netlink support")
MFC after:		1 week
Sponsored by:		Rubicon Communications, LLC ("Netgate")
Differential Revision:	https://reviews.freebsd.org/D60541
2026-10-11 19:58:48 -04:00

401 lines
10 KiB
C

/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2022 Alexander V. Chernikov <[email protected]>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/param.h>
#include <sys/malloc.h>
#include <sys/lock.h>
#include <sys/rmlock.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/socketvar.h>
#include <sys/syslog.h>
#include <netlink/netlink.h>
#include <netlink/netlink_ctl.h>
#include <netlink/netlink_linux.h>
#include <netlink/netlink_var.h>
#define DEBUG_MOD_NAME nl_writer
#define DEBUG_MAX_LEVEL LOG_DEBUG3
#include <netlink/netlink_debug.h>
_DECLARE_DEBUG(LOG_INFO);
static bool
nlmsg_get_buf(struct nl_writer *nw, size_t len, bool waitok)
{
const int mflag = waitok ? M_WAITOK : M_NOWAIT;
MPASS(nw->buf == NULL);
NL_LOG(LOG_DEBUG3, "Setting up nw %p len %zu %s", nw, len,
waitok ? "wait" : "nowait");
nw->buf = nl_buf_alloc(len, mflag);
if (__predict_false(nw->buf == NULL))
return (false);
nw->hdr = NULL;
nw->malloc_flag = mflag;
nw->num_messages = 0;
nw->enomem = false;
return (true);
}
static bool
nl_send_one(struct nl_writer *nw)
{
return (nl_send(nw, nw->nlp));
}
bool
_nl_writer_unicast(struct nl_writer *nw, size_t size, struct nlpcb *nlp,
bool waitok)
{
*nw = (struct nl_writer){
.nlp = nlp,
.cb = nl_send_one,
};
return (nlmsg_get_buf(nw, size, waitok));
}
bool
_nl_writer_group(struct nl_writer *nw, size_t size, uint16_t protocol,
uint16_t group_id, int priv, bool waitok)
{
*nw = (struct nl_writer){
.group.proto = protocol,
.group.id = group_id,
.group.priv = priv,
.cb = nl_send_group,
};
return (nlmsg_get_buf(nw, size, waitok));
}
void
_nlmsg_ignore_limit(struct nl_writer *nw)
{
nw->ignore_limit = true;
}
bool
_nlmsg_flush(struct nl_writer *nw)
{
bool result;
if (__predict_false(nw->hdr != NULL)) {
/* Last message has not been completed, skip it. */
int completed_len = (char *)nw->hdr - nw->buf->data;
/* Send completed messages */
nw->buf->datalen -= nw->buf->datalen - completed_len;
nw->hdr = NULL;
}
if (nw->buf->datalen == 0) {
MPASS(nw->num_messages == 0);
nl_buf_free(nw->buf);
nw->buf = NULL;
return (true);
}
result = nw->cb(nw);
nw->num_messages = 0;
if (!result) {
NL_LOG(LOG_DEBUG, "nw %p flush with %p() failed", nw, nw->cb);
}
return (result);
}
/*
* Flushes previous data and allocates new underlying storage
* sufficient for holding at least @required_len bytes.
* Return true on success.
*/
bool
_nlmsg_refill_buffer(struct nl_writer *nw, size_t required_len)
{
struct nl_buf *new;
size_t completed_len, new_len, last_len;
MPASS(nw->buf != NULL);
if (nw->enomem)
return (false);
NL_LOG(LOG_DEBUG3, "no space at offset %u/%u (want %zu), trying to "
"reclaim", nw->buf->datalen, nw->buf->buflen, required_len);
/* Calculate new buffer size and allocate it. */
completed_len = (nw->hdr != NULL) ?
(char *)nw->hdr - nw->buf->data : nw->buf->datalen;
last_len = nw->buf->datalen - completed_len;
if (completed_len > 0 && required_len < NLMBUFSIZE) {
/* We already ran out of space, use largest effective size. */
new_len = max(nw->buf->buflen, NLMBUFSIZE);
} else {
if (nw->buf->buflen < NLMBUFSIZE)
/* XXXGL: does this happen? */
new_len = NLMBUFSIZE;
else
new_len = nw->buf->buflen * 2;
}
/* The unfinished message is copied over and needs room as well. */
while (new_len < last_len + required_len)
new_len *= 2;
new = nl_buf_alloc(new_len, nw->malloc_flag | M_ZERO);
if (__predict_false(new == NULL)) {
nw->enomem = true;
NL_LOG(LOG_DEBUG, "getting new buf failed, setting ENOMEM");
return (false);
}
/* Copy last (unfinished) header to the new storage. */
if (last_len > 0) {
memcpy(new->data, nw->hdr, last_len);
new->datalen = last_len;
}
NL_LOG(LOG_DEBUG2, "completed: %zu bytes, copied: %zu bytes",
completed_len, last_len);
if (completed_len > 0) {
nlmsg_flush(nw);
MPASS(nw->buf == NULL);
} else
nl_buf_free(nw->buf);
nw->buf = new;
nw->hdr = (last_len > 0) ? (struct nlmsghdr *)new->data : NULL;
NL_LOG(LOG_DEBUG2, "switched buffer: used %u/%u bytes",
new->datalen, new->buflen);
return (true);
}
bool
_nlmsg_add(struct nl_writer *nw, uint32_t portid, uint32_t seq, uint16_t type,
uint16_t flags, uint32_t len)
{
struct nl_buf *nb = nw->buf;
struct nlmsghdr *hdr;
size_t required_len;
MPASS(nw->hdr == NULL);
required_len = NETLINK_ALIGN(len + sizeof(struct nlmsghdr));
if (__predict_false(nb->datalen + required_len > nb->buflen)) {
if (!nlmsg_refill_buffer(nw, required_len))
return (false);
nb = nw->buf;
}
hdr = (struct nlmsghdr *)(&nb->data[nb->datalen]);
hdr->nlmsg_len = len;
hdr->nlmsg_type = type;
hdr->nlmsg_flags = flags;
hdr->nlmsg_seq = seq;
hdr->nlmsg_pid = portid;
nw->hdr = hdr;
nb->datalen += sizeof(struct nlmsghdr);
return (true);
}
bool
_nlmsg_end(struct nl_writer *nw)
{
struct nl_buf *nb = nw->buf;
MPASS(nw->hdr != NULL);
if (nw->enomem) {
NL_LOG(LOG_DEBUG, "ENOMEM when dumping message");
nlmsg_abort(nw);
return (false);
}
nw->hdr->nlmsg_len = nb->data + nb->datalen - (char *)nw->hdr;
NL_LOG(LOG_DEBUG2, "wrote msg len: %u type: %d: flags: 0x%X seq: %u pid: %u",
nw->hdr->nlmsg_len, nw->hdr->nlmsg_type, nw->hdr->nlmsg_flags,
nw->hdr->nlmsg_seq, nw->hdr->nlmsg_pid);
nw->hdr = NULL;
nw->num_messages++;
return (true);
}
void
_nlmsg_abort(struct nl_writer *nw)
{
struct nl_buf *nb = nw->buf;
if (nw->hdr != NULL) {
nb->datalen = (char *)nw->hdr - nb->data;
nw->hdr = NULL;
}
}
void
nlmsg_ack(struct nlpcb *nlp, int error, struct nlmsghdr *hdr,
struct nl_pstate *npt)
{
struct nlmsgerr *errmsg;
int payload_len;
uint32_t flags = nlp->nl_flags;
struct nl_writer *nw = npt->nw;
bool cap_ack;
payload_len = sizeof(struct nlmsgerr);
/*
* The only case when we send the full message in the
* reply is when there is an error and NETLINK_CAP_ACK
* is not set.
*/
cap_ack = (error == 0) || (flags & NLF_CAP_ACK);
if (!cap_ack)
payload_len += hdr->nlmsg_len - sizeof(struct nlmsghdr);
payload_len = NETLINK_ALIGN(payload_len);
uint16_t nl_flags = cap_ack ? NLM_F_CAPPED : 0;
if ((npt->err_msg || npt->err_off) && nlp->nl_flags & NLF_EXT_ACK)
nl_flags |= NLM_F_ACK_TLVS;
NL_LOG(LOG_DEBUG3, "acknowledging message type %d seq %d",
hdr->nlmsg_type, hdr->nlmsg_seq);
if (!nlmsg_add(nw, nlp->nl_port, hdr->nlmsg_seq, NLMSG_ERROR, nl_flags, payload_len))
goto enomem;
errmsg = nlmsg_reserve_data(nw, payload_len, struct nlmsgerr);
errmsg->error = error;
/* In case of error copy the whole message, else just the header */
memcpy(&errmsg->msg, hdr, cap_ack ? sizeof(*hdr) : hdr->nlmsg_len);
if (npt->err_msg != NULL && nlp->nl_flags & NLF_EXT_ACK)
nlattr_add_string(nw, NLMSGERR_ATTR_MSG, npt->err_msg);
if (npt->err_off != 0 && nlp->nl_flags & NLF_EXT_ACK)
nlattr_add_u32(nw, NLMSGERR_ATTR_OFFS, npt->err_off);
if (npt->cookie != NULL)
nlattr_add_raw(nw, npt->cookie);
if (nlmsg_end(nw))
return;
enomem:
NLP_LOG(LOG_DEBUG, nlp, "error allocating ack data for message %d seq %u",
hdr->nlmsg_type, hdr->nlmsg_seq);
nlmsg_abort(nw);
}
bool
_nlmsg_end_dump(struct nl_writer *nw, int error, struct nlmsghdr *hdr)
{
if (!nlmsg_add(nw, hdr->nlmsg_pid, hdr->nlmsg_seq, NLMSG_DONE, 0, sizeof(int))) {
NL_LOG(LOG_DEBUG, "Error finalizing table dump");
return (false);
}
/* Save operation result */
int *perror = nlmsg_reserve_object(nw, int);
NL_LOG(LOG_DEBUG2, "record error=%d at off %d (%p)", error,
nw->buf->datalen, perror);
*perror = error;
nlmsg_end(nw);
nw->suppress_ack = true;
return (true);
}
/*
* KPI functions.
*/
u_int
nlattr_save_offset(const struct nl_writer *nw)
{
return (nw->buf->datalen - ((char *)nw->hdr - nw->buf->data));
}
void *
nlmsg_reserve_data_raw(struct nl_writer *nw, size_t sz)
{
struct nl_buf *nb = nw->buf;
void *data;
sz = NETLINK_ALIGN(sz);
if (__predict_false(nb->datalen + sz > nb->buflen)) {
if (!nlmsg_refill_buffer(nw, sz))
return (NULL);
nb = nw->buf;
}
data = &nb->data[nb->datalen];
bzero(data, sz);
nb->datalen += sz;
return (data);
}
bool
nlattr_add(struct nl_writer *nw, uint16_t attr_type, uint16_t attr_len,
const void *data)
{
struct nl_buf *nb = nw->buf;
struct nlattr *nla;
size_t required_len;
KASSERT(attr_len <= UINT16_MAX - sizeof(struct nlattr),
("%s: invalid attribute length %u", __func__, attr_len));
required_len = NLA_ALIGN(attr_len + sizeof(struct nlattr));
if (__predict_false(nb->datalen + required_len > nb->buflen)) {
if (!nlmsg_refill_buffer(nw, required_len))
return (false);
nb = nw->buf;
}
nla = (struct nlattr *)(&nb->data[nb->datalen]);
nla->nla_len = attr_len + sizeof(struct nlattr);
nla->nla_type = attr_type;
if (attr_len > 0) {
if ((attr_len % 4) != 0) {
/* clear padding bytes */
bzero((char *)nla + required_len - 4, 4);
}
memcpy((nla + 1), data, attr_len);
}
nb->datalen += required_len;
return (true);
}
#include <netlink/ktest_netlink_message_writer.h>