一个应用程序创建的线程如何被视为与该应用程序的ContentProvider不同的应用程序?

13

我有一个应用程序,当通过ContentObserver通知ContentProvider更改时,尝试在后台线程上查询提供程序。这将导致抛出SecurityException

8-10 15:54:29.577    3057-3200/com.xxxx.mobile.android.xxx W/Binder﹕ Caught a RuntimeException from the binder stub implementation.
  java.lang.SecurityException: Permission Denial: reading com.xxx.mobile.android.mdk.model.customer.ContentProvider uri content://com.xxx.mobile.android.consumer.xxx/vehicle from pid=0, uid=1000 requires the provider be exported, or grantUriPermission()
at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:539)
           at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:452)
           at android.content.ContentProvider$Transport.query(ContentProvider.java:205)
           at android.content.ContentResolver.query(ContentResolver.java:478)
           at android.content.ContentResolver.query(ContentResolver.java:422)

那么一个应用程序创建的线程怎么会和应用程序的ContentProvider具有不同的UID呢?

通过在android.content.ContentProvider中放置一个异常断点,我发现UserHandle.isSameApp(uid, mMyUid)false,而UserHandle.isSameUser(uid, mMyUid)true。 我还看到提供程序的UID为10087。


你是在询问uid=1000吗?那是Android系统用户ID。很可能该请求正在内部代理到系统进行处理。 - adelphus
@adelphus 是的。我原以为这是安全异常的原因,但现在我不确定了,因为 UserHandle.isSameUser 返回 true - Julian A.
Android用户与应用程序UID值无关。不要混淆它们!应用程序UID值用于在应用程序之间强制实施沙盒隔离,而用户安全性则采用不同的实现方式。 - adelphus
啊,好的。所以问题是为什么UserHandle.isSameApp返回false,即使线程是由应用程序创建的。 - Julian A.
当你说“后台线程”时,你是指Thread还是AsyncTask,或者其他什么? - Henry
3个回答

5

uid值为1000属于Android系统。Android的许多功能都涉及将请求代理到系统线程进行处理。如果在此期间抛出异常,错误将包括系统的uid,而不是原始请求者的uid。

至于其他几点:

UserHandle.isSameApp(uid, mMyUid)是false

UserHandle.isSameUser(uid, mMyUid)是true

这些最容易通过查看源代码来解释。在支持多用户的Android设备上,每个用户由一系列UID定义。因为id的模数不匹配,所以isSameApp是false:

 public static final boolean isSameApp(int uid1, int uid2) {
        return getAppId(uid1) == getAppId(uid2);
}

 public static final int getAppId(int uid) {
        return uid % PER_USER_RANGE;
}

同样地,这两个ID属于同一用户,因为它们位于相同的范围内:
 public static final boolean isSameUser(int uid1, int uid2) {
        return getUserId(uid1) == getUserId(uid2);
 }

public static final int getUserId(int uid) {
        if (MU_ENABLED) {
            return uid / PER_USER_RANGE;
        } else {
            return 0;
        }
}

请注意,这种逻辑是有缺陷的,因为这意味着所有 Android 系统 uid(<10000)都被假定“属于”第一个用户。
另外请注意,如果第二个用户安装了超过 1000 个应用程序(!),那么就可能会将某个应用程序错误地识别为系统应用程序(因为 uid % PER_USER_RANGE 都将返回 1000)。但这并不会真正产生太严重的后果,因为强大的沙箱机制可以防止任何严重的问题发生。

这有助于我理解ID的工作原理。谢谢。您有什么建议,可以避免安全异常,而不需要将内容提供程序开放给其他应用程序访问? - Julian A.
保护内容提供者的正常方式是定义和包含自定义权限像这样。但我不完全确定您想要实现什么,所以也许这不是您需要的。 - adelphus
那个链接似乎很有用,可以将一个应用程序连接到另一个内容提供者。但在我的情况下,我只有一个应用程序。而这个应用程序的内容提供者似乎认为由应用程序生成的线程来自另一个应用程序,因此会抛出安全异常。 - Julian A.
@Julian 抱歉 - 链接的重点是展示,即使在声明权限的清单中,您也始终需要使用该权限,否则无法访问自己的CP。但这里可能还有其他问题。 - adelphus
不用担心。谢谢你的尝试 :) - Julian A.

3
当我试图在系统回调(LeScanCallback)中与我的ContentProvider交互时,我遇到了同样的问题。问题在于回调线程是由Android系统拥有的,而不是由我的应用拥有,即使代码在我的应用中也是如此。
在尝试与我的ContentProvider交互之前,将工作从回调传递到我的应用程序线程可以成功解决这个问题。
为了减少线程创建和回收的样板文件(需要频繁的回调来减少开销),我在我的委托方法上使用了AndroidAnnotation的@Background注释(但今天会使用Kotlin协程)。

2
如果应用程序的任何组件启动了一个线程,并且该组件具有提供者,则可以访问ContentProvider而不会出现SecurityException
我在我的应用程序中使用ContentProvider作为额外的抽象层,没有将内容公开给其他应用程序。我在后台线程(不是AsyncTask,而是简单的java.lang.Thread)中访问ContentProvider,但我没有得到任何SecurityException。以下是来自我的应用程序的代码。 AndroidManifest.xml
 <provider
    android:authorities="com.sample.provider"
    android:name="com.sample.MyProvider"
    android:exported="false" />

MainActivity

public void performContinue(Bundle extras){
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            String AUTHORITY = "com.sample.provider";
            Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
            Uri currentUri = BASE_URI.buildUpon().appendPath("SAMPLE_COUNT").build();
            final Cursor query = InputActivity.this.getContentResolver().query(currentUri, null, null, null, null);
            if (query != null) {
                final int count = query.getCount();
                Log.d("DEBUG","CONTENT = " + count);
            }else{
                Log.d("DEBUG","CONTENT = CURSOR NULL");
            }
        }
    });
    thread.setName("THREAD_1");
    thread.start();
}


我似乎没有遇到任何SecurityException。理想情况下,我们应该使用AsyncQueryHandler来访问ContentProvider,因为它允许你在后台线程中完成所有获取过程,并将使用UI线程在UI中发布结果。但是看完这篇文章后,我只是想看看是否可以只使用Thread,并检查是否仍然可以在没有任何异常的情况下访问它。它运行得很好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接