ohmu
·
2026-10-15
1#include "wallpaper.h"
2#include "common/android/binder.h"
3#include "common/android/dex.h"
4#include "common/android/package.h"
5#include "common/debug.h"
6#include "common/io.h"
7#include "detection/displayserver/displayserver.h"
8
9#include <errno.h>
10#include <string.h>
11#include <unistd.h>
12
13// Android keeps the wallpaper as a file per user, in a directory no app can enter:
14//
15// /data/system/users/<userId>/wallpaper the image the launcher draws
16// /data/system/users/<userId>/wallpaper_orig the picture as it was uploaded, before cropping
17//
18// /data/system/users/<userId> is mode 0700 and owned by system:system, so `ls`, `stat` and `open` all
19// answer EACCES to an app UID: the path is not readable even though it is not secret. What is
20// reachable is a descriptor to the file, because android.app.IWallpaperManager opens it inside
21// system_server and passes one back, and a descriptor is an ordinary file from then on. readlink(2)
22// on it names the file, which is the string this module is asked for -- the image itself is never
23// read or decoded.
24//
25// The gate is the calling package. `getWallpaper(String callingPackage, int which, int userId)`
26// answers only when that name is the caller's own: the two-argument overload, which is the null name,
27// comes back as a SecurityException raised by StorageManager.checkPermissionReadImages(), and an
28// empty string, an unknown package and a real package belonging to a different uid all leave with no
29// descriptor. That READ_MEDIA_IMAGES denial is not something a Termux-style app can lift -- the
30// permission is not in its manifest and it targets SDK 28 -- so naming the caller is the only route.
31//
32// Measured on an Android 16 device as an app UID: the descriptor for `which` 1 is
33// /data/system/users/0/wallpaper, a 2833949-byte JPEG whose SOF0 reads 2159x3168, while `which` 2
34// answers with no descriptor at all because that lock screen is a live wallpaper.
35//
36// Only one of the two display servers is ever asked. The binder route is Android's, and it answers
37// while SurfaceFlinger is the display server; a Termux:X11 or Wayland session puts a real compositor
38// on top, and there the wallpaper belongs to that desktop -- so that case goes to wallpaper_linux.c,
39// which is compiled alongside this file for exactly this reason.
40
41#define FF_WALLPAPER_ANDROID_SERVICE "wallpaper"
42#define FF_WALLPAPER_ANDROID_DESCRIPTOR "android.app.IWallpaperManager"
43
44// WallpaperManager.FLAG_SYSTEM and FLAG_LOCK.
45typedef enum FFWallpaperAndroidWhich : int32_t {
46 FF_WALLPAPER_ANDROID_WHICH_SYSTEM = 1,
47 FF_WALLPAPER_ANDROID_WHICH_LOCK = 2,
48} FFWallpaperAndroidWhich;
49
50// The jar and the stub class the transaction code is read out of. framework.jar answered on every
51// device measured (Android 11, 13 and 16), so that is the one path and there is no fallback; the
52// stub sits in classes.dex there, which is the entry the lookup tries first.
53#define FF_WALLPAPER_ANDROID_JAR "/system/framework/framework.jar"
54#define FF_WALLPAPER_ANDROID_STUB "Landroid/app/IWallpaperManager$Stub;"
55
56// The method this module calls, by the name of the constant the dex carries for it. `getWallpaper`
57// is the fourth method of IWallpaperManager$Stub, but the number is resolved from the device's own
58// jar rather than written down here: the numbering is a property of the `.aidl` the ROM was built
59// from, and a vendor that inserts a method ahead of this one shifts it, after which a transcribed
60// code reaches a different method or none. Spelling the name out is the whole point -- the lookup
61// turns it into whatever this build uses, and a build that does not declare it says so instead of
62// reaching another method. See common/android/dex.h.
63#define FF_WALLPAPER_ANDROID_GET_WALLPAPER "TRANSACTION_getWallpaper"
64
65// `ffAndroidGetOwnPackage` writes at most this many bytes, terminator included.
66#define FF_WALLPAPER_ANDROID_PACKAGE_SIZE 128
67
68// The request is the interface token, the package name, `which` and `userId`. A string16 costs
69// 4 + 2 * (length + 1) bytes padded to four, and the token puts three int32 in front of the
70// descriptor. Deriving the size from the two names rather than rounding it up is what keeps the
71// longest package name `ffAndroidGetOwnPackage` can produce from marking the parcel truncated --
72// that fails the whole detection, rather than degrading to a call without a name.
73#define FF_WALLPAPER_ANDROID_PARCEL_SIZE \
74 (12 + ((4 + 2 * sizeof(FF_WALLPAPER_ANDROID_DESCRIPTOR) + 3) & ~3u) \
75 + ((4 + 2 * FF_WALLPAPER_ANDROID_PACKAGE_SIZE + 3) & ~3u) + 8)
76
77// The reply carries the descriptor object plus, for this method, a Bundle of two ints: 108 bytes on
78// the device it was measured on, 80 when the wallpaper is a live one and the Bundle comes alone. Only
79// the descriptor table is read out of it, so this is sized for headroom rather than for a field --
80// being wrong here would fail the detection, not misread it.
81#define FF_WALLPAPER_ANDROID_REPLY_SIZE 512
82
83static const char* getWallpaperFile(FFBinder* binder, uint32_t handle, const char* package, FFWallpaperAndroidWhich which, int32_t transaction, FFstrbuf* result) {
84 uint8_t parcelBuffer[FF_WALLPAPER_ANDROID_PARCEL_SIZE];
85 FFBinderParcel parcel = ffBinderParcelCreate(parcelBuffer, sizeof(parcelBuffer));
86 ffBinderParcelPutInterfaceToken(&parcel, FF_WALLPAPER_ANDROID_DESCRIPTOR);
87 ffBinderParcelPutString16(&parcel, package);
88 ffBinderParcelPutI32(&parcel, (int32_t) which);
89 ffBinderParcelPutI32(&parcel, 0); // userId, which is the one user a phone has
90
91 uint8_t replyBuffer[FF_WALLPAPER_ANDROID_REPLY_SIZE];
92 FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
93
94 // TF_ACCEPT_FDS is neither optional nor a hint: a reply carrying a descriptor is only delivered
95 // when the request asked for one, and a request that did not is answered with BR_FAILED_REPLY --
96 // which reads like a service that is not running rather than like a missing flag.
97 const char* error = ffBinderTransact(binder, handle, (uint32_t) transaction, TF_ACCEPT_FDS, &parcel, &reply);
98
99 // Whatever else happened, a descriptor that did arrive is ours: the kernel installed it in our fd
100 // table, and on the paths below that return early nothing else would close it.
101 FF_AUTO_CLOSE_FD FFNativeFD fd = reply.fdCount > 0 ? reply.fds[0] : -1;
102
103 if (error != nullptr) {
104 FF_DEBUG("getWallpaper(which %d) could not be transacted: %s", (int) which, error);
105 return error;
106 }
107 if (ffBinderReplyIsStatus(&reply)) {
108 FF_DEBUG("getWallpaper(which %d) came back as a status reply of %d instead of a parcel",
109 (int) which, (int) ffBinderReadI32(reply.data, reply.size, 0));
110 return "Wallpaper service rejected the request";
111 }
112
113 const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
114 if (exception != 0) {
115 // -1 is a SecurityException, and for this method it is what the package name check raises.
116 FF_DEBUG("getWallpaper(which %d) raised exception %d over %zu bytes of reply", (int) which, exception, reply.size);
117 return "Wallpaper service refused the request";
118 }
119 if (!ffIsValidNativeFD(fd)) {
120 FF_DEBUG("getWallpaper(which %d) carried no descriptor, which is how a live wallpaper answers: "
121 "%zu bytes of reply",
122 (int) which, reply.size);
123 return "The wallpaper is not a file";
124 }
125
126 char path[64];
127 const int pathLength = snprintf(path, sizeof(path), "/proc/self/fd/%d", (int) fd);
128 if (pathLength <= 0 || (size_t) pathLength >= sizeof(path)) {
129 FF_DEBUG("Failed to build the /proc path of the wallpaper descriptor");
130 return "Failed to build the /proc path of the wallpaper descriptor";
131 }
132
133 char target[512];
134 const ssize_t length = readlink(path, target, sizeof(target) - 1);
135 if (length <= 0) {
136 FF_DEBUG("readlink(%s) failed: %s", path, strerror(errno));
137 return "Failed to resolve the wallpaper path";
138 }
139 target[length] = '\0';
140
141 ffStrbufSetNS(result, (uint32_t) length, target);
142 FF_DEBUG("The wallpaper for which %d is \"%s\"", (int) which, result->chars);
143 return nullptr;
144}
145
146const char* ffDetectWallpaper(FFstrbuf* result) {
147 // The binder route below reads Android's own wallpaper, which is the one behind the launcher
148 // only while SurfaceFlinger is the display server. A Termux:X11 or Wayland session runs a real
149 // compositor on top of the device, and the wallpaper visible there belongs to that desktop, not
150 // to Android -- so that case is handed to the Linux implementation, which knows GTK and Qt
151 // settings. Wallpaper is only reported for one of the two, never merged.
152 const FFDisplayServerResult* wm = ffConnectDisplayServer();
153 if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) {
154 FF_DEBUG("The display server is \"%s\", so the Linux implementation answers", wm->wmProtocolName.chars);
155 const char* ffDetectWallpaperLinux(FFstrbuf* result);
156 return ffDetectWallpaperLinux(result);
157 }
158
159 char package[FF_WALLPAPER_ANDROID_PACKAGE_SIZE];
160 if (!ffAndroidGetOwnPackage(package, sizeof(package))) {
161 FF_DEBUG("Cannot determine the package name of this process");
162 return "Cannot determine the package name of this process";
163 }
164
165 // Resolved before the binder is opened, so that a jar which does not declare the method says so
166 // rather than the module failing later with a message about the transport. The reader reports a
167 // code it cannot find by leaving the sentinel in the result and says nothing more -- what a
168 // missing code means for this module is decided here, not there.
169 int32_t transaction = FF_DEX_STATIC_INT_UNRESOLVED;
170 const FFDexStaticIntRequest request = { FF_WALLPAPER_ANDROID_STUB, FF_WALLPAPER_ANDROID_GET_WALLPAPER, &transaction };
171 const char* error = ffDexStaticInts(FF_WALLPAPER_ANDROID_JAR, &request, 1);
172 if (error != nullptr) {
173 FF_DEBUG("Reading \"%s\" from \"%s\" failed: %s", FF_WALLPAPER_ANDROID_GET_WALLPAPER, FF_WALLPAPER_ANDROID_JAR, error);
174 return error;
175 }
176 if (transaction == FF_DEX_STATIC_INT_UNRESOLVED) {
177 FF_DEBUG("The wallpaper service does not declare getWallpaper");
178 return "The wallpaper service does not declare getWallpaper";
179 }
180
181 [[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
182 error = ffBinderOpen(&binder);
183 if (error != nullptr) {
184 return error;
185 }
186
187 // Released on every path out of this function, including the ones below that return early.
188 [[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
189 error = ffBinderLookupService(&binder, FF_WALLPAPER_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
190 if (error != nullptr) {
191 FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_WALLPAPER_ANDROID_SERVICE, error);
192 return error;
193 }
194 FF_DEBUG("The \"%s\" service is handle %u, getWallpaper is transaction %d", FF_WALLPAPER_ANDROID_SERVICE, service.handle, transaction);
195
196 // The system wallpaper is what the module means by "the current wallpaper"; the lock screen is a
197 // second, separate picture, and is only reported when the system one is not a file -- which is
198 // the live-wallpaper case, where a component draws it and there is no image behind it.
199 error = getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_SYSTEM, transaction, result);
200 if (error == nullptr) {
201 return nullptr;
202 }
203
204 FF_DEBUG("The system wallpaper is not a file (%s), so the lock screen is asked for instead", error);
205 return getWallpaperFile(&binder, service.handle, package, FF_WALLPAPER_ANDROID_WHICH_LOCK, transaction, result);
206}