ruvnet/RuView · error · Error

${path}: ${res.status} ${res.statusText}

Error message

${path}: ${res.status} ${res.statusText}

What it means

nl_recvmsgs_default() drives the libnl3 receive loop: it calls recvmsg() on the generic-netlink socket, parses each message, and dispatches callbacks (here the NL_CB_VALID handler veil_event_cb installed at line 266). It returns 0 on success or a negative libnl error (-NLE_*) on failure; the loop at veil_shieldd.c:284-290 treats every code except -NLE_AGAIN as fatal, logs the raw numeric code, and breaks. Because the message prints the number without decoding, the actual cause (intr, parse error, bad socket, seq mismatch, dump interruption) is hidden from the operator. The daemon then exits with status 0, so a crashed event loop is indistinguishable from a clean shutdown.

Source

Thrown at dashboard/src/transport/WsClient.ts:75

  private frameSubs = new Set<(b: MagFrameBatch) => void>();
  private eventSubs = new Set<(e: NvsimEvent) => void>();
  private running = false;
  private framesEmitted = 0;
  private fpsLast = performance.now();
  private fpsCount = 0;

  /** @param baseUrl e.g. `http://localhost:7878` */
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.wsUrl = `${toWsUrl(this.baseUrl)}/ws/stream`;
  }

  private async json<T>(path: string, init?: RequestInit): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      ...init,
      headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) },
    });
    if (!res.ok) throw new Error(`${path}: ${res.status} ${res.statusText}`);
    return (await res.json()) as T;
  }

  async boot(): Promise<WsBootInfo> {
    if (this.bootInfo) return this.bootInfo;
    const h = await this.json<HealthBody>('/api/health');
    this.bootInfo = {
      buildVersion: h.nvsim_version,
      frameMagic: h.magic,
      frameBytes: h.frame_bytes,
      expectedWitnessHex: h.expected_witness_hex,
    };
    this.openWs();
    return this.bootInfo;
  }

  private openWs(): void {
    if (this.ws) return;

View on GitHub (pinned to 4685618388)

Solutions

  1. Decode the printed number against the libnl3 NLE_* enum (NLE_FAILURE=1, NLE_INTR=2, NLE_BAD_SOCK=3, NLE_MSGSIZE=5, NLE_NOATTR=8, NLE_SEQ_MISMATCH=14, NLE_DUMP_INTR=15, NLE_PARSE_ERR=16, NLE_NOMEM=18) - or print nl_geterror(r) instead of %d - to identify which failure path fired.
  2. If the value is -NLE_INTR (or -NLE_FAILURE right after Ctrl-C), it is benign shutdown noise: also skip -NLE_INTR in the loop condition, or check g_ctx.running before deciding the error is fatal.
  3. Audit veil_event_cb: it must return only NL_OK (0) or NL_SKIP for messages it ignores; any negative return propagates out of nl_recvmsgs_default and kills the loop.
  4. Keep nl_socket_disable_seq_check(g_ctx.sock) immediately after nl_socket_modify_cb - multicast mlme frames will otherwise trigger -NLE_SEQ_MISMATCH; ensure the duplicate copy in firmware/privshield has not dropped it.
  5. If strace shows ENOBUFS on the recvmsg, enlarge the socket buffers before the loop with nl_socket_set_buffer_size(g_ctx.sock, 1<<20, 1<<20).
  6. Never share g_ctx.sock across threads or free it elsewhere; libnl nl_sock has no internal locking.
  7. Return a non-zero exit code when the loop breaks unexpectedly (return g_ctx.running ? 1 : 0) so procd/supervisors restart the daemon instead of treating the crash as clean exit 0.

Example fix

// before (veil_shieldd.c:284-293)
while (g_ctx.running) {
    int r = nl_recvmsgs_default(g_ctx.sock);
    if (r < 0 && r != -NLE_AGAIN) {
        fprintf(stderr, "veil: nl_recvmsgs_default: %d\n", r);
        break;
    }
}
nl_socket_free(g_ctx.sock);
return 0;

// after
while (g_ctx.running) {
    int r = nl_recvmsgs_default(g_ctx.sock);
    if (r < 0 && r != -NLE_AGAIN && r != -NLE_INTR) {
        fprintf(stderr, "veil: nl_recvmsgs_default: %d (%s)\n",
                r, nl_geterror(r));
        break;
    }
}
nl_socket_free(g_ctx.sock);
return g_ctx.running ? 1 : 0; /* signal unexpected loop exit */
Defensive patterns

Strategy: try-catch

Validate before calling

/* Before entering the loop (after veil_nl_connect at line 263) */
if (nl_socket_get_fd(g_ctx.sock) < 0) { fprintf(stderr, "veil: bad nl fd\n"); return 1; }
if (g_ctx.family < 0)              { fprintf(stderr, "veil: nl80211 unresolved\n"); return 1; }
nl_socket_set_buffer_size(g_ctx.sock, 1 << 20, 1 << 20); /* survive mlme floods */

Type guard

/* C predicate: classify libnl receive errors as transient vs fatal */
static int nl_recv_transient(int r) {
    return r == -NLE_AGAIN || r == -NLE_INTR || r == -NLE_DUMP_INTR;
}

Try / catch

/* C error-branch equivalent of try/catch for the libnl event loop */
int r = nl_recvmsgs_default(g_ctx.sock);
if (r < 0) {
    if (nl_recv_transient(r)) continue;                 /* retry same iteration */
    fprintf(stderr, "veil: nl_recvmsgs_default: %s\n", nl_geterror(r));
    g_ctx.running = 0;                                  /* orderly teardown */
    exit_code = 1;                                      /* surface the failure */
}

Prevention

When it happens

Trigger: Specific producers: (1) SIGINT/SIGTERM delivered while the blocking recvmsg() is parked - on_signal() sets running=0 but the interrupted syscall surfaces as -NLE_INTR, which the loop logs as fatal; (2) veil_event_cb returning a negative value - libnl propagates callback errors straight out of nl_recvmsgs_default (e.g. after nla_parse fails on an unexpected mlme attribute); (3) ENOBUFS from the kernel when the 'mlme' multicast group overflows the socket rcvbuf (station flaps, auth/assoc storms); (4) -NLE_BAD_SOCK if the nl_sock is freed or its fd closed by another thread - libnl sockets are not thread-safe; (5) -NLE_SEQ_MISMATCH if a copy of this file drops the nl_socket_disable_seq_check() call at line 268, since multicast events carry out-of-sequence sequence numbers; (6) an unhandled NLMSG_ERROR ACK from the optional veil_set_tx_antenna_mask() request.

Common situations: Running this BUILD-ONLY OpenWrt scaffold (README/usage both flag it as untested on silicon) on a live AP where mlme events arrive; shutting the daemon down with Ctrl-C or procd stop and seeing a spurious error line; vendoring this file into firmware/privshield (an exact duplicate of the wifi-veil copy) and having one side drift - e.g. losing the seq-check-disable line; linking against an older OpenWrt libnl3 whose nl_recv maps unknown errnos to -NLE_FAILURE, making the printed number useless; running the antenna-mask self-check uncommented on a driver that rejects NL80211_CMD_SET_WIPHY, leaving an error reply on the socket.

Related errors


AI-assisted analysis of ruvnet/RuView@4685618388 (2026-08-16). Data as JSON: /api/errors/371aa63833aba857. Report an issue: GitHub.