Binder(七):getCallingUid() 读到的是谁?clearCallingIdentity() 又清掉了什么?
java
@Override
public void updateState(State state) {
int uid = Binder.getCallingUid();
}
这段代码可以同时满足三个事实:进程是 system_server,执行者是 system_server 的 Binder 线程,uid 却是某个 App。若把"当前进程是谁""当前线程在哪""这笔事务是谁发来的"压成同一种身份,这个结果看起来就是矛盾的。
进一步,常见代码还会主动执行:
csharp
long token = Binder.clearCallingIdentity();
try {
// 这里发生了什么?
} finally {
Binder.restoreCallingIdentity(token);
}
它清掉的是 Linux 进程 UID、Java 线程身份,还是当前 Binder 事务上下文?如果随后把工作 post 到 Handler,原 App 身份还会不会跟过去?A → B → C 多跳调用时,C 看到的又是谁?
本文用一条线程上的 identity 状态变化回答四个连续问题:
scss
App UID 是怎样随入站事务落到服务端 Binder 线程的?
为什么一旦离开 Binder 入口,getCallingUid() 会读到服务端自身?
clear / restore 修改的究竟是哪一层上下文?
多跳调用与异步归因为什么不能只靠 calling UID?
方向性的提示只有一句:calling identity 是当前 Binder 事务上下文,不是"代码所在进程"的别名。后面的源码与实验要把这句话的线程边界和安全后果逐项证明出来。
calling identity 的源码位置沿用导读"系列统一基线"。
代码在 system_server,calling UID 为什么还能是 App?
同一行代码里究竟并存哪三种"身份"?
| 概念 | 示例 | 从哪里来 |
|---|---|---|
| 服务端进程 UID | system_server 的 UID |
Linux 进程凭据 |
| 服务端执行线程 | TID 1236,线程名可能显示为 binder:1234_2 |
服务端 Binder 线程池;TID 是身份与拓扑证据,线程名只是实现标签 |
| 当前 Binder calling UID | 发起事务的 App UID | 驱动随入站事务提供,libbinder 保存在线程上下文 |
因此下面三句话可以同时成立:
objectivec
当前代码运行在 system_server 进程;
当前线程是 system_server 的一条 Binder 线程;
Binder.getCallingUid() 返回发起当前事务的 App UID。
最危险的错误,是把"代码在哪个进程执行"和"当前 Binder 调用者是谁"当成同一个问题。
还要提前固定一个多跳边界:Binder calling UID 表示当前这笔事务的直接发送方,不是跨多跳自动透传的原始业务发起者。 因此 A → B → C 时,C 通常看到直接调用它的 B;后面的"每一跳看到的是直接发送方"会把这条链展开。
一条 Binder 线程上的 identity 会怎样随事务变化?
下面的图是普通顶层 App → system_server 调用示例。嵌套或重入事务进入同一线程时,libbinder 保存的原上下文可能是外层 calling context,而不一定是"没有调用者 / 服务自身身份";事务退出后恢复的也是进入这一层之前的上下文。

读图时只跟踪一条服务端 Binder 线程上的状态变化:
objectivec
事务到达前:线程没有入站调用者上下文;
处理事务时:callingUid = App UID;
clear 后:callingUid 暂时变成服务端自身 UID;
restore 后:恢复 App UID;
事务退出后:libbinder 恢复线程原来的上下文。
App UID 是怎样落到当前 Binder 线程的?
谁保证 calling UID 不是客户端伪造的业务参数?
Binder 事务不是让业务客户端在 Parcel 里随便写一个:
ini
callerUid = 10086
然后服务端照单全收。
驱动在发送事务时记录发送进程凭据,并在目标线程读取事务时填写接收侧 binder_transaction_data。固定内核基线的 binder_thread_read() 会设置:
ini
trd->code = t->code;
trd->flags = t->flags;
trd->sender_euid = from_kuid(
current_user_ns(),
t->sender_euid);
同步事务存在明确发送线程时,还会提供 sender PID;没有可用于该语义的发送线程时,PID 会是 0。
因此 calling UID 是 Binder 基础设施建立的事务元数据,不是 AIDL 业务参数。
libbinder 怎样把发送方身份保存成线程上下文?
服务端用户态读到 BR_TRANSACTION 后,Android 16 的 IPCThreadState::executeCommand() 先保存当前线程旧状态:
ini
const pid_t origPid = mCallingPid;
const uid_t origUid = mCallingUid;
const bool origHasExplicitIdentity = mHasExplicitIdentity;
再把入站事务身份装到当前线程的 IPCThreadState:
ini
mCallingPid = tr.sender_pid;
mCallingUid = tr.sender_euid;
mHasExplicitIdentity = false;
随后才调用本地 BBinder / JavaBBinder 和 AIDL Stub。
事务处理结束时,libbinder 恢复:
ini
mCallingPid = origPid;
mCallingUid = origUid;
mHasExplicitIdentity = origHasExplicitIdentity;
所以 calling identity 具有明确作用域:
diff
当前线程
+
当前正在处理的入站 Binder 事务
它不是进程级全局变量。
Java getCallingUid() 最终读取的是哪份 native 状态?
Java Binder.getCallingUid() 是 native 方法。JNI 的 android_os_Binder_getCallingUid() 直接返回:
css
IPCThreadState::self()->getCallingUid();
而 IPCThreadState::getCallingUid() 读取当前线程字段:
kotlin
return mCallingUid;
因此判断公式是:
scss
当前线程正在处理入站 Binder 事务:
getCallingUid() = 该事务发送方 UID
当前线程不在入站 Binder 事务上下文:
getCallingUid() = 当前进程自身 UID
Android 16 还提供内部检查语义更强的 getCallingUidOrThrow(),用于避免在错误上下文中悄悄读到自身 UID。
一旦离开原入站上下文,calling identity 为什么会改变?
为什么权限判断必须在 Binder 入口消费 calling identity?
典型入口:
java
@Override
public void updateState(State state) {
final int callingUid = Binder.getCallingUid();
enforceCallerAllowed(callingUid, state);
// 再进入后续业务
}
入口时可以读取的证据并不等价,也不是每项都无条件存在:
css
当前 transaction code;
当前 Parcel 参数;
当前 Binder calling UID;
同步事务中的 calling PID:只适合作为短时诊断线索,不能作为稳定身份;
calling SID:仅当端点明确请求并实际收到调用方安全上下文时可用。
SID 未启用、未传递或读取结果不可用时,必须按"没有这项证据"处理,不能回退成空字符串匹配、默认受信任,也不能假设每个 Binder 服务天然都能取得 SID。权限决策通常以入口处的 UID、声明权限和服务自身规则为主;确实启用安全上下文的端点,才把 SID 作为额外 MAC 证据。calling PID 即使在同步事务里读到,也会随进程退出与 PID 复用失去长期意义;oneway 场景还可能直接为 0。
如果先异步转交,再在别的线程重新调用 getCallingUid(),原事务身份已经不在那个线程上。
所以服务端权限模型通常应当:
在 Binder 入口读取并校验调用者;
只把校验后的最小身份数据显式传给异步任务;
不要依赖另一个线程重新读取 Binder calling context。
为什么 Runnable 不会自动携带 Binder calling context?
错误代码:
scss
@Override
public void updateState(State state) {
mHandler.post(() -> {
int uid = Binder.getCallingUid();
// 误以为这里仍是原 App UID
applyForCaller(uid, state);
});
}
Handler 回调运行在目标 Looper 线程,不是原服务端 Binder 线程,也不在原入站事务调用栈中。
此时:
scss
Binder.getCallingUid()
通常返回服务端进程自身 UID。
正确做法是在入口捕获:
ini
@Override
public void updateState(State state) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
enforceCallerAllowed(callingUid, state);
mHandler.post(() -> {
applyForCaller(callingUid, callingUserId, state);
});
}
这里 UID 已经变成普通显式数据,线程切换不会改变它的值;但服务仍应警惕包名与 UID 映射变化,不要只凭客户端自报的 package name 建立信任。
clearCallingIdentity() 到底重置了哪一层身份?
Android 16 的 IPCThreadState::clearCallingIdentity() 做两件事:
ini
int64_t token = packCallingIdentity(
mHasExplicitIdentity,
mCallingUid,
mCallingPid);
clearCaller();
mHasExplicitIdentity = true;
return token;
clearCaller() 把当前线程的 Binder 调用上下文设置为本进程:
ini
mCallingPid = getpid();
mCallingUid = getuid();
所以它改变的是:
scss
当前 IPCThreadState 中 getCallingPid()/getCallingUid() 所见的上下文。
它没有:
sql
修改 Linux 进程 uid;
把 App 提升为 system_server;
改变其他线程的 calling identity;
绕过 SELinux;
永久删除原调用者信息。
"不是 Linux 提权"不等于"误用没有安全影响"。clear 之后,依赖 Binder calling identity 的后续本地权限判断 会看到服务端自身身份;如果服务在验证 App 权限之前就 clear,或把未验证参数带入高权限内部操作,仍可能形成 confused deputy 或权限语义错误。正确顺序始终是:入口先基于真实调用者授权和校验,再只在明确需要的最小代码段 clear,并在同线程 finally 中 restore。
什么情况下 clear identity 是必要隔离,而不是绕过检查?
考虑 system_server 正在处理 App 请求,入口已经完成权限检查,后续要调用本进程内部接口:
bash
App UID
↓ Binder
system_server Binder 入口
↓
调用同进程 local Binder / 本地代码
同进程 local Binder 调用可能不会经过驱动重新建立"system_server 作为发送方"的身份。如果内部代码继续调用 Binder.getCallingUid(),它可能仍看到外层 App 的入站 calling UID。
这时服务端可以在完成入口授权后:
ini
long token = Binder.clearCallingIdentity();
try {
performInternalOperationAsService();
} finally {
Binder.restoreCallingIdentity(token);
}
准确语义是:
外层权限决策仍以 App 调用者为依据;
内部服务逻辑暂时不再继承外层 Binder calling context;
当前线程按服务端自身身份执行后续本地检查;
结束后恢复外层事务身份。
它不是"先 clear,所有权限检查就都通过"的万能工具。入口权限检查不应被它绕过去。
为什么 token 必须在同线程的 finally 中恢复?
clearCallingIdentity() 返回的 token 编码了原来的:
objectivec
calling UID
calling PID
是否显式身份
restoreCallingIdentity(token) 把这些值写回当前 IPCThreadState。
如果业务抛异常而没有 restore:
sql
同一条 Binder 线程继续执行外层调用代码时,
看到的 calling identity 已经被污染;
后续权限检查和审计可能使用错误身份。
因此固定模板是:
ini
final long token = Binder.clearCallingIdentity();
try {
doWork();
} finally {
Binder.restoreCallingIdentity(token);
}
不要把 restore 放在普通顺序代码尾部。
token 还绑定当前线程的 IPCThreadState 上下文,不能传给 Handler 或另一条线程恢复:
ini
long token = Binder.clearCallingIdentity();
mHandler.post(() -> {
Binder.restoreCallingIdentity(token); // 错误:跨线程恢复
});
正确原则是四件事同时成立:
arduino
同一线程;
同一调用栈;
try / finally;
成对 clear / restore。
多跳调用里,谁才算"调用者"?
为什么每一跳只能看到直接发送方?
假设:
css
App A
→ system_server B
→ 远端服务 C
B 的入站入口中:
css
Binder.getCallingUid() = A UID
但 B 再发起一笔真正的远端 Binder 事务到 C 时,驱动按发送任务的 Linux 凭据建立 C 所见的调用者:
css
C 看到的直接 Binder caller 通常是 B UID,
不是自动继承 A UID。
如果 C 需要知道原始业务发起者,B 必须通过受信任的显式协议、token 或系统定义的委托机制传递;不能假设 Binder calling UID 会自动跨多跳透传。
clearCallingIdentity() 的主要价值在于重置 B 当前线程继承的入站 calling context,尤其影响本地代码、local Binder 与之后继续读取 getCallingUid() 的逻辑。它不是远端驱动凭据的伪造接口。
calling PID 为什么比 UID 更不适合作为长期凭据?
AOSP AIDL backends:Caller information 明确给出两个限制:
objectivec
oneway 调用中的 calling PID 为 0;
PID 会在进程退出后复用,存在竞态。
因此:
objectivec
UID:通常用于权限与用户隔离判断;
PID:适合短时调试、线程栈和进程关联,不应作为长期可信身份。
错误做法:
ini
int pid = Binder.getCallingPid();
trustedClients.put(pid, true); // 长期缓存为可信客户端
原进程死亡后,同一个 PID 可能被另一个进程复用。
即使是同步调用,读取 PID 后也存在对方立刻退出的竞态;需要长期对象身份时,应使用 UID、Binder token、包管理校验或专门会话对象等更合适的机制。
WorkSource 能补充归因,为什么不能替代 calling identity?
Android Binder 还有 calling WorkSource 等归因信息。它和 calling UID 不同:
objectivec
calling UID:驱动建立的直接 Binder 调用者身份;
WorkSource:用于工作归因,可以按协议传播,但不等于权限身份。
Android Binder.getCallingWorkSourceUid() 文档也提醒:该值可由调用方设置,不能像 calling UID 一样直接作为不可信调用者的权限依据。
在 Handler 中重新读取 callingUid,会怎样绕开原调用者检查?
现象:
scss
App 调用 system_server;
Binder 入口 post 到 Handler;
Handler 中 getCallingUid() 返回 SYSTEM_UID;
代码误以为请求来自系统进程,从而跳过用户隔离或权限检查。
根因不是:
objectivec
Binder 丢了 UID。
而是:
sql
calling identity 本来就属于原 Binder 线程的入站事务上下文;
Handler 回调已经换线程、换执行上下文。
修复原则:
入口读取;
入口校验;
显式传递最小身份数据;
Handler 中不重新猜测原调用者。
四点实验怎样证伪"identity 跟着任务走"?
在服务端打印:
arduino
void logIdentity(String point) {
Log.i(TAG, point
+ " processUid=" + Process.myUid()
+ " pid=" + Process.myPid()
+ " tid=" + Process.myTid()
+ " callingUid=" + Binder.getCallingUid()
+ " callingPid=" + Binder.getCallingPid());
}
调用顺序:
scss
logIdentity("binder-entry");
final int capturedUid = Binder.getCallingUid();
mHandler.post(() -> {
logIdentity("handler");
Log.i(TAG, "capturedUid=" + capturedUid);
});
long token = Binder.clearCallingIdentity();
try {
logIdentity("after-clear");
} finally {
Binder.restoreCallingIdentity(token);
}
logIdentity("after-restore");
BinderLab 的 App 与 :remote 属于同一 APK,默认共享 Linux UID,因此现有证据不适合验证这组身份变化。实验应使用两个不同 UID 的客户端/服务端 APK,或经过权限与生命周期审计的 isolated-process 方案,并同时记录 Process.myUid()、Binder.getCallingUid()、PID、TID 和 requestId。否则入口 calling UID 与 Handler 上的进程 UID 可能数值相同,产生假阴性。
跨进程同步调用的判定关系应是:
ini
binder-entry.callingUid = 客户端 UID
handler.callingUid = 服务端进程 UID
handler.capturedUid = 客户端 UID
after-clear.callingUid = 服务端进程 UID
after-restore.callingUid = 客户端 UID
现有证据包没有执行这组四点对照,上述关系是可复现的实验判定条件,不是实测数值。即使关系成立,它也只能说明 calling context 依附 Binder 入口线程并在转线程后消失,不能把 calling UID 当成多跳链路的原始发起者,也不能替代权限与 SELinux 判断。
回到开头:三种身份怎样同时成立而不矛盾?
scss
1. 驱动记录发送方凭据,并随 BR_TRANSACTION 交给服务端;
2. 服务端 IPCThreadState 把 sender PID/UID 保存为当前线程的 calling context;
3. Binder.getCallingUid() 读取当前线程的 mCallingUid;
4. 权限检查应在 Binder 入口、身份仍然有效时完成;
5. post 到 Handler 后,原 calling context 不会自动跟随,必须显式捕获并传递;
6. clearCallingIdentity() 保存旧上下文,并把当前线程的 Binder calling PID/UID 重置为服务端自身;
7. 它不修改 Linux 进程 UID,也不是 Linux credential 提权,但会改变后续本地 Binder identity 权限判断所见身份,误用仍可能造成 confused deputy;
8. restoreCallingIdentity() 必须在 finally 中恢复原事务上下文;
9. calling PID 在 oneway 中为 0,且存在复用竞态,不适合作为长期身份凭据。
身份上下文只在一笔入站事务和当前执行线程上成立;它既不会随 Handler 自动传播,也不能证明远端端点持续存活。客户端仍持有 BinderProxy 时,远端进程完全可能已经退出。第八篇处理这条引用链的另一端:驱动怎样把 node 死亡变成 DeathRecipient.binderDied()。