Project

General

Profile

Actions

Bug #8824

open
SB OD

unix-socket: unbounded blocking call to accept new client hangs Unix manager thread

Bug #8824: unix-socket: unbounded blocking call to accept new client hangs Unix manager thread

Added by Shivani Bhardwaj 9 days ago. Updated 2 days ago.

Status:
New
Priority:
Normal
Assignee:
Target version:
Affected Versions:
Effort:
Difficulty:
Label:

Description

Reported by Communications Security Establishment (CSE):

## Summary

When the Unix command socket accepts a new client, it immediately performs a blocking `recv()` to read the version handshake before the descriptor has been put into non-blocking mode or guarded by `select()`. A local client that simply `connect()`s to the socket and never sends a single byte causes this `recv()` to block forever, freezing the sole Unix manager thread. While the thread is wedged, no other clients are serviced, registered background tasks (including the pcap-driver task in unix-socket runmode) stop running, and the thread's shutdown check is never reached. The result is a local, low-privilege denial of service of Suricata's control plane.

## Affected Piece of Code

- **File:** `src/unix-manager.c`
- **Function / Location:** `UnixCommandAccept()` ~L344-367
- **Subsystem:** unix-runmodes — Unix socket command server, runmodes, main, privilege drop, landlock

```c
   342        /* accept client socket */
   343        socklen_t len = sizeof(this->client_addr);
   344        client = accept(this->socket, (struct sockaddr *) &this->client_addr,
   345                              &len);
   346        if (client < 0) {
   347            SCLogInfo("Unix socket: accept() error: %s",
   348                      strerror(errno));
   349            return 0;
   350        }
   351        SCLogDebug("Unix socket: client connection");
   352
   353        /* read client version */
   354        buffer[sizeof(buffer)-1] = 0;
   355        ret = recv(client, buffer, sizeof(buffer)-1, 0);
   356        if (ret < 0) {
   357            SCLogInfo("Command server: client doesn't send version");
   358            close(client);
   359            return 0;
   360        }
   361        if (ret >= (int)(sizeof(buffer)-1)) {
   362            SCLogInfo("Command server: client message is too long, " 
   363                      "disconnect him.");
   364            close(client);
   365            return 0;
   366        }
   367        buffer[ret] = 0;
```

## The Bug

`UnixCommandAccept()` is invoked whenever `select()` reports the listening socket as readable. After `accept()` returns a new client descriptor at line 344, the function immediately calls `recv(client, buffer, sizeof(buffer)-1, 0)` at line 355 to read the JSON version handshake. The accepted descriptor inherits the listening socket's blocking semantics: `UnixNew()` (`src/unix-manager.c:167`) creates the listening socket as a plain `AF_UNIX` / `SOCK_STREAM` socket and never sets `O_NONBLOCK`, nor does any code path apply `SO_RCVTIMEO`, `fcntl()`, or a `select()` readiness check to the freshly accepted fd before this first read. Consequently, if the peer has completed `connect()` but has not written any bytes, `recv()` at line 355 blocks indefinitely.

The call chain that places this blocking read on the critical path is as follows:

- `SuricataMain()` (`src/suricata.c:3183`) calls `UnixManagerThreadSpawnNonRunmode()` when `unix-command.enabled: yes`; alternatively, in unix-socket runmode, `RunModeUnixSocketSingle()` (`src/runmode-unix-socket.c:1764-1790`) calls `UnixManagerInit()` followed by `UnixManagerThreadSpawn(1)`.
- `UnixManagerThreadSpawn()` (`src/unix-manager.c:1187`) creates exactly one management thread whose callback is `UnixManager()` (`src/unix-manager.c:1150`, registered at `:1295`).
- `UnixManager()` loops calling `UnixMain()` (`src/unix-manager.c:626`). `UnixMain()` `select()`s on the listening socket and the set of established clients; when the *listening* fd is readable (line 672) it calls `UnixCommandAccept()` (`src/unix-manager.c:330`).
- `UnixCommandAccept()` performs `accept()` at line 344 and then immediately `recv(client, buffer, 200, 0)` at line 355 on the new fd, with no preceding `select()`/timeout.

Because there is only a single Unix manager thread, blocking inside `recv()` halts the entire control loop. Specifically, while the thread is stuck:

- `UnixCommandBackgroundTasks()` at `src/unix-manager.c:1178` is never invoked again. In `RUNMODE_UNIX_SOCKET` this function drives `UnixSocketPcapFilesCheck`, so queued pcap processing stops entirely.
- No further iterations of `UnixMain()` occur, so other connected clients (and new connections) receive no service.
- The `THV_KILL` shutdown check at `src/unix-manager.c:1167` is never reached, delaying clean shutdown of the process.

Note that this differs from the handling of *established* clients: once a client has been added to the list, `UnixMain()` only calls `UnixCommandRun()` after `select()` has confirmed the client fd is readable. The version-handshake read in `UnixCommandAccept()` is the one place where a `recv()` is issued without that readiness guarantee.

The socket is created at the default path `${localstatedir}/run/suricata/suricata-command.socket` (commonly `/var/run/suricata/suricata-command.socket`), `chmod`'d `0660` inside a `0750` directory, so any local user that is the suricata user or a member of its group can trigger the hang.

**Vulnerability class:** infinite-loop / unbounded blocking syscall (denial of service).

## Reproduction Results

The trigger below is **analytically derived** from source inspection of the call chain and socket-creation flags described above; it was not executed against a live build in this audit environment. The behaviour follows directly from POSIX `recv()` semantics on a blocking `SOCK_STREAM` socket with no data pending, and no code path was found that could alter the blocking mode of the accepted fd before line 355.

1. Configure Suricata with the unix command socket enabled. Either run in unix-socket runmode (`suricata --unix-socket`) or set in `suricata.yaml`:
   ```yaml
   unix-command:
     enabled: yes
   ```
   The default socket path is `${localstatedir}/run/suricata/suricata-command.socket`.
2. Start Suricata and wait for the `unix socket '<path>'` log line confirming the listener is up.
3. As any local user with write permission on the socket (owner or group of the `0660` socket), connect and send nothing. Either:
   - `nc -U /var/run/suricata/suricata-command.socket` and do not type anything; or
   - `python3 -c 'import socket,time; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.connect("/var/run/suricata/suricata-command.socket"); time.sleep(10**9)'`

   No bytes are written — the expected version-handshake JSON (`{"version":"0.2"}`) is deliberately withheld.
4. Observe that the UnixManager thread is stuck inside `recv()` at `unix-manager.c:355` (verifiable with `gdb -p <pid>` / `pstack`). A second client such as `suricatasc` receives no response to any command. In unix-socket runmode, queued pcaps stop being processed because `UnixCommandBackgroundTasks()` is never called again.
5. The hang persists until the silent client closes its fd or the Suricata process is killed.

## Severity

**LOW**

This is a local denial of service of the Suricata control plane. The single Unix manager thread blocks indefinitely, with the following concrete effects:

- (a) No further unix-socket commands (`reload-rules`, `dump-counters`, `shutdown`, `dataset-add`, etc.) are serviced for any client.
- (b) Registered background tasks stop. In `RUNMODE_UNIX_SOCKET` this halts `UnixSocketPcapFilesCheck` and therefore all pcap processing.
- (c) The `THV_KILL` shutdown check in `UnixManager()` is never reached, delaying clean shutdown.

There is no memory corruption, no crash, and no remote reachability: the `AF_UNIX` socket is `chmod`'d `0660` under a `0750` directory, so only the suricata user/group can trigger the condition — and such a principal can already legitimately issue the `shutdown` command. The rating is therefore kept at LOW despite the complete stall of the management thread.

## Suggested Fix

Apply a receive timeout (or non-blocking mode) to the accepted fd before the handshake `recv()` in `UnixCommandAccept()`, so a silent client is dropped instead of hanging the manager thread. Minimal patch in `src/unix-manager.c`, immediately after the successful `accept()` at line 350:

```diff
@@ -350,6 +350,14 @@ static int UnixCommandAccept(UnixCommand *this)
     }
     SCLogDebug("Unix socket: client connection");

+    /* bound the version-handshake read so a silent client cannot stall the
+     * manager thread */
+    struct timeval rcv_to = { .tv_sec = 5, .tv_usec = 0 };
+    if (setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &rcv_to, sizeof(rcv_to)) != 0) {
+        SCLogWarning("Unix socket: unable to set SO_RCVTIMEO: %s", strerror(errno));
+        /* fall through: better to risk a hang than to refuse the client */
+    }
+
     /* read client version */
     buffer[sizeof(buffer)-1] = 0;
     ret = recv(client, buffer, sizeof(buffer)-1, 0);
```

With `SO_RCVTIMEO` set, a client that sends nothing causes `recv()` to return `-1` with `EAGAIN`/`EWOULDBLOCK` after 5 seconds, hitting the existing `ret < 0` branch which logs and closes the fd.

A more thorough fix would defer the version handshake entirely: after `accept()`, add the fd to the client list in a "pending-version" state and let `UnixMain()`'s existing `select()` loop signal readability before any `recv()` is attempted, mirroring how `UnixCommandRun()` already handles established clients.

VJ Updated by Victor Julien 9 days ago Actions #1

  • Subject changed from Blocking recv() on freshly accepted client lets one connection hang the Unix manager thread to unix-socket: Blocking recv() on freshly accepted client lets one connection hang the Unix manager thread

Think this could be a regular bug.

SB Updated by Shivani Bhardwaj 5 days ago Actions #2

  • Subject changed from unix-socket: Blocking recv() on freshly accepted client lets one connection hang the Unix manager thread to unix-socket: unbounded blocking call to accept new client hangs Unix manager thread

VJ Updated by Victor Julien 2 days ago Actions #3

  • Tracker changed from Security to Bug
  • Private changed from Yes to No

We consider this a bug as the unix socket is an admin interface. Access to it means many other things can be done, including simply stopping Suricata.

Actions

Also available in: PDF Atom