nextcloud / nextcloud/desktop

[Bug]: Segfault on pause/resume — use-after-free in PushNotifications::isReady() from ETag poll timer

Open
#10,438 0 comments 0 reactions 0 assignees View on GitHub
0. Needs triage bug os: :penguin: Linux
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.