There are multiple syscalls available to PebbleOS SDK applications (sys_send_pebble_event_to_kernel, sys_accel_manager_get_num_samples, sys_event_service_client_subscribe, sys_data_logging_create / sys_data_logging_log, sys_accel_manager_data_subscribe, and sys_pebble_log) that do not sufficiently validate arguments provided by the app, allowing either direct privilege escalation or arbitrary memory read and write (which can be trivially turned into privilege escalation).
The syscalls are located at addresses that are not provided directly in the jump table of symbols publicly documented and automatically linked into app binaries, but exist in the syscall table that apps are allowed to call if they know the address.
HIGH - Any application that a user installs from the app store (which anyone can publish to) could do anything on the watch, including silently listening to the microphone or bricking the watch permanently.
sys_send_pebble_event_to_kernel (src/fw/syscall/event_syscalls.c)This syscall does not validate the type of event, and posts the event to be processed directly to the kernel event loop. This allows sending a PEBBLE_CALLBACK_EVENT event which lets the caller provide a pointer to be called. In the below example, kernel_callback is provided by app code but called by the kernel after the sys_send call is made and the event loop processes the event.
ExploitEvent ev = { .callback = (void (*)(void *))kernel_callback, .data = 0, .task_mask = 0, .type = 0x1b, // PEBBLE_CALLBACK_EVENT }; void (*sys_send_pebble_event_to_kernel)(void *) = (void *)sys_send_pebble_event_to_kernel_addr; sys_send_pebble_event_to_kernel(&ev);
sys_accel_manager_get_num_samples (src/fw/services/accel_manager/service.c)This syscall does not validate the state structure pointer provided by the app, and can be used as a primitive to read or write an arbitrary 64-bit value to/from an arbitrary address. The read part can be used to read an 8-byte region first, and then written back, where you wish only to overwrite a single 4-byte value (e.g., a callback function pointer).
sys_accel_manager_get_num_samples_fn get_num_samples = (sys_accel_manager_get_num_samples_fn)SYS_ACCEL_MANAGER_GET_NUM_SAMPLES_ADDR; #define ACCEL_MANAGER_STATE_TIMESTAMP_MS_OFF ((uintptr_t)0x30) // read uint64_t value = 0; void *state = (void *)(addr - ACCEL_MANAGER_STATE_TIMESTAMP_MS_OFF); get_num_samples(state, &value); return value; // write AccelManagerState fake_state = {0}; fake_state.timestamp_ms = value; get_num_samples(&fake_state, (uint64_t *)addr);
sys_event_service_client_subscribe (src/fw/syscall/event_syscalls.c)This syscall accepts ListNode pointers provided within a struct from an app, but does not validate that they point to app memory, only that the struct itself is in app memory.
It can therefore be used to write 4 bytes into any address, with the limitation that the value written will also be written to as an address itself (i.e., *a = b and *b = a). In practice this does not matter if you are overwriting a pointer in kernel memory to point to app memory: it just means the kernel memory address will get written into your app memory. You can patch up your landing code buffer after overwriting the callback before triggering it to be called.
sys_data_logging_create / sys_data_logging_log (src/fw/services/data_logging/dls_syscalls.c)This pair of syscalls can be abused since they write logs to a user-provided buffer without validating the buffer is within app memory. Normally the applib function that is exposed as a symbol to SDK apps mallocs the buffer from the app heap, but the underlying syscall that this uses allows any buffer to be passed without validation. Passing a buffer that points at a target address in kernel memory allows an arbitrary-length write to any memory address.
DataLoggingSessionRef session = dls_create_sys(0x1337, // Tag DATA_LOGGING_BYTE_ARRAY, // Item Type 4, // Item Size (Length of our payload) (void *)S_CHANGE_CALLBACK_ADDR, // Buffer false // Resume ); uint32_t payload = (uint32_t)&syscall_callback_fn | 1; DataLoggingResult res = dls_log_sys(session, &payload, 1);
The last three do not directly allow for privileged execution, but there is a callback function in memory called s_change_callback which can be overwritten and triggered using a call to persist_write_int, since it gets called when any settings key changes.
You can avoid privilege being dropped when execution returns to the app by rewriting the stack to avoid prv_drop_privilege being called, replacing it with the original LR that was stashed in thread-local storage:
void patch_drop_privilege(void) { uintptr_t *sp; __asm__ volatile("mov %0, sp" : "=r"(sp)); for (int i = 0; i < 64; i++) { if (sp[i] == PRV_DROP_PRIVILEGE_ADDR) { sp[i] = GET_SYSCALL_LR(); } } }
sys_accel_manager_data_subscribe and sys_pebble_log)sys_accel_manager_data_subscribe: Allows abusing the accelerometer subscription API to get a kernel callback in user-controlled code.sys_pebble_log: Allows an unprivileged app to read any memory by providing a pointer to it, and then reading the contents that got logged out of external flash, which is memory-mapped and readable by external apps.Each of the above PoCs requires memory addresses to be located and provided for the specific firmware build being targeted. Because an app can identify what firmware version and device it is running on, it is possible to build an app that can exploit multiple firmware versions and device types in one go (and silently do nothing if running on an unknown combination).
Testing was completed on an Asterix / Core 2 Duo watch running firmware 4.9.168 and on the Emery / Pebble Time 2 QEMU images provided as part of the SDK. Symbols are available for Core Devices hardware on GitHub, making finding offsets trivial.
Core Devices fixed these issues in coredevices/PebbleOS PR #1280 and PR #1284 (released in firmware v4.9.177).
Date reported: May 8, 2026
Date fixed: May 13, 2026 (released in v4.9.177 on May 15, 2026)
Date disclosed: September 24, 2026