Skip to content

rtpengine: avoid wrongful node disabling (local errors, stale replies, timeouts) + fd and bencode fixes - #4258

Closed
zbyslb wants to merge 5 commits into
OpenSIPS:masterfrom
zbyslb:rtpengine-local-resource-errors
Closed

zbyslb wants to merge 5 commits into
OpenSIPS:masterfrom
zbyslb:rtpengine-local-resource-errors

Conversation

@zbyslb

@zbyslb zbyslb commented Sep 14, 2026

Copy link
Copy Markdown

Summary

Five fixes for the rtpengine module (plus one in its bundled bencode helper), all found and verified while hardening a production OpenSIPS 3.6 platform. The overarching theme: the node-disabling logic is too eager — it takes the proxy down for conditions that are not the proxy's fault, and in the worst case a single local resource shortage flags every node as down at once.

Each commit is self-contained and can be reviewed/cherry-picked independently.

bencode: fully initialize the buffer before the first allocation

If __bencode_piece_new() fails (pkg memory exhausted), bencode_buffer_init() returns -1 having only assigned buf->piecesfree_list and error keep their previous (stack garbage) values. Callers cannot distinguish the failure stage and routinely run bencode_buffer_free(), whose first statement dereferences buf->free_list: undefined behaviour (call through a garbage pointer). Zeroing the struct up front makes the failure path a safe no-op free; the success path assigns all fields explicitly, so behaviour is unchanged.

rtpengine: close the actual fd, not the node index, on reply read error

RTPE_IO_ERROR_CLOSE(param->node->idx) expands (on its EPIPE/EBADF branch) to close(idx) — closing whichever descriptor happens to carry the node's array index number (for the first nodes in a set: one of the stdio fds) — and then writes -1 into node->idx, making every later rtpe_socks[node->idx] an out-of-bounds array access. The fd that actually failed is the per-command socket created by start_async_send_rtpe_command().

rtpengine: do not disable nodes on local resource errors

An async command builds a fresh UDP socket per command; connect() on it makes the kernel autobind a local source port from ip_local_port_range (shared between TCP and UDP). Once that pool runs out, connect() fails with EAGAIN, and socket() may fail with EMFILE/ENFILE/ENOMEM. These are local machine conditions, yet the code jumped to badproxy and disabled the node. With the pool exhausted, every command towards every node fails at once — a single local resource shortage flags all nodes as down, while the long-lived rtpe_socks[] probes (already bound, no new port needed) keep succeeding, so the nodes flip between enabled and disabled until the process is restarted. We hit exactly this in production (log signature: can't connect to RTP proxy udp:... (11:Resource temporarily unavailable) immediately followed by disable it for every node). Local errnos (EAGAIN, EADDRNOTAVAIL, ENOBUFS, ENOMEM, EMFILE, ENFILE) now only release the fd; errors that do reflect node/network reachability (ECONNREFUSED, ENETUNREACH, EPIPE, ...) still disable the node as before. The check is only performed immediately after the failing syscall.

rtpengine: do not disable a node that is proven alive by its data

In the sync command path, a late reply of a previous command (still queued in the socket buffer) fails the cookie comparison and counts as a full retry failure — although the proxy just sent us a datagram. The fix tracks whether any data was received during the retries; a node that produced traffic keeps its enabled state, a completely silent node is still disabled as before.

rtpengine: do not disable a node on an async reply timeout

An async timeout means one command did not get its reply in time — a single lost/late datagram is enough. That is not evidence the node is down, and a burst of timeouts (brief network issues) would disable all nodes at once. Node liveness is better left to the existing probes: the periodic rtpe_test() ping and the sync path, which only disables a node that stays completely silent while retrying.

Testing

Built and running on a production OpenSIPS 3.6.9 deployment (Debian 12, ~10 rtpengine nodes behind it); the local-resource-error path was exercised by the production incident described above — after the fix, the same port-exhaustion condition only fails individual commands while all nodes stay enabled.

If __bencode_piece_new() fails (pkg memory exhausted), the function
returns -1 after only having assigned buf->pieces, leaving free_list
and error with their previous (stack garbage) values. Callers, which
cannot distinguish the failure stage, routinely run
bencode_buffer_free() on the returned error - and the free routine
starts by dereferencing buf->free_list, producing undefined behaviour
(reading a garbage pointer and calling through it).

Zero the whole struct up front: on the failure path the buffer is now
an empty, safely freeable object (both loops in
bencode_buffer_free() simply do not run), while on the success path
all three fields are explicitly assigned, so behaviour is unchanged.
RTPE_IO_ERROR_CLOSE() expands to close(_fd) followed by (_fd) = -1,
but the resume handler passed param->node->idx - the node's array
index - instead of the failed fd. Two consequences on the
EPIPE/EBADF branch of the macro:

  * close(idx) closes whichever descriptor happens to have that
    number; for the first nodes in a set this is one of the stdio
    fds (idx 0, 1 or 2), silently breaking logging/stdin;
  * node->idx is overwritten with -1, so every later
    rtpe_socks[node->idx] access indexes the array out of bounds.

Pass the fd that actually failed; it is a per-command socket created
by start_async_send_rtpe_command(), not the shared rtpe_socks slot,
so no array slot needs to be invalidated here.
An async rtpengine command builds a fresh UDP socket per command.
connect() on it makes the kernel autobind a local source port from
ip_local_port_range first, so once that pool (shared between TCP and
UDP) runs out, connect() fails with EAGAIN, and socket() may fail
with EMFILE/ENFILE/ENOMEM.

All of these are local machine conditions and say nothing about the
health of the rtpengine node, yet the code treated them like any
other send failure: it jumped to the badproxy label and disabled the
node for rtpengine_disable_tout. With the pool exhausted, every
command towards every node fails at once, so a single local resource
shortage ends up flagging ALL nodes as down - while the long-lived
rtpe_socks[] probes (already bound, no new port needed) keep
succeeding, making the nodes flip between enabled and disabled until
the process is restarted.

Route the local resource errnos (EAGAIN, EADDRNOTAVAIL, ENOBUFS,
ENOMEM, EMFILE, ENFILE) to the existing error label instead, which
only releases the fd without touching rn_disabled. Errors that do
reflect node/network reachability (ECONNREFUSED, ENETUNREACH,
EPIPE ...) still disable the node as before.

The check is only performed immediately after the failing syscall,
where errno is guaranteed to belong to it.
The sync command path disables a proxy after rtpengine_retr failed
attempts. An attempt only counts as failed when no matching reply
arrives, but a late reply of a *previous* command (still queued in
the socket buffer, e.g. after a timeout-triggered resend) also fails
the cookie comparison - and such an attempt currently counts as a
full failure, although the proxy just sent us a datagram.

Track whether any data at all was received during the retries; if
the node produced traffic, skip the disabling at the badproxy label:
the proxy is demonstrably up, only its replies were stale. A node
that stays completely silent still gets disabled as before.
An async command timing out says that one command did not get its
reply in time - a single lost/late datagram is enough. That is no
evidence that the node is down: it may just be slow, busy, or the
reply may have been dropped on a congested link. Disabling the node
for rtpengine_disable_tout punishes a healthy node for one missing
datagram, and when a burst of timeouts hits (e.g. brief network
issues), all nodes get disabled at once, dropping the whole platform
to 'no available proxies'.

Node liveness is better left to the existing probes: the periodic
rtpe_test() ping (when ping/timer logic is enabled) and the sync
command path, which only disables a node that stays completely
silent while retrying.
@zbyslb

zbyslb commented Sep 15, 2026

Copy link
Copy Markdown
Author

Closing in favor of a follow-up PR containing only the clear-cut bug fixes (crash/heap-corruption class), split out from the behavioral changes discussed here.

@zbyslb zbyslb closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants