So I managed to reproduce the original crash (--dev-tap --ifconfig-noexec,
and then kill the socket), and can confirm that the patch fixes that.
I have also run this through the full client/server test bed, to verify
that it's not breaking anything else in the process. Looks good
(the patch looks bigger than it actually is, because "sb->next" is
now "next" and that changes all lines in stream_buf_get_next() without
a "real" change).
I have simplified the instructions in the commit message - it's not
necessary to have a special server config, just "any TAP+TCP server"
will do, with "--ifconfig-noexec" on the client side.
Your patch has been applied to the master & release/2.7 branch.
Backporting to 2.6 has not been done as the bug is considered too exotic
to be a "real problem" in general (it's day 1 bug, so we *could* backport
it all the way back to 2.2.x ;-) ).
commit 5e85c3491fcf75f1a006d410d1a2a7720c2d3f09 (master)
commit 4d6cf3dcb821e281fdede8a989b50b9c1a2255c2 (release/2.7)
Author: Arne Schwabe
Date: Mon Feb 16 17:22:31 2026 +0100
Merge stream_buf_get_next and stream_buf_set_next
Signed-off-by: Arne Schwabe <arne-openvpn@rfc2549.org>
Acked-by: Frank Lichtenheld <frank@lichtenheld.com>
Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1477
Message-Id: <20260216162236.22304-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg35673.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
--
kind regards,
Gert Doering
@@ -44,9 +44,7 @@
bool
sockets_read_residual(const struct context *c)
{
- int i;
-
- for (i = 0; i < c->c1.link_sockets_num; i++)
+ for (int i = 0; i < c->c1.link_sockets_num; i++)
{
if (c->c2.link_sockets[i]->stream_buf.residual_fully_formed)
{
@@ -2053,13 +2051,19 @@
* stream connection.
*/
+/**
+ * resets the stream buffer to be set up for the next round of
+ * reassembling a packet
+ *
+ * But still leaves the current packet in \c sb->buf to be potentially
+ * read.
+ */
static inline void
stream_buf_reset(struct stream_buf *sb)
{
dmsg(D_STREAM_DEBUG, "STREAM: RESET");
sb->residual_fully_formed = false;
sb->buf = sb->buf_init;
- buf_reset(&sb->next);
sb->len = -1;
}
@@ -2081,19 +2085,35 @@
dmsg(D_STREAM_DEBUG, "STREAM: INIT maxlen=%d", sb->maxlen);
}
-static inline void
-stream_buf_set_next(struct stream_buf *sb)
+/**
+ * Return a buffer that is backed by the same backend as sb->buf that
+ * determines where the next read should be done by also having the
+ * right offset into \c sb->buf.
+ * @param sb the stream buffer from which to construct the next buffer
+ */
+static inline struct buffer
+stream_buf_get_next(struct stream_buf *sb)
{
/* set up 'next' for next i/o read */
- sb->next = sb->buf;
- sb->next.offset = sb->buf.offset + sb->buf.len;
- sb->next.len = (sb->len >= 0 ? sb->len : sb->maxlen) - sb->buf.len;
- dmsg(D_STREAM_DEBUG, "STREAM: SET NEXT, buf=[%d,%d] next=[%d,%d] len=%d maxlen=%d",
- sb->buf.offset, sb->buf.len, sb->next.offset, sb->next.len, sb->len, sb->maxlen);
- ASSERT(sb->next.len > 0);
- ASSERT(buf_safe(&sb->buf, sb->next.len));
+ struct buffer next;
+ next = sb->buf;
+ next.offset = sb->buf.offset + sb->buf.len;
+ next.len = (sb->len >= 0 ? sb->len : sb->maxlen) - sb->buf.len;
+ dmsg(D_STREAM_DEBUG, "STREAM: GET NEXT, buf=[%d,%d] next=[%d,%d] len=%d maxlen=%d",
+ sb->buf.offset, sb->buf.len, next.offset, next.len, sb->len, sb->maxlen);
+ ASSERT(next.len > 0);
+ ASSERT(buf_safe(&sb->buf, next.len));
+ return next;
}
+/**
+ * Sets the parameter buf to the current buffer of \c sb->buf.
+ * This function assumes that caller already checked if the packet in \c sb->buf
+ * is fully assembled.
+ *
+ * @param sb stream buffer to operate on
+ * @param buf buffer to point to the contents of buf
+ */
static inline void
stream_buf_get_final(struct stream_buf *sb, struct buffer *buf)
{
@@ -2102,14 +2122,6 @@
*buf = sb->buf;
}
-static inline void
-stream_buf_get_next(struct stream_buf *sb, struct buffer *buf)
-{
- dmsg(D_STREAM_DEBUG, "STREAM: GET NEXT len=%d", buf_defined(&sb->next) ? sb->next.len : -1);
- ASSERT(buf_defined(&sb->next));
- *buf = sb->next;
-}
-
bool
stream_buf_read_setup_dowork(struct stream_buf *sb)
{
@@ -2122,13 +2134,30 @@
sb->residual_fully_formed ? "YES" : "NO", sb->residual.len);
}
- if (!sb->residual_fully_formed)
- {
- stream_buf_set_next(sb);
- }
return !sb->residual_fully_formed;
}
+/**
+ * This will determine if \c sb->buf contains a full packet. It will also
+ * move anything in \c sb->buf beyond a full packet to \c sb->residual.
+ *
+ * The first time the function is called with a valid buffer and port sharing
+ * is enabled, the function will also determine if the buffer contains
+ * OpenVPN protocol data and store the result in \c sb->port_share_state.
+ *
+ * If a packet outside the allowed range is detected, the error state
+ * on \c sb is set.
+ *
+ * Since the buffer in \c sb->buf is modified from the outside (via
+ * \c stream_buf_get_next) the parameter \p length_added needs to be set
+ * to the amount of bytes that have been written to this buffer. If the
+ * buffer was not modified but should still be analysed and potentially
+ * split to \c sb->residual, the parameter \p length_added should be 0.
+ *
+ * @param sb the stream buffer
+ * @param length_added The length that has been added to \c sb->buf
+ * @return true if \c sb->buf contains fully reassembled packet
+ */
static bool
stream_buf_added(struct stream_buf *sb, int length_added)
{
@@ -2191,7 +2220,6 @@
else
{
dmsg(D_STREAM_DEBUG, "STREAM: ADD returned FALSE (have=%d need=%d)", sb->buf.len, sb->len);
- stream_buf_set_next(sb);
return false;
}
}
@@ -2261,8 +2289,7 @@
sockethandle_t sh = { .s = sock->sd };
len = sockethandle_finalize(sh, &sock->reads, buf, NULL);
#else
- struct buffer frag;
- stream_buf_get_next(&sock->stream_buf, &frag);
+ struct buffer frag = stream_buf_get_next(&sock->stream_buf);
len = recv(sock->sd, BPTR(&frag), BLEN(&frag), MSG_NOSIGNAL);
#endif
@@ -2538,7 +2565,7 @@
}
else if (proto_is_tcp(sock->info.proto))
{
- stream_buf_get_next(&sock->stream_buf, &sock->reads.buf);
+ sock->reads.buf = stream_buf_get_next(&sock->stream_buf);
}
else
{
@@ -95,23 +95,41 @@
int mtu_changed; /* Set to true when mtu value is changed */
};
-/*
- * Used to extract packets encapsulated in streams into a buffer,
- * in this case IP packets embedded in a TCP stream.
+/**
+ * struct used to extract packets encapsulated in streams into a buffer,
+ * in this case OpenVPN packets (data or control) embedded in a TCP stream.
+ *
+ * This struct is used to packetise the TCP stream into the
+ * OpenVPN packet. Each OpenVPN packet has a two-byte header determining
+ * the length of the packet.
*/
struct stream_buf
{
+ /* Buffer to hold the initial buffer that will be used to reset buf */
struct buffer buf_init;
+
+ /** buffer holding the excess bytes that are not part of the
+ * packet. */
struct buffer residual;
+
+ /** Maximum length of a packet that we accept */
int maxlen;
+
+ /** The buffer in buf contains a full packet without a header. Any
+ * extra data is in residual */
bool residual_fully_formed;
+ /** Holds the data of the current packet. This might be a partial packet */
struct buffer buf;
- struct buffer next;
- int len; /* -1 if not yet known */
- bool error; /* if true, fatal TCP error has occurred,
- * requiring that connection be restarted */
+ /** -1 if not yet known. Otherwise holds the length of the
+ * packet. If >= 0, buf is already moved past the initial
+ * size header */
+ int len;
+
+ /** if true, a fatal TCP error has occurred,
+ * requiring that connection be restarted */
+ bool error;
#if PORT_SHARE
#define PS_DISABLED 0
#define PS_ENABLED 1