11 KiB
Crash Fixes Reference
This document catalogues crashes that have been diagnosed and fixed in Didactyl. Use it as a reference when investigating future crashes — the symptoms, root causes, and investigation techniques described here may save significant debugging time.
1. Silent process exit on relay disconnect — SIGPIPE (v0.0.24)
Date: 2026-03-02
Version: v0.0.23 → fixed in v0.0.24
Severity: Critical — process terminates without any log output
Files changed: src/main.c
Symptoms
- The process exits cleanly to the shell prompt with no error message.
- The
[didactyl] shutting downlog line does not appear. - No coredump is generated.
- The last log lines show all relays transitioning from
connected → disconnectedand then immediatelydisconnected → connected. - The crash typically follows a period of network instability or DNS resolution
failure — for example, an LLM HTTP request failing with
curl=Could not resolve hostname.
Root cause
SIGPIPE was never handled. The default OS action for SIGPIPE is to terminate
the process immediately — no signal handler runs, no cleanup occurs, no coredump
is written.
The TLS write path in nostr_core_lib uses SSL_write(), which internally calls
write() on the underlying socket. When a wss:// relay drops its TCP connection
and the relay pool attempts to write to that socket, the kernel delivers SIGPIPE.
The plain-TCP path for ws:// connections was already protected by using
MSG_NOSIGNAL on send() calls, but the TLS path had no equivalent protection.
How it was diagnosed
- No crash message or coredump ruled out
SIGSEGV,SIGABRT, and OOM. - No shutdown log ruled out a graceful exit via the signal handler.
dmesgandjournalctlshowed no OOM-kill or segfault entries.- Searching the entire codebase for
SIGPIPEorSIG_IGNreturned zero results. - The websocket TLS transport was confirmed to use
SSL_write()withoutMSG_NOSIGNALor any per-thread signal mask. - The timeline matched: relay disconnects occurred, the next poll cycle attempted a write to a dead TLS socket, and the process vanished.
Fix
Added signal(SIGPIPE, SIG_IGN) in main() alongside the existing SIGINT and
SIGTERM handlers:
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGPIPE, SIG_IGN); /* ← added */
This causes SSL_write() and any other write to a broken pipe to return -1 with
errno = EPIPE instead of killing the process. The relay pool and libcurl already
handle write errors gracefully.
How to recognise this class of bug in the future
- Process disappears without any log output or coredump.
- Happens after network disruption or relay disconnects.
ulimit -cmay be 0, but even with unlimited core size,SIGPIPEdoes not produce a coredump by default.- Any new network transport layer added to the project should be audited for
SIGPIPEprotection.
2. Use-after-free in execute_nostr_list_manage — SIGSEGV (v0.0.24)
Date: 2026-03-01 (coredump), fixed 2026-03-02
Version: v0.0.23 → fixed in v0.0.24
Severity: Critical — segmentation fault during tool execution
Files changed: src/tools.c
Symptoms
SIGSEGVcrash during execution of thenostr_list_managetool.- Coredump shows the crash at
src/tools.c:29insidejson_error(), but with heavily corrupted stack frames — the real crash site is inexecute_nostr_list_manage(). - The corrupted stack is characteristic of heap corruption from use-after-free.
Root cause
In execute_nostr_list_manage(), the action pointer was obtained from the
args cJSON tree:
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
Later, args was freed:
cJSON_Delete(args); /* frees the entire tree including action */
But action->valuestring was still accessed afterwards:
cJSON_AddStringToObject(out, "action", action->valuestring); /* dangling pointer */
After cJSON_Delete(args), the action pointer is dangling. Accessing
action->valuestring reads freed heap memory, which may contain arbitrary data
or may have been reallocated for another purpose.
How it was diagnosed
- The coredump was extracted from systemd-coredump storage:
zstd -d /var/lib/systemd/coredump/core.didactyl_static.*.zst -o /tmp/core.bin gdb ./didactyl_static_x86_64_debug /tmp/core.bin -batch -ex "bt" - The backtrace showed
execute_nostr_list_managewith corrupted frames. - Code review of the function identified the
cJSON_Delete(args)call occurring before the last use ofaction->valuestring.
Fix
Deferred cJSON_Delete(args) until after action->valuestring has been consumed
by cJSON_AddStringToObject(). The free now occurs immediately after the last use:
cJSON_AddStringToObject(out, "action", action->valuestring);
cJSON_Delete(args); /* ← moved here, after last use of action */
All early-return error paths before this point also received their own
cJSON_Delete(args) call to prevent leaks.
How to recognise this class of bug in the future
SIGSEGVwith corrupted or nonsensical stack frames.- Crash location reported by GDB does not match the actual buggy code — the corruption happened earlier.
- Any function that calls
cJSON_Delete()on a parent object should be audited to ensure no child pointers are used afterwards. - Pattern to watch for:
cJSON* child = cJSON_GetObjectItemCaseSensitive(parent, "key"); /* ... */ cJSON_Delete(parent); /* ... */ use(child->valuestring); /* BUG: child is dangling */
3. DM subscription not receiving incoming kind 4 events — INVESTIGATION (v0.0.24)
Date: 2026-03-02
Version: v0.0.24
Severity: High — agent does not respond to incoming DMs
Status: Under investigation — diagnostic logging added
Files changed: src/nostr_handler.c
Symptoms
- The agent starts up normally, connects to relays, and can send kind 4 DMs.
- Incoming kind 4 messages posted to the same relays are never processed.
- No error messages appear in the log — the agent simply sits idle after sending its startup DM.
- The last log lines show successful outbound DM publishing but no inbound event processing.
Possible root causes (under investigation)
-
Events never reach
on_event()callback — The relay pool library only callson_event()when it receives anEVENTmessage matching the subscription ID. If the subscription was silently closed, errored, or not re-established after a relay reconnect, no events would be delivered. The relay could also be rejecting the subscription filter. -
sincefilter timing mismatch —g_start_timeis set totime(NULL)duringnostr_handler_init()(early in startup), but the DM subscription is created much later (after admin context subscription, startup event reconciliation, and startup DM sending). If the sender's clock is behind the server's clock, theircreated_attimestamp would be beforeg_start_timeand the relay would filter them out. -
Sender tier filtering — If the sender's pubkey doesn't match the admin pubkey and isn't in the WoT contact list, the message is classified as
DIDACTYL_SENDER_STRANGERand silently dropped (only a DEBUG_LOG at level 4). -
Signature verification failure — If
verify_signaturesis enabled and the event has an invalid signature, it is dropped with only a WARN log. -
Pool-level deduplication — The relay pool's
is_event_seen()cache is shared across all subscriptions. If an event ID was somehow marked as seen by another subscription, it would be silently dropped.
Diagnostic logging added
TRACE-level (level 5 / --debug 5) logs were added at every decision point
in the on_event() callback and the nostr_handler_subscribe_dms() setup:
| Log message prefix | What it tells you |
|---|---|
DEBUG on_event ENTRY |
Callback was called — events are reaching didactyl |
DEBUG on_event NULL guard |
Event, config, or callback pointer is NULL |
DEBUG on_event: missing required fields |
Event JSON is malformed |
DEBUG on_event: kind=N id=... from=... |
Event kind, ID, and sender pubkey |
DEBUG on_event: ignoring non-kind4 |
Event is not kind 4 |
DEBUG on_event: no p-tag found |
Kind 4 event has no p tag |
DEBUG on_event: p-tag mismatch |
p tag doesn't match agent's pubkey |
DEBUG on_event: sender=... tier=N |
Sender tier classification |
DEBUG on_eose called |
EOSE received from relays |
DEBUG DM subscription filter |
Full subscription filter JSON |
DEBUG DM subscription g_start_time=... |
since timestamp and time delta |
DEBUG DM subscription sub=... |
Subscription pointer (confirms creation) |
How to use the diagnostic logs
- Build with
./build_static.sh --debug - Run with
--debug 5to enable TRACE output - Send a kind 4 DM to the agent
- Check the output:
- No
on_event ENTRYlines → problem is at the relay/subscription level on_event ENTRYappears but processing stops → the specific drop-point log identifies the exact filter that rejected the event
- No
- Check the
DM subscription filterlog to verify thesincetimestamp and#ptag are correct
How to recognise this class of bug in the future
- Agent can send but not receive — asymmetric connectivity.
- No error messages in the log — silent event filtering.
- The subscription filter (
since,#p,kinds) may not match what the sender is actually publishing. - Clock skew between sender and receiver can cause
sincefilter mismatches. - Relay reconnections may not automatically re-subscribe.
General debugging checklist
When investigating a crash in Didactyl, work through these steps:
- Check for coredumps:
ls /var/lib/systemd/coredump/ | grep didactyl - Check dmesg/journalctl:
dmesg | grep -i 'didactyl\|oom\|segfault' - Check for the shutdown log line: If
[didactyl] shutting downis missing, the process was killed by a signal that bypassed the handler. - Check ulimit:
ulimit -c— if 0, coredumps are disabled. - Extract and analyse coredumps:
zstd -d /var/lib/systemd/coredump/core.didactyl*.zst -o /tmp/core.bin gdb ./didactyl_static_x86_64_debug /tmp/core.bin -batch -ex "bt full" - Common silent killers:
SIGPIPE— process writes to a broken socket/pipeSIGKILL— OOM killer or external killSIGBUS— memory-mapped file issues
- Common crash causes:
- Use-after-free on cJSON child pointers after parent deletion
- Buffer overflows in fixed-size stack buffers
- NULL pointer dereference on failed allocations
- Re-entrancy in callbacks during internal
nostr_relay_pool_poll()calls