[Bug]: Segfault on pause/resume — use-after-free in PushNotifications::isReady() from ETag poll timer
- Dominant language
- C++
- Stars
- 3.9k
- Forks
- 1k
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 123
Description
### ⚠️ Before submitting, please verify the following: ⚠️
- [x] This is a **bug**, not a question or a configuration issue.
- [x] This issue is **not** already reported on Github (I have searched for it).
- [x] Nextcloud Server and Desktop Client are **up to date**. See [Server Maintenance and Release Schedule](https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule) and [Desktop Releases](https://nextcloud.com/install/#install-clients) for supported versions.
- [x] I agree to follow Nextcloud's [Code of Conduct](https://nextcloud.com/contribute/code-of-conduct/)
### Bug description
Client segfaults reproducibly when sync is paused and then resumed. The crash is a **use-after-free of the `PushNotifications` object, dereferenced from the ETag poll timer slot**.
I captured a backtrace under gdb and traced it to source. The faulting instruction is a one-byte field read on a stale pointer:
```
Thread 1 "AppRun" received signal SIGSEGV, Segmentation fault.
#0 PushNotifications::isReady() <- reads _isReady at object offset 0x41
#1 FolderMan::pushNotificationsFilesReady(account)
#2 (copy_if lambda in slotEtagPollTimerTimeout)
#3-#7 FolderMan::slotEtagPollTimerTimeout() and inlined helpers
#8 QtPrivate signal dispatch
#9 QTimer::timeout(QTimer::QPrivateSignal)
#10 QObject::event
#11 QApplicationPrivate::notify_helper
#12 QCoreApplication::notifyInternal2
#13 QTimerInfoList::activateTimers
... QEventDispatcherGlib / QEventLoop::exec / QCoreApplication::exec
```
Kernel log for the same crash:
```
AppRun[5280]: segfault at 41 ip ... error 4 in nextcloud[454d04,...]
```
`error 4` = user-mode **read** of an unmapped page; fault address `0x41`.
**Faulting function** (frame #0), disassembled from the shipped binary at the crashing offset `0x454d04`:
```asm
push %rbp
mov %rsp,%rbp
mov %rdi,-0x8(%rbp) ; this
mov -0x8(%rbp),%rax
movzbl 0x41(%rax),%eax ; <-- FAULT: read byte _isReady at this+0x41
pop %rbp
ret
```
This is exactly `bool PushNotifications::isReady() const { return _isReady; }`, with `_isReady` at object offset `0x41`. `this` (`%rax`) is a stale pointer, so the read lands at `0x41`.
**Root cause.** The call chain is:
```cpp
// FolderMan::slotEtagPollTimerTimeout(), in the copy_if lambda:
const auto account = folder->accountState()->account();
return !pushNotificationsFilesReady(account);
// FolderMan::pushNotificationsFilesReady():
const auto pushNotifications = account->pushNotifications();
const auto pushFilesAvailable = account->capabilities().availablePushNotifications() & PushNotificationType::Files;
return pushFilesAvailable && pushNotifications && pushNotifications->isReady();
```
The `&&` guards a **null** `pushNotifications` but not a **dangling** one. `Account::trySetupPushNotifications()` does a raw `delete _pushNotifications; _pushNotifications = nullptr;` (and recreates it) when the WebSocket drops. Pause→resume drops and re-establishes the push WebSocket, tearing down and recreating the `PushNotifications` object. `Account::pushNotifications()` returns the raw member with no liveness check.
If the ETag poll timer fires during that reconnect window, `pushNotificationsFilesReady()` reads a `pushNotifications` pointer that is non-null but points at freed memory (or is mid-teardown across the signal chain), passes the null guard, and calls `isReady()` on dead memory → segfault at `0x41`.
This only fires for accounts whose folders are **not** using push notifications yet (the poll path). My log shows `Number of folders that don't use push notifications: 2`, and the ETag poll timer runs every ~30s — matching the trigger.
### Steps to reproduce
1. Account with 2 sync folders, push notifications not (yet) active for them (client polls via ETag every 30s).
2. Pause synchronization.
3. Resume synchronization.
4. Client segfaults within ~1s of the sync completing (when the next poll-timer tick hits the reconnect window). Intermittent by nature (it's a race between the ETag poll timer and the push-notifications reconnect), but reproduces reliably within a few pause/resume cycles here.
### Expected behavior
Pausing and resuming sync should not crash. `pushNotificationsFilesReady()` / `PushNotifications::isReady()` must not be invoked on a torn-down `PushNotifications` object; the poll-timer slot should hold only a valid pointer (or the teardown/recreate should be sequenced so the poll path can't observe a dangling pointer).
### Which files are affected by this bug
src/libsync/pushnotifications.cpp (PushNotifications::isReady()), src/gui/folderman.cpp (FolderMan::pushNotificationsFilesReady(), slotEtagPollTimerTimeout()), src/libsync/account.cpp (Account::pushNotifications(), Account::trySetupPushNotifications())
### Operating system
Linux
### Which version of the operating system you are running.
Ubuntu 24.04.4 LTS
### Installation method
Official Linux AppImage
### Nextcloud Server version
31.0.6.2
### Nextcloud Desktop Client version
33.0.7 (build 40389), Qt 6.10.2, OpenSSL 3.5.7
### Did this occur after an update or on a clean installation?
Clean desktop client installation
### Are you using the Nextcloud Server Encryption module?
No
### Are you using an external user-backend?
- [ ] Default internal user-backend
- [ ] LDAP or Active Directory
- [ ] SSO - SAML
- [ ] Other
### Nextcloud Server logs
```shell
Client log ends cleanly right before the crash (the segfault is in the Qt/GUI thread post-sync, so nothing is logged at the fault):
... syncengine.cpp:949 Sync run took 443 ms
... folder.cpp:1334 SyncEngine finished without problem.
... owncloudgui.cpp:253 Sync state changed for folder "https://.../remote.php/dav/files/USER/" : "Success"
```
### Additional info
- Full gdb backtrace (all threads) below.
- Related historical reports in the same pause/resume + timer/reconnect area, all stale/unfixed: #1684 (QMutex::lock on pause→sleep→resume→settings), #4084 (no recovery from standby), #6861 (resume-after-error). This report differs in that the faulting frame is pinned to `PushNotifications::isReady()` via the ETag poll timer.
- Suggested fix directions (pick per maintainer preference): (a) validate liveness in `pushNotificationsFilesReady()` beyond the null check, (b) use `deleteLater()` + a QPointer/guard for `_pushNotifications` so stale raw pointers can't be dereferenced, or (c) stop/skip the ETag poll timer while the push WebSocket is reconnecting.
Full gdb backtrace — crash thread (Thread 1)
Binary is the stripped official AppImage, so the client's own frames show as `??`; addresses are annotated from disassembly + source mapping. The other ~20 threads are all idle (`__futex_abstimed_wait` in `QWaitCondition::wait` thread-pool workers, `ppoll` in `QNetworkAccessManager`/`gmain`/`gdbus`/`QXcbEventQueue`/`QDBusConnection`) — none relevant to the fault.
```
Thread 1 "AppRun" received signal SIGSEGV, Segmentation fault.
#0 0x00005555559a8d04 PushNotifications::isReady() [movzbl 0x41(%rax) — reads _isReady on stale this]
#1 0x0000555555a00137 FolderMan::pushNotificationsFilesReady(account)
#2 0x000055555598fb1c (copy_if lambda in slotEtagPollTimerTimeout)
#3 0x00005555556f8014 FolderMan::slotEtagPollTimerTimeout()
#4 0x00005555556fc3d6 "
#5 0x00005555556f8086 "
#6 0x00005555556f2c78 "
#7 0x00005555556ec98b "
#8 0x00007fffe7409222 in ?? () from libQt6Core.so.6
#9 0x00007fffe741905e in QTimer::timeout(QTimer::QPrivateSignal) () from libQt6Core.so.6
#10 0x00007fffe73fab75 in QObject::event(QEvent*) () from libQt6Core.so.6
#11 0x00007fffe9192eea in QApplicationPrivate::notify_helper(QObject*, QEvent*) () from libQt6Widgets.so.6
#12 0x00007fffe73a01a8 in QCoreApplication::notifyInternal2(QObject*, QEvent*) () from libQt6Core.so.6
#13 0x00007fffe757388f in QTimerInfoList::activateTimers() () from libQt6Core.so.6
#14 0x00007fffe76c6c24 in ?? () from libQt6Core.so.6
#15 0x00007fffe4d07a76 in ?? () from libglib-2.0.so.0
#16 0x00007fffe4d0ade7 in ?? () from libglib-2.0.so.0
#17 0x00007fffe4d0b4fc in g_main_context_iteration () from libglib-2.0.so.0
#18 0x00007fffe76c6dd3 in QEventDispatcherGlib::processEvents(...) () from libQt6Core.so.6
#19 0x00007fffe73acefb in QEventLoop::exec(...) () from libQt6Core.so.6
#20 0x00007fffe73a85de in QCoreApplication::exec() () from libQt6Core.so.6
#21 0x000055555567a7b5 in ?? ()
#22 0x00007fffe6a2a1ca in __libc_start_call_main (...)
#23 0x00007fffe6a2a28b in __libc_start_main_impl (...)
#24 0x000055555567971e in ?? ()
rip 0x5555559a8d04 rsp 0x7fffffffc3c0
```
Kernel log, same crash (independent run — note the load-relative offset `454d04` is identical, i.e. a deterministic fault site):
```
AppRun[5280]: segfault at 41 ip 0000567647e15d04 error 4 in nextcloud[454d04,567647ade000+3e2000]
```
Contributor guide
Research direction
Start by reading src/gui/folderman.cpp, especially pushNotificationsFilesReady() and slotEtagPollTimerTimeout(), then inspect the lifetime handling in src/libsync/account.cpp and src/libsync/pushnotifications.cpp. Reproduce pause/resume under gdb while observing the ETag timer and push reconnect. Done means repeated pause/resume cycles no longer segfault or dereference a torn-down PushNotifications object.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100