Binder(七):getCallingUid() 读到的是谁?clearCallingIdentity() 又清掉了什么?

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()

源码与官方文档

相关推荐
2501_915909067 小时前
iOS 应用反调试技详解术 检测调试器的原理与防护实践
android·ios·小程序·https·uni-app·iphone·webview
Lvan的前端笔记7 小时前
AndroidX 完全入门指南
android·androidx
帅次8 小时前
Android 应用高级面试:Window 近1年高频追问 18 题
android·面试·职场和发展
总捣什么乱18 小时前
MySQL MySQL是怎么保证主备一致的?
android·mysql·adb
zzm6289 小时前
精读 HinDroid:基于异构信息网络的安卓恶意软件检测
android
weixin_440784119 小时前
Android基础知识汇总
android·java·android studio
sbjdhjd10 小时前
安全初级 | Upload 文件上传漏洞实操
android·经验分享·安全·网络安全·开源·php·apache
small-pudding10 小时前
Cocos Creator Android 热更新方案实践:逻辑更新、远程 Bundle 与线上回滚
android
00后程序员张10 小时前
iOS加固技术路线全面解析:Bitcode模式、源码模式与汇编模式对比及爱加密优势
android·汇编·ios·小程序·uni-app·cocoa·iphone