10 KiB
CamSwapper Status - Android 16 (Pixel 9a / tegu)
Date: 2026-05-08
Device: Pixel 9a (tegu)
Android Version: 16 (API 36)
Root Solution: KernelSU with ReZygisk
Executive Summary
Goal: Replace physical camera frames with virtual video/RTSP stream at the system level (HAL layer).
Current Status: ⚠️ PARTIALLY WORKING - Hook library built and Zygisk module loads, but injection into camera processes FAILS because cameraserver and camera provider are init services, not zygote-forked processes.
Process Architecture (Pixel 9a)
Camera-Related Processes
| Process | PID | PPID | Type | Can Zygisk Hook? |
|---|---|---|---|---|
cameraserver |
~12018 | 1 (init) | init service | ❌ NO |
camera.provider |
~12019 | 1 (init) | init service | ❌ NO |
virtual_camera |
~12361 | 1 (init) | init service | ❌ NO |
zygote64 |
958 | 1 (init) | init service | ✅ YES (but irrelevant) |
system_server |
varies | zygote64 | zygote-forked | ✅ YES |
Key Discovery
From zygisk.hpp line 30-45:
"Please note that modules will only be loaded after zygote has forked the child process."
This means Zygisk CANNOT hook init services. The cameraserver and camera provider are started by init (PPID=1), not forked from zygote.
What Works
1. HAL Hook Library (libcamera_hook.so)
- Built: 336KB (stripped), ARM64 aarch64
- Location:
/data/local/camera_magic/libcamera_hook.so - Functionality:
- ✅ Intercepts
dlopen/dlsymto hook Camera HAL symbols - ✅ Implements
CameraProviderHook,CameraDeviceHook,CameraDeviceSessionHook - ✅
inject_video_frame()implemented (reads from shared memory ring buffer) - ✅ Video decoder (MediaCodec NDK) → YUV420 → shared memory
- ✅ RTSP client → RTP receive → shared memory
- ✅ Buffer converter (YUV420↔NV21 with NEON)
- ✅ Intercepts
2. Zygisk Module (camswapper-zygisk-v2.zip)
- Properly implemented using Zygisk API v4
- File:
/data/adb/modules/camera-hook-zygisk/zygisk/arm64-v8a.so - Status: Loaded in ReZygisk (confirmed in
/data/adb/rezygisk/state.json) - Symbols:
zygisk_module_entryexported correctly - Code: Uses
zygisk::ModuleBasewithREGISTER_ZYGISK_MODULE(CamSwapperModule)
3. KernelSU Module (camswapper-kernelsu-v2.zip)
- Contains:
libcamera_hook.so,service.sh,post-fs-data.sh - Injection method: ptrace dlopen (BROKEN - see below)
What Does NOT Work
1. ❌ Zygisk Injection Fails for Camera Processes
Root Cause: cameraserver and camera.provider are init services (PPID=1), not zygote-forked.
Evidence:
$ ps -A -o pid,ppid,cmd | grep camera
12018 1 cameraserver
12019 1 camera.provider
12361 1 virtual_camera
Zygisk module loaded in zygote64:
$ cat /proc/zygote64/maps | grep camera-hook
70fa88f000-70fa8c2000 r-xp ... /data/adb/modules/camera-hook-zygisk/zygisk/arm64-v8a.so
But NOT in cameraserver:
$ cat /proc/cameraserver/maps | grep camera-hook
(empty - not loaded)
Conclusion: Zygisk postServerSpecialize() only runs for system_server, not cameraserver. The is_camera_process() check in zygisk_entry.cpp never triggers because cameraserver isn't zygote-forked.
2. ❌ Ptrace dlopen Injection Crashes (SIGSEGV)
Method: Attach to cameraserver via ptrace, call __loader_dlopen in linker64 to load libcamera_hook.so.
Result: Crashes with SIGSEGV when loading libcamera_hook.so (works for libc.so).
Root Cause: Linker namespace restrictions on Android 16. The __loader_dlopen function requires proper namespace setup that ptrace can't replicate.
Evidence (from previous testing):
- dlopen for
libc.so→ returns valid handle (0x2443dbca3ce35c9b) - dlopen for
libcamera_hook.so→ returns NULL or crashes - APEX linker namespace blocks
/data/local/tmppaths
3. ⚠️ Virtual Camera Service (Potential Solution)
Service: virtual_camera (PID ~12361)
Interface: android.hardware.camera.provider.ICameraProvider/virtual/0
Status: Running but provides 0 camera devices
Evidence:
$ dumpsys android.hardware.camera.provider.ICameraProvider/virtual/0
== Camera Provider HAL ... virtual/0-1 (v2.0, remote) static info: 0 devices: ==
Service Definition (/system/etc/init/virtual_camera.hal.rc):
service virtual_camera /system/bin/virtual_camera
class core
user system
group system
interface aidl virtual_camera
interface aidl android.hardware.camera.provider.ICameraProvider/virtual/0
oneshot
disabled
Key Finding: The service is "disabled" in RC file but is somehow running. It provides 0 devices, meaning it's not configured with a virtual camera stream.
Attempted Solutions (All Failed)
1. wrap.cameraserver Property
- Method: Set
wrap.cameraserver=/system/bin/cameraserver_wrapper - Result: ❌ Silently ignored on production build (ro.debuggable=0)
2. Bind-Mount Wrapper
- Method: Bind-mount wrapper binary over
/system/bin/cameraserver - Result: ❌ cameraserver crashes (SELinux context or APEX library resolution)
3. Ptrace dlopen Injection
- Result: ❌ SIGSEGV (linker namespace issue)
4. Zygisk Module (API v4)
- Result: ❌ Loads in zygote but can't hook init services
5. Original zygisk_entry.cpp (Wrong API)
- Problem: Exported raw C symbols (
zygisk_module_entry,pre_app_specialize, etc.) - Fix: Rewrote to use proper
zygisk::ModuleBaseclass - Result: Still can't hook init services (fundamental limitation)
Path Forward (Recommended Approaches)
Option A: Use virtual_camera Service (BEST OPTION)
Concept: Instead of hooking existing camera provider, configure Android's built-in virtual_camera service to provide virtual camera devices.
Steps:
- Determine how to configure
virtual_camerato provide a camera device - Point it to our video/RTSP stream
- No injection needed - uses Android's native virtual camera support
Research Needed:
- Read AOSP source for
virtual_cameraservice - Find configuration file or binder interface to add virtual camera streams
- Check
IVirtualCameraServiceinterface (service list shows it exists)
Option B: Hook at Binder IPC Layer
Concept: Intercept camera HAL binder calls instead of hooking the process.
Method:
- Use binder hook (via Zygisk or KernelSU)
- Intercept
ICameraDeviceSession::processCaptureRequest - Replace buffer contents before they reach the camera HAL
Advantage: Works regardless of which process handles the camera
Option C: Modify init.rc (Require Reboot + Possible Bootloop)
Concept: Add setenv LD_PRELOAD=/path/to/libcamera_hook.so to cameraserver service definition.
Risk: High - modifying init.rc can cause bootloops on production builds
Option D: Use virtualizationservice (Discovered Running)
Concept: Pixel 9a runs virtualizationservice (PID 6360) which spawns virtmgr_virtualizationservice and virtual_camera.
Possibility: The virtual camera infrastructure is already running - we just need to configure it properly.
File Inventory
Built Files
| File | Size | Purpose |
|---|---|---|
native/build/libcamera_hook.so |
336KB | HAL hook library (stripped) |
zygisk-module/zygisk/arm64-v8a.so |
223KB | Zygisk module (proper API v4) |
camswapper-kernelsu-v2.zip |
1.9MB | KernelSU module zip |
camswapper-zygisk-v2.zip |
79KB | Zygisk module zip |
Source Files
| File | Lines | Purpose |
|---|---|---|
native/src/camera_wrapper.cpp |
386 | HAL hook with inject_video_frame() |
native/src/video_decoder.cpp |
401 | MediaCodec decoder → shared memory |
native/src/rtsp_client.cpp |
653 | RTSP client → RTP receive |
native/src/buffer_converter.cpp |
173 | YUV420↔NV21 conversion |
zygisk-module/zygisk_entry.cpp |
88 | Zygisk module (API v4) |
zygisk-module/jni/zygisk.hpp |
391 | Zygisk API header |
Headers
| File | Purpose |
|---|---|
native/include/video_decoder.h |
Ring buffer shared memory definitions |
native/include/rtsp_client.h |
RTSP client state structures |
native/include/camera_hal/*.h |
AIDL camera HAL interface definitions |
Key Learnings for Future AI
1. Always Check Process Parent (PPID)
Before attempting Zygisk injection, run:
adb shell "ps -A -o pid,ppid,cmd | grep <target_process>"
If PPID=1 (init), Zygisk cannot hook it.
2. Zygisk API v4 Correct Usage
#include "zygisk.hpp"
class MyModule : public zygisk::ModuleBase {
void onLoad(Api *api, JNIEnv *env) override { ... }
void postServerSpecialize(const ServerSpecializeArgs *args) override { ... }
};
REGISTER_ZYGISK_MODULE(MyModule)
3. ReZygisk State File
Check module loading status:
adb shell "su -c 'cat /data/adb/rezygisk/state.json'"
4. Virtual Camera Service Exists
On Android 16 (Pixel 9a), virtual_camera service is available but disabled by default. It provides 0 devices until configured.
5. Ptrace dlopen Broken on Android 16
Linker namespace restrictions prevent ptrace-based dlopen for anything other than system libraries.
Next Session Action Plan
-
Research
virtual_cameraconfiguration:- Search AOSP source for
virtual_cameraimplementation - Find how to add virtual camera streams
- Check
IVirtualCameraServicebinder interface
- Search AOSP source for
-
Test Option A first (most promising):
- Enable virtual camera with custom stream
- Verify it appears as a camera device
- Point it to video/RTSP source
-
If Option A fails, try Option B (binder hook)
-
Document findings in this file for future reference
Quick Reference Commands
# Check camera processes
adb shell "ps -Z | grep -E 'cameraserver|camera.provider|virtual_camera'"
# Check if Zygisk module loaded in process
adb shell "su -c 'cat /proc/\$(pidof zygote64)/maps | grep camera-hook'"
# Check virtual camera status
adb shell "dumpsys android.hardware.camera.provider.ICameraProvider/virtual/0"
# Check binder services
adb shell "service list | grep -i camera"
# View ReZygisk state
adb shell "su -c 'cat /data/adb/rezygisk/state.json'"
# Check module.prop description (updated by service.sh)
adb shell "su -c 'cat /data/adb/modules/camera-hook-zygisk/module.prop'"
Last Updated: 2026-05-08 by Atlas (OhMyOpenCode)
Session ID: Compressed conversation (b3, b4, b5, b6, b7, b12, b13, b14, b15, b16, b17)