Reproducible SIGSEGV (139) crash at fixed binary offset 0x2c0ee15 — v0.16.22, PostgreSQL backend

Issue Description

The server process crashes with SIGSEGV (exit code 139). The crash is fully reproducible and occurs at the exact same binary offset (0x2c0ee15) across multiple pod restarts and across different recent versions — this points to a deterministic software bug rather than data corruption or environment issues.

What we ruled out:

Data integrity: Verified via stalwart-cli that all Accounts, Roles, Tenants, and Domains are present and structurally healthy. No orphaned/ghost index entries found.
Alert rules: No Alert objects configured (checked via both stalwart-cli query Alert and Web UI → Telemetry → Alerts) — ruling out alert-evaluation logic as the trigger.
Mail queue: Emails/Queued is empty.
Storage consistency: Confirmed PostgreSQL is used uniformly across all store types.

Expected Behavior

Trace log context leading up to the crash:
The crash consistently occurs immediately after a data store iteration combined with cache lookups for accessToken, role, and tenant. This suggests the bug may be located in a caching or iteration routine touching these object types.

Question:
Has anyone else seen a crash at this same offset (0x2c0ee15) on 0.16.22? Any pointers on what subsystem this offset maps to, or suggestions for further isolation (e.g. disabling specific internal tasks), would be appreciated. We have Enterprise support access and can provide full trace logs / core dump if useful.

Actual Behavior

Crash Error 139

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This isn’t covered in the current docs. A community member or staff member will follow up.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Update: After several pod restarts, we captured a case where the server started successfully. Notably, the log shows a PostgreSQL error (store.postgresql-error) reason = "connection closed" occurring immediately after Task manager started, at the exact same point in the startup sequence where the crash otherwise occurs:

causedBy = "crates/store/src/dispatch/store.rs:99"  
causedBy = "crates/common/src/storage/document.rs:47"  
causedBy = "crates/services/src/state_manager/push.rs:104"  

This suggests a race condition during startup between the task scheduler/state manager (push.rs) and the PostgreSQL connection pool not yet being fully warmed up. In this instance the error was handled gracefully and the server continued normally. We suspect that in the crashing cases, the same race condition is hit but with a timing variant that is not handled safely (e.g. a stale/invalid reference after the failed connection attempt), which would explain why the crash always occurs at the same binary offset.

This might narrow the search to the connection pool initialization / state_manager::push interaction at startup.

Update: Reproduced identically on bare-metal VM (outside Kubernetes) — confirms pure application bug, includes disassembly

To rule out any Kubernetes/container/Harvester-specific cause, we deployed stalwart (v0.16.22) as a native systemd service on a plain VM, connected to the exact same PostgreSQL database. The crash reproduces identically, with the exact same trace log pattern (two near-simultaneous accessToken/role/tenant cache lookups — key=3 then key=7 — immediately followed by the crash).

Kernel log from the VM:

kernel: tokio-rt-worker[1516]: segfault at 2a9870df ip 00006119dacb0e15 sp 00007b4f2b3f6170 error 4 in stalwart[6119d80a2000+4d51000] likely on CPU 0 (core 0, socket 0)  
kernel: Code: ef fe 49 89 c6 48 83 c3 10 48 89 df e8 04 59 af fe 4c 89 f7 e8 5c ce 13 02 e8 8a f6 ef fe cc cc cc cc cc cc cc 41 57 41 56 53 <4c> 8b 37 4d 85 f6 0f 84 91 00 00 00 48 89 fb 49 8b 46 40 0f 1f 84  

Calculating the binary-relative offset: ip (0x6119dacb0e15) - base (0x6119d80a2000) = 0x2c0ee15 — identical to the offset observed in every Kubernetes crash reported above. This confirms it is the exact same fault, independent of the deployment environment (container vs. bare VM), and rules out Kubernetes/Harvester/CNI/cgroup/resource factors entirely.

The faulting thread is tokio-rt-worker, and error 4 indicates a user-mode read page fault. The faulting instruction at the <4c 8b 37> marker corresponds to mov r14, [rdi] — i.e., a pointer dereference where rdi holds an invalid/dangling address (sp=0x7b4f2b3f6170, fault addr 0x2a9870df looks like a small/garbage pointer value, not a legitimate heap address). This is consistent with a use-after-free or dereference of a not-yet-initialized/already-dropped shared structure, matching our hypothesis of a race condition in the Role/Tenant/AccessToken cache population path during startup, when two authentication requests are resolved concurrently.

We’d appreciate it if a maintainer could run addr2line/objdump -d against the 0.16.22 release binary at offset 0x2c0ee15 to identify the exact function — happy to provide the raw binary or further disassembly if needed.

Hi all,
We did some more troubleshooting and have done the following:

  1. Export Data
    stalwart --config=/etc/stalwart/config.json --export /root/stalwart-full-backup
  2. Created new Database and user
    kubectl exec -it -n mail mail-db-1 -c postgres – psql -U postgres <<EOF

CREATE USER stalwart_new WITH ENCRYPTED PASSWORD ‘$NEW_PASSWORD’;
CREATE DATABASE stalwart_new OWNER stalwart_new;
GRANT ALL PRIVILEGES ON DATABASE stalwart_new TO stalwart_new;
EOF
3. Restore Data
stalwart --config=/etc/stalwart/config.json --import /root/stalwart-full-backup

After restarting Stalwart we get again the following:
Sep 17 12:18:09 mail2 stalwart[6432]: 2026-09-17T12:18:09Z DEBUG Cache miss (store.cache-miss) key = 7, collection = “accessToken”
Sep 17 12:18:09 mail2 stalwart[6432]: 2026-09-17T12:18:09Z DEBUG Cache hit (store.cache-hit) key = 1, collection = “role”
Sep 17 12:18:09 mail2 stalwart[6432]: 2026-09-17T12:18:09Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
Sep 17 12:18:09 mail2 kernel: tokio-rt-worker[6434]: segfault at 4524c0df ip 0000564ddfaffe15 sp 000070ab45df6170 error 4 in stalwart[564ddcef1000+4d51000] likely on CPU 0 (core 0, socket 0)
Sep 17 12:18:09 mail2 kernel: Code: ef fe 49 89 c6 48 83 c3 10 48 89 df e8 04 59 af fe 4c 89 f7 e8 5c ce 13 02 e8 8a f6 ef fe cc cc cc cc cc cc cc 41 57 41 56 53 <4c> 8b 37 4d 85 f6 0f 84 91 00 00 00 48 89 fb 49 8b 46 40 0f 1f 84
Sep 17 12:18:09 mail2 systemd[1]: stalwart.service: Main process exited, code=dumped, status=11/SEGV
Sep 17 12:18:09 mail2 systemd[1]: stalwart.service: Failed with result ‘core-dump’.

This in the VM.

We need some urgent help here since our Mail Services are down!

Thanks

Hi all,
We have now added 150 Client Licenses.
How is the Enterprise Prio Support working?

Thanks for feedback.

Start the node with STALWART_RECOVERY_MODE=1. That skips every background service and brings the management API up on port 8080. If it starts clean, the fault is in one of those services, and that alone cuts the search space a long way. To run normally minus the suspect path, create a ClusterRole with the pushNotifications task disabled and point STALWART_ROLE at it. If that starts too, you have mail flowing while we work out the rest.

The ordering in your log (accessToken miss, then role hit, then store.data-iterate) is an access token being built: permissions get resolved, then an ACL query iterates. That runs on every authentication, not only in the push notification sweep, so it pins the moment but not the caller. What the offset tells us is solid though. We build with lto = true and codegen-units = 1, so a stable offset from the load base on both Docker and bare metal means the same monomorphised function every crash, which in turn means one specific stored record type is being read back. I cannot tell you which one from here, and that is something we need your help with.

addr2line will never give you anything against the shipped binary. Our release profile sets debug = false and strip = true, so the symbols are gone before the binary ever ships. Quickest way through is to reproduce the crash under a build that still has them:

git clone https://github.com/stalwartlabs/stalwart
cd stalwart
git checkout v0.16.22

In the top-level Cargo.toml, under [profile.release], change debug = false to debug = 2 and strip = true to strip = false. Leave opt-level, lto and codegen-units exactly as they are; changing those changes the code path and you may lose the bug. Then build with the same features as the Docker image:

cargo build --release -p stalwart --no-default-features \
  --features "sqlite postgres mysql rocks s3 redis azure nats enterprise"

Point that binary at a copy of the database, not the live one. Two nodes against the same PostgreSQL will fight over the same keys and you will get failures that have nothing to do with this bug.

For the core dump, on the bare metal VM the systemd route is easiest. Add LimitCORE=infinity to the unit, systemctl daemon-reload, restart, and let it crash. Then:

coredumpctl list stalwart
coredumpctl gdb stalwart

If systemd-coredump is not installed, ulimit -c unlimited before launching plus a writable /proc/sys/kernel/core_pattern gets you the same file. Inside gdb the four things I want are:

thread apply all bt full
frame 1
info registers rdi rsi rdx
x/64xb $rdi

The register dump matters as much as the backtrace here. The faulting instruction is a load from the first argument, so whatever rdi holds is the bad pointer, and the bytes around it tell us whether we are walking a relative pointer out of a stored record or chasing freed memory. Those two have very different fixes.

RUST_BACKTRACE=1 will not help, so do not bother setting it. A SIGSEGV does not unwind, so no Rust backtrace is ever printed.

On traces, you already have trace level running, which is what I need. Send the whole window from process start through to the fault, not just the last few lines. The account id is the useful part: your paste shows key = 7, collection = "accessToken" immediately before the crash, so account 7 is the first thing I would look at.

You can pull the suspect record straight out of PostgreSQL. Push subscriptions live in table p under a ten byte key: account id as a big-endian u32, then collection 7, then field 44, then a zero document id.

SELECT (get_byte(k,0)<<24)|(get_byte(k,1)<<16)|(get_byte(k,2)<<8)|get_byte(k,3)
         AS account_id,
       length(v) AS len,
       encode(v, 'hex') AS value
FROM p
WHERE length(k) = 10 AND get_byte(k,4) = 7 AND get_byte(k,5) = 44;

That should be a short list. If you would rather not paste record contents into a public thread, send them privately to [email protected].

One more thing, and it may turn out to be the whole story. Did this deployment come up from v0.15.x or older, and if so, how was that upgrade done? I am asking specifically whether you went through the migration proxy or did the offline steps from UPGRADING/v0_16.md by hand. The offline path requires every node to be stopped for the duration, and on Kubernetes a rolling update does the opposite by default: it runs old and new pods against the same database at the same time. A v0.15.x pod writing records while a v0.16.x pod is migrating produces exactly the kind of record that would read back as garbage, and the symptom would be permanent and survive an export and import, which matches what you are seeing. If that is what happened, this is a migration bug on our side and I want to fix the migration, not just this one record.

Also useful to know: how many replicas were running during the upgrade, and whether the crash predates the export and import or first appeared after it.

Hi Stalwart,
Thanks a lot, STALWART_ROLE=no-push was the temporary solution so that we can access the EMails again, great.
I need some more time to check everything, then I will create a VM with the coredump etc…
I will come back to you…

Thanks,
Peter

Great to read that the email is back online again. In the meantime while you collect the coredump, can you confirm if this is a migrated instance or a fresh v0.16 install?

Hello,
It was a fresh v0.16 install, I had no v0.15 installed.

Thanks, in that case we will need the dump. In the meantime while you build Stalwart with debugging info, it will help if you can send us a dump of the push subscription table.