1.问题现象

​ 在logcat日志和dropbox日志中持续打印 E/ActivityManager( 803): Sending non-protected broadcast android.intent.action.VIEW from system 3235:com.iflytek.cutefly.speechclient.hmi/1000 pkg com.iflytek.cutefly.speechclient.hmi

2.问题定位

查看logcat,持续输出日志如下:

1
2
3
4
5
6
7
8
9
10
11
E/ActivityManager( 803): Sending non-protected broadcast android.intent.action.VIEW from system 3235:com.iflytek.cutefly.speechclient.hmi/1000 pkg com.iflytek.cutefly.speechclient.hmi
E/ActivityManager( 803): java.lang.Throwable
E/ActivityManager( 803): at com.android.server.am.ActivityManagerService. checkBroadcastFromSystem(ActivityManagerService.java:21241)
E/ActivityManager( 803): at com.android.server.am.ActivityManagerService. broadcastIntentLocked(ActivityManagerService.java:21845)
E/ActivityManager( 803): at com.android.server.am.ActivityManagerService. broadcastIntent(ActivityManagerService.java:21987)
E/ActivityManager( 803): at android.app.IActivityManager$Stub. onTransact$broadcastIntent$(IActivityManager.java:10171)
E/ActivityManager( 803): at android.app.IActivityManager$Stub. onTransact(IActivityManager.java:167)
E/ActivityManager( 803): at com.android.server.am.ActivityManagerService. onTransact(ActivityManagerService.java:3291)
E/ActivityManager( 803): at android.os.Binder.execTransact(Binder.java:731)
// ActivityManager打印com.iflytek.cutefly.speechclient.hmi发送了未受保护的广播
I/am_wtf ( 803): [0,803,system_server,-1,ActivityManager,Sending non-protected broadcast android.intent.action.VIEW from system 3235:com.iflytek.cutefly.speechclient.hmi/1000 pkg com.iflytek.cutefly.speechclient.hmi]

撸出源码,grep报错日志,定位到ActivityManagerService中

1
2
3
4
5
// 如果是系统应用,则判断声明发送的广播都声明成protect-broadcast
if (isCallerSystem) {
checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
isProtectedBroadcast, registeredReceivers);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
private void checkBroadcastFromSystem(Intent intent, ProcessRecord callerApp,
String callerPackage, int callingUid, boolean isProtectedBroadcast, List receivers) {
if ((intent.getFlags() & Intent.FLAG_RECEIVER_FROM_SHELL) != 0) {
// Don't yell about broadcasts sent via shell
return;
}

final String action = intent.getAction();
// 如果是受保护的广播或者下面枚举的广播,直接返回,不处理
if (isProtectedBroadcast
|| Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
|| Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(action)
|| Intent.ACTION_MEDIA_BUTTON.equals(action)
|| Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action)
|| Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(action)
|| Intent.ACTION_MASTER_CLEAR.equals(action)
|| Intent.ACTION_FACTORY_RESET.equals(action)
|| AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action)
|| AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)
|| LocationManager.HIGH_POWER_REQUEST_CHANGE_ACTION.equals(action)
|| TelephonyIntents.ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE.equals(action)
|| SuggestionSpan.ACTION_SUGGESTION_PICKED.equals(action)
|| AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION.equals(action)
|| AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION.equals(action)) {
// Broadcast is either protected, or it's a public action that
// we've relaxed, so it's fine for system internals to send.
return;
}

// This broadcast may be a problem... but there are often system components that
// want to send an internal broadcast to themselves, which is annoying to have to
// explicitly list each action as a protected broadcast, so we will check for that
// one safe case and allow it: an explicit broadcast, only being received by something
// that has protected itself.
if (intent.getPackage() != null || intent.getComponent() != null) {
if (receivers == null || receivers.size() == 0) {
// Intent is explicit and there's no receivers.
// This happens, e.g. , when a system component sends a broadcast to
// its own runtime receiver, and there's no manifest receivers for it,
// because this method is called twice for each broadcast,
// for runtime receivers and manifest receivers and the later check would find
// no receivers.
return;
}
boolean allProtected = true;
for (int i = receivers.size()-1; i >= 0; i--) {
Object target = receivers.get(i);
if (target instanceof ResolveInfo) {
ResolveInfo ri = (ResolveInfo)target;
// 如果activity的exported是true,说明该activity有action过滤器,直接判断广播是否开启了保护模式
if (ri.activityInfo.exported && ri.activityInfo.permission == null) {
allProtected = false;
break;
}
} else {
BroadcastFilter bf = (BroadcastFilter)target;
if (bf.requiredPermission == null) {
allProtected = false;
break;
}
}
}
if (allProtected) {
// All safe!
return;
}
}

// The vast majority of broadcasts sent from system internals
// should be protected to avoid security holes, so yell loudly
// to ensure we examine these cases.
// 为了避免安全漏洞,系统应用的广播需要加受保护权限,打印wtf日志!!!
if (callerApp != null) {
Log.wtf(TAG, "Sending non-protected broadcast " + action
+ " from system " + callerApp.toShortString() + " pkg " + callerPackage,
new Throwable());
} else {
Log.wtf(TAG, "Sending non-protected broadcast " + action
+ " from system uid " + UserHandle.formatUid(callingUid)
+ " pkg " + callerPackage,
new Throwable());
}
}

1
2
// 判断系统应用是不是受保护的。这个状态是从PKMS启动扫描各个应用apk的时候存储的
isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean isProtectedBroadcast(String actionName) {
// allow instant applications
synchronized (mProtectedBroadcasts) {
if (mProtectedBroadcasts.contains(actionName)) {
return true;
} else if (actionName != null) {
// TODO: remove these terrible hacks
if (actionName.startsWith("android.net.netmon.lingerExpired")
|| actionName.startsWith("com.android.server.sip.SipWakeupTimer")
|| actionName.startsWith("com.android.internal.telephony.data-reconnect")
|| actionName.startsWith("android.net.netmon.launchCaptivePortalApp")) {
return true;
}
}
}
return false;
}

通过以上代码可知,实际是PackageManagerService里面维护一个名为mProtectedBroadcasts的系统广播白名单。在PackageManagerService扫描系统App时会将AndroidManifest.xml中的所有protected-broadcast加入到此ArraySet变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void commitPackageSettings(PackageParser.Package pkg,
@Nullable PackageParser.Package oldPkg, PackageSetting pkgSetting, UserHandle user,
final @ScanFlags int scanFlags, boolean chatty) {
.........
if (pkg.protectedBroadcasts != null) {
N = pkg.protectedBroadcasts.size();
synchronized (mProtectedBroadcasts) {
for (i = 0; i < N; i++) {
mProtectedBroadcasts.add(pkg.protectedBroadcasts.get(i));
}
}
}
.......
}

PackageManagerService每次启动时会扫描下面三个目录的App:
/system/app
/system/priv-app
/system/framework/framework-res.apk

所以理论上只要在自己App中的AndroidManifest.xml中将自己新增的广播声明为protected-broadcast即可,不应该会出现Sending non-protected broadcast才对。

但是PKMS中有个广播白名单机制,如果扫描的app不在priv-app目录,就会把protectedBroadcasts 标志位置为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void applyPolicy(PackageParser.Package pkg, final @ParseFlags int 	
parseFlags, final @ScanFlags int scanFlags, PackageParser.Package platformPkg) {
if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
......
// 如果不是priv-app,将pkg.protectedBroadcasts置为空
if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) {
// clear protected broadcasts
pkg.protectedBroadcasts = null;
// ignore export request for single user receivers
if (pkg.receivers != null) {
for (int i = pkg.receivers.size() - 1; i >= 0; --i) {
final PackageParser.Activity receiver = pkg.receivers.get(i);
if ((receiver.info.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
receiver.info.exported = false;
}
}
}
......
}

从以上代码可知,PackageManagerService在扫描App时,发现如果App不是来自priv-app目录下的App(当然framework-res.apk除外),就会将此App的中声明的protected广播清空,这样这个广播就不会加入到白名单,所以才就一直打印上面的warning log

不过这个仅仅是warning的log而已,广播还是可以正常发送和接收的,该结果和金(磊)老板确认过,只是输出日志,不影响收发,但是对于在做系统稳定性的人来说,眼里容不下这种刺。

3.解决办法

3.1 修改原生代码

将自定义的广播在frameworks/base/core/res/AndroidManifest.xml中声明为protected-broadcast,例如

1
2
3
4
5
<!-- Added in O -->
<protected-broadcast android:name="android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED" />
<protected-broadcast android:name="com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION" />
<protected-broadcast android:name="android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED" />

这种方案是不可取的。为了减少耦合,修改问题的原则尽量采用不修改原生的方式完成。

3.2 修改应用的包位置

如果不希望改Android的源码,可以将自己的App放置到/system/priv-app下,而不是默认的/system/app目录下:
可在自己app的Android.mk中指定路径:
LOCAL_MODULE_PATH := $(TARGET_OUT_APPS_PRIVILEGED)

但是特别注意的是:如果App放置到priv-app目录,是会一些权限限制的。

4.问题回溯与反思

针对Android P持续打印non-protected broadcast 问题,google很多地方考虑到安全漏洞:使用了平台签名的系统应用广播应该收到保护。虽然没有采取严厉的措施,只是打印wtf日志,但是我们做系统稳定性也要思考是否所有预装应用都赋予平台权限。赋予了平台权限的应用可以和SystemServer跑在一个进程中,如果应用crash了,极有可能导致系统崩溃。因此,系统稳定性下一步应该考虑的哪些应用可以赋予平台权限,哪些应用不必赋予平台权限。

例如HMI层的apk,几乎不会调用到系统隐藏api等,是否可以取消平台权限,加强系统稳定性。