diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c index de72e54031..3e83d01636 100644 --- a/bpf/progs/netd.c +++ b/bpf/progs/netd.c @@ -147,6 +147,11 @@ DEFINE_BPF_MAP_NO_NETD_API(local_net_note_op_enabled_map, ARRAY, uint32_t, bool, // used. DEFINE_BPF_MAP_NO_NETD_API(local_net_cache_generation_id_map, ARRAY, uint32_t, uint64_t, 1, 25Q2) +// A ring buffer on which blocked SO_BINDTODEVICE events are pushed. +DEFINE_BPF_RINGBUF_EXT(sk_bind_to_device_event_ringbuf, SkBindToDeviceEvent, 8 * 512, + AID_ROOT, AID_SYSTEM, 0060, "net_shared", DEFAULT_BPF_PIN_SUBDIR, + 26Q2, MAXAPI); + // A ring buffer on which loopback access events are pushed. DEFINE_BPF_RINGBUF_EXT(loopback_access_ringbuf, LoopbackAccessEvent, 16 * 512, AID_ROOT, AID_SYSTEM, 0060, "net_shared", DEFAULT_BPF_PIN_SUBDIR, @@ -1506,57 +1511,54 @@ function int inet_setsockopt(struct bpf_sockopt *ctx, UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid); uint32_t uidRule = uidEntry ? uidEntry->rule : 0; + if (ctx->level == SOL_SOCKET + && ctx->optname == SO_BINDTODEVICE + && !is_system_uid(uid) + && !(uidRule & APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH)) { + SkBindToDeviceEvent *event = bpf_sk_bind_to_device_event_ringbuf_reserve(); + if (event != NULL) { + event->uid = uid; + // Refer to is_netd() for explanation of shift. + event->pid = bpf_get_current_pid_tgid() >> 32; + bpf_sk_bind_to_device_event_ringbuf_submit(event); + } + return SETSOCKOPT_EPERM; + } + if (!(uidRule & LOCKDOWN_VPN_MATCH)) { return SETSOCKOPT_ALLOWED; } - { - // Prevent SO_BINDTODEVICE from being triggered by a UID that is under a lockdown VPN as - // this can leak unicast traffic. Can only do this for regular apps as some core system and - // system apps rely on this being allowed. - // TODO: Review IP_UNICAST_IF and IP_PKTINFO. - // TODO: Review PermissionMonitor#hasRestrictedNetworkPermission to see if this covers all - // of the system uids that need to SO_BINDTODEVICE. These uids do not have - // LOCKDOWN_VPN_MATCH. - if ((uidRule & LOCKDOWN_VPN_REGULAR_APP_MATCH) - && ctx->level == SOL_SOCKET - && ctx->optname == SO_BINDTODEVICE) { - return SETSOCKOPT_EPERM; - } + // Prevent kernel-generated multicast traffic (IGMP, MLD) from being triggered by a + // UID that is under a lockdown VPN. A known leak that still exists is when a UID joins a multicast + // group prior to being under a lockdown VPN and then becomes under a lockdown VPN. In this case the + // IGMP/MLD will be generated when the kernel destroys the thread. This is considered very low + // severity. + if (ctx->level == IPPROTO_IP + && (ctx->optname == IP_ADD_MEMBERSHIP + || ctx->optname == IP_ADD_SOURCE_MEMBERSHIP + || ctx->optname == IP_DROP_MEMBERSHIP + || ctx->optname == IP_DROP_SOURCE_MEMBERSHIP + || ctx->optname == IP_BLOCK_SOURCE + || ctx->optname == IP_UNBLOCK_SOURCE + || ctx->optname == IP_MSFILTER)) { + return SETSOCKOPT_EPERM; } - { - // Prevent kernel-generated multicast traffic (IGMP, MLD) from being triggered by a - // UID that is under a lockdown VPN. A known leak that still exists is when a UID joins a multicast - // group prior to being under a lockdown VPN and then becomes under a lockdown VPN. In this case the - // IGMP/MLD will be generated when the kernel destroys the thread. This is considered very low - // severity. - if (ctx->level == IPPROTO_IP - && (ctx->optname == IP_ADD_MEMBERSHIP - || ctx->optname == IP_ADD_SOURCE_MEMBERSHIP - || ctx->optname == IP_DROP_MEMBERSHIP - || ctx->optname == IP_DROP_SOURCE_MEMBERSHIP - || ctx->optname == IP_BLOCK_SOURCE - || ctx->optname == IP_UNBLOCK_SOURCE - || ctx->optname == IP_MSFILTER)) { - return SETSOCKOPT_EPERM; - } - - if (ctx->level == IPPROTO_IPV6 - && (ctx->optname == IPV6_ADD_MEMBERSHIP /** IPV6_JOIN_GROUP **/ - || ctx->optname == IPV6_DROP_MEMBERSHIP /** IPV6_LEAVE_GROUP **/)) { - return SETSOCKOPT_EPERM; - } + if (ctx->level == IPPROTO_IPV6 + && (ctx->optname == IPV6_ADD_MEMBERSHIP /** IPV6_JOIN_GROUP **/ + || ctx->optname == IPV6_DROP_MEMBERSHIP /** IPV6_LEAVE_GROUP **/)) { + return SETSOCKOPT_EPERM; + } - if ((ctx->level == IPPROTO_IP || ctx->level == IPPROTO_IPV6) - && (ctx->optname == MCAST_JOIN_GROUP - || ctx->optname == MCAST_LEAVE_GROUP - || ctx->optname == MCAST_BLOCK_SOURCE - || ctx->optname == MCAST_UNBLOCK_SOURCE - || ctx->optname == MCAST_JOIN_SOURCE_GROUP - || ctx->optname == MCAST_LEAVE_SOURCE_GROUP)) { - return SETSOCKOPT_EPERM; - } + if ((ctx->level == IPPROTO_IP || ctx->level == IPPROTO_IPV6) + && (ctx->optname == MCAST_JOIN_GROUP + || ctx->optname == MCAST_LEAVE_GROUP + || ctx->optname == MCAST_BLOCK_SOURCE + || ctx->optname == MCAST_UNBLOCK_SOURCE + || ctx->optname == MCAST_JOIN_SOURCE_GROUP + || ctx->optname == MCAST_LEAVE_SOURCE_GROUP)) { + return SETSOCKOPT_EPERM; } return SETSOCKOPT_ALLOWED; diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h index 9e74a00286..664526c6aa 100644 --- a/bpf/progs/netd.h +++ b/bpf/progs/netd.h @@ -235,6 +235,7 @@ ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilte #define LOCAL_NET_NOTE_OP_RINGBUF_PATH BPF_NETD_PATH "map_netd_local_net_note_op_ringbuf" #define LOCAL_NET_NOTE_OP_CACHE_MAP_PATH BPF_NETD_PATH "map_netd_local_net_note_op_cache_map" #define LOCAL_NET_NOTE_OP_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_local_net_note_op_enabled_map" +#define SK_BIND_TO_DEVICE_EVENT_RINGBUF_PATH BPF_NETD_PATH "map_netd_sk_bind_to_device_event_ringbuf" #define LOCAL_NET_CACHE_GENERATION_ID_MAP_PATH \ BPF_NETD_PATH "map_netd_local_net_cache_generation_id_map" #define LOOPBACK_ACCESS_RINGBUF_NETD_PATH BPF_NETD_PATH "map_netd_loopback_access_ringbuf" @@ -270,7 +271,7 @@ enum UidOwnerMatchType : uint32_t { OEM_DENY_3_MATCH = (1 << 11), BACKGROUND_MATCH = (1 << 12), PENALTY_BOX_ADMIN_MATCH = (1 << 13), - LOCKDOWN_VPN_REGULAR_APP_MATCH = (1 << 30), + APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH = (1 << 30), }; // LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java) @@ -361,6 +362,13 @@ typedef struct { } LocalNetNoteOp; STRUCT_SIZE(LocalNetNoteOp, 4 + 4); // 8 +// uid and pid of process that had their attempt to use SO_BINDTODEVICE blocked. +typedef struct { + uint32_t uid; + uint32_t pid; +} SkBindToDeviceEvent; +STRUCT_SIZE(SkBindToDeviceEvent, 4 + 4); // 8 + // IP packet data from an __sk_buff typedef struct { struct in6_addr saddr; // Stores v6 or v4-mapped-v6 diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java index 1819c9c57b..f44040245e 100644 --- a/framework/src/android/net/BpfNetMapsConstants.java +++ b/framework/src/android/net/BpfNetMapsConstants.java @@ -109,7 +109,7 @@ private BpfNetMapsConstants() {} public static final long OEM_DENY_3_MATCH = (1 << 11); public static final long BACKGROUND_MATCH = (1 << 12); public static final long PENALTY_BOX_ADMIN_MATCH = (1 << 13); - public static final long LOCKDOWN_VPN_REGULAR_APP_MATCH = (1 << 30); + public static final long APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH = (1 << 30); public static final List> MATCH_LIST = Arrays.asList( Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"), @@ -126,7 +126,8 @@ private BpfNetMapsConstants() {} Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"), Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH"), Pair.create(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH"), - Pair.create(LOCKDOWN_VPN_REGULAR_APP_MATCH, "LOCKDOWN_VPN_REGULAR_APP_MATCH") + Pair.create(APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH, + "APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH") ); /** diff --git a/service/Android.bp b/service/Android.bp index 3bd14718d5..b967632d6d 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -137,6 +137,7 @@ cc_library_shared { "jni/com_android_server_connectivity_BpfEventPoller.cpp", "jni/com_android_server_connectivity_ClatCoordinator.cpp", "jni/com_android_server_connectivity_LocalNetEventListener.cpp", + "jni/com_android_server_connectivity_SkBindToDeviceEventListener.cpp", "jni/onload.cpp", ], header_libs: [ diff --git a/service/jni/com_android_server_connectivity_SkBindToDeviceEventListener.cpp b/service/jni/com_android_server_connectivity_SkBindToDeviceEventListener.cpp new file mode 100644 index 0000000000..02161e21d1 --- /dev/null +++ b/service/jni/com_android_server_connectivity_SkBindToDeviceEventListener.cpp @@ -0,0 +1,61 @@ +// Based on Connectivity/service/jni/com_android_server_connectivity_LocalNetEventListener.cpp + +#define LOG_TAG "SkBindToDeviceEventListenerNative" + +#include +#include +#include +#include +#include +#include +#include + +#include "bpf/BpfUtils.h" +#include "libeventpolling/SkBindToDeviceEventHandler.h" + +namespace android { + +using android::net::eventpolling::SkBindToDeviceEventHandler; + +static jobject nativeGetSkBindToDeviceEventRingbufFd(JNIEnv *env, jclass clazz) { + android::base::unique_fd fd = SkBindToDeviceEventHandler::GetNewRingbufFd(); + if (!fd.ok()) { + ALOGE("Failed to get sk_bind_to_device_event_ringbuf fd"); + return nullptr; + } + return jniCreateFileDescriptor(env, fd.release()); +} + +static jintArray nativeConsumeAllSkBindToDeviceEvents(JNIEnv *env, + jclass clazz) { + std::vector uids_pids = SkBindToDeviceEventHandler::ConsumeAll(); + if (uids_pids.empty()) { + return env->NewIntArray(0); + } + + ScopedLocalRef result(env, env->NewIntArray(uids_pids.size())); + if (!result.get()) { + ALOGE("Failed to allocate jintArray"); + return nullptr; + } + env->SetIntArrayRegion(result.get(), 0, uids_pids.size(), + reinterpret_cast(uids_pids.data())); + return result.release(); +} + +static const JNINativeMethod gMethods[] = { + MAKE_JNI_NATIVE_METHOD("nativeGetSkBindToDeviceEventRingbufFd", + "()Ljava/io/FileDescriptor;", + nativeGetSkBindToDeviceEventRingbufFd), + MAKE_JNI_NATIVE_METHOD_AUTOSIG("nativeConsumeAllSkBindToDeviceEvents", + nativeConsumeAllSkBindToDeviceEvents), +}; + +int register_com_android_server_connectivity_SkBindToDeviceEventListener( + JNIEnv *env) { + return jniRegisterNativeMethods(env, + "android/net/connectivity/com/android/" + "server/connectivity/SkBindToDeviceEventListener", + gMethods, NELEM(gMethods)); +} +} // namespace android diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp index 3d8809a84e..05d2635d35 100644 --- a/service/jni/onload.cpp +++ b/service/jni/onload.cpp @@ -24,6 +24,7 @@ namespace android { int register_com_android_server_connectivity_BpfEventPoller(JNIEnv *env); int register_com_android_server_connectivity_ClatCoordinator(JNIEnv *env); int register_com_android_server_connectivity_LocalNetEventListener(JNIEnv *env); +int register_com_android_server_connectivity_SkBindToDeviceEventListener(JNIEnv *env); int register_android_server_net_NetworkStatsFactory(JNIEnv *env); int register_android_server_net_NetworkStatsService(JNIEnv *env); int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env, @@ -49,6 +50,10 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { return JNI_ERR; } + if (register_com_android_server_connectivity_SkBindToDeviceEventListener(env) < 0) { + return JNI_ERR; + } + if (register_android_server_net_NetworkStatsFactory(env) < 0) { return JNI_ERR; } diff --git a/service/native/libs/libeventpolling/Android.bp b/service/native/libs/libeventpolling/Android.bp index 19aa262a94..30e3df1757 100644 --- a/service/native/libs/libeventpolling/Android.bp +++ b/service/native/libs/libeventpolling/Android.bp @@ -46,6 +46,7 @@ cc_library_static { srcs: [ "LocalNetEventHandler.cpp", "LoopbackEventHandler.cpp", + "SkBindToDeviceEventHandler.cpp", ], defaults: ["libstatslog_defaults"], export_include_dirs: ["include"], diff --git a/service/native/libs/libeventpolling/SkBindToDeviceEventHandler.cpp b/service/native/libs/libeventpolling/SkBindToDeviceEventHandler.cpp new file mode 100644 index 0000000000..56f6c5419a --- /dev/null +++ b/service/native/libs/libeventpolling/SkBindToDeviceEventHandler.cpp @@ -0,0 +1,48 @@ +// Based on Connectivity/service/native/libs/libeventpolling/LocalNetEventHandler.cpp + +#define LOG_TAG "SkBindToDeviceEventHandler" + +#include "libeventpolling/SkBindToDeviceEventHandler.h" + +#include +#include + +#include +#include +#include +#include + +#include "netd.h" + +namespace android::net::eventpolling { + +using bpf::BpfRingbuf; +using bpf::RingbufEventPoller; + +// static +SkBindToDeviceEventHandler::SkBindToDeviceEventRingbuf *SkBindToDeviceEventHandler::GetRingbuf() { + static SkBindToDeviceEventRingbuf *const sRingbuf = + []() -> SkBindToDeviceEventRingbuf * { + auto rb = std::make_unique( + SK_BIND_TO_DEVICE_EVENT_RINGBUF_PATH); + return rb.release(); + }(); + return sRingbuf; +} + +// static +std::vector SkBindToDeviceEventHandler::ConsumeAll() { + std::vector uids_pids; + base::Result ret = + GetRingbuf()->ConsumeAll([&](const SkBindToDeviceEvent &event) { + uids_pids.push_back(event.uid); + uids_pids.push_back(event.pid); + }); + if (!ret.ok()) { + ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str()); + return {}; + } + return uids_pids; +} + +} // namespace android::net::eventpolling \ No newline at end of file diff --git a/service/native/libs/libeventpolling/include/libeventpolling/SkBindToDeviceEventHandler.h b/service/native/libs/libeventpolling/include/libeventpolling/SkBindToDeviceEventHandler.h new file mode 100644 index 0000000000..eb5e8f7c39 --- /dev/null +++ b/service/native/libs/libeventpolling/include/libeventpolling/SkBindToDeviceEventHandler.h @@ -0,0 +1,44 @@ +// Based on Connectivity/service/native/libs/libeventpolling/include/libeventpolling/LocalNetEventHandler.h + +#pragma once + +#include + +#include +#include + +#include "netd.h" + +namespace android::net::eventpolling { + +class SkBindToDeviceEventHandler { + public: + SkBindToDeviceEventHandler() = delete; + SkBindToDeviceEventHandler(const SkBindToDeviceEventHandler &) = delete; + SkBindToDeviceEventHandler &operator=(const SkBindToDeviceEventHandler &) = delete; + + static android::base::unique_fd GetNewRingbufFd() { + return GetRingbuf()->GetDuplicateFd(); + } + + // Consumes all available events in the ring buffer. Returns a list of + // alternating UIDs / PIDs, where each UID / PID pair represents a single + // access event. + static std::vector ConsumeAll(); + + private: + class SkBindToDeviceEventRingbuf : public bpf::BpfRingbuf { + public: + SkBindToDeviceEventRingbuf(const char *path) + : BpfRingbuf(path) {} + + android::base::unique_fd GetDuplicateFd() { + return android::base::unique_fd( + fcntl(mRingFd.get(), F_DUPFD_CLOEXEC, 0)); + } + }; + + static SkBindToDeviceEventRingbuf *GetRingbuf(); +}; + +} // namespace android::net::eventpolling diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java index 426a356526..d9aa687ff7 100644 --- a/service/src/com/android/server/BpfNetMaps.java +++ b/service/src/com/android/server/BpfNetMaps.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.net.BpfNetMapsConstants.APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH; import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH; import static android.net.BpfNetMapsConstants.COOKIE_TAG_MAP_PATH; import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY; @@ -37,7 +38,6 @@ import static android.net.BpfNetMapsConstants.LOOPBACK_CHECKS_ENABLED_MAP_PATH; import static android.net.BpfNetMapsConstants.PERMISSION_PROPAGATION_ENABLED_MAP_PATH; import static android.net.BpfNetMapsConstants.UID_MIGRATION_ENABLED_MAP_PATH; -import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_REGULAR_APP_MATCH; import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH; import static android.net.BpfNetMapsConstants.UID_PERMISSION_CHUNK_MAP_PATH; import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH; @@ -101,7 +101,6 @@ import android.app.StatsManager; import android.content.Context; import android.net.BpfNetMapsUtils; -import android.ext.ConnectivityUtil; import android.net.INetd; import android.net.UidOwnerValue; import android.os.Build; @@ -289,8 +288,6 @@ public static boolean isL4sSupported() { return sL4sSupported; } - private Context mContext; - /** * Set configurationMap for test. */ @@ -981,7 +978,6 @@ public BpfNetMaps(final Context context, final INetd netd, final Dependencies de mNetd = netd; mDeps = deps; mInterfaceTracker = interfaceTracker; - mContext = context; } private void maybeThrow(final int err, final String msg) { @@ -1358,12 +1354,18 @@ public void updateUidLockdownRule(final int uid, final boolean add) { if (add) { addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule"); - if (!ConnectivityUtil.isSystem(mContext, uid)) { - addRule(uid, LOCKDOWN_VPN_REGULAR_APP_MATCH, "updateUidLockdownRule"); - } } else { removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule"); - removeRule(uid, LOCKDOWN_VPN_REGULAR_APP_MATCH, "updateUidLockdownRule"); + } + } + + public void updateAppStrictLeakBlockingDisabledRule(final int uid, final boolean add) { + if (add) { + addRule(uid, APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH, + "updateAppStrictLeakBlockingDisabledRule"); + } else { + removeRule(uid, APP_STRICT_LEAK_BLOCKING_DISABLED_MATCH, + "updateAppStrictLeakBlockingDisabledRule"); } } diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 2503979890..e01658ca32 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -435,6 +435,7 @@ import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; import com.android.server.connectivity.QuicConnectionCloser; +import com.android.server.connectivity.SkBindToDeviceEventListener; import com.android.server.connectivity.UidRangeUtils; import com.android.server.connectivity.VpnNetworkPreferenceInfo; import com.android.server.connectivity.proxy.MultiProxyTracker; @@ -660,6 +661,7 @@ public class ConnectivityService extends IConnectivityManager.Stub protected INetd mNetd; private DscpPolicyTracker mDscpPolicyTracker = null; private final LocalNetEventListener mLocalNetEventListener; + private final SkBindToDeviceEventListener mSkBindToDeviceEventListener; private final NetworkStatsManager mStatsManager; private final NetworkPolicyManager mPolicyManager; private final BpfNetMaps mBpfNetMaps; @@ -1954,6 +1956,14 @@ public LocalNetEventListener getLocalNetEventListener( context, looper, metricsEnabled, noteOpsEnabled); } + /** + * Creates a SkBindToDeviceEventListener. + */ + public SkBindToDeviceEventListener getSkBindToDeviceEventListener(Context context, + Looper looper) { + return new SkBindToDeviceEventListener(context, looper); + } + /** * Wraps {@link TcUtils#tcFilterAddDevIngressPolice} */ @@ -2333,6 +2343,9 @@ protected ConnectivityService(Context context, IDnsResolver dnsresolver, mLocalNetEventListener = null; } + mSkBindToDeviceEventListener = mDeps.getSkBindToDeviceEventListener(mContext, + mHandler.getLooper()); + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext); @@ -2419,6 +2432,7 @@ mHandler, new SharedLog(20, TAG), TAG, mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); + mPermissionMonitor.registerGosPackageStateChangeCallback(); // TODO: Move all intent receivers to the helper class. mBroadcastReceiveHelper.registerReceivers(); @@ -4706,6 +4720,8 @@ public void systemReadyInternal() { mLocalNetEventListener.start(); } + mSkBindToDeviceEventListener.start(); + // Clear all clsact stubs on all interfaces. mHandler.post(() -> maybeClearTcQdiscClsact()); @@ -8548,6 +8564,21 @@ public void onUserRemoved(@NonNull final UserHandle user) { mSettingsObserver.onUsersChanged(); } + @Override + public void onUserStarted(@NonNull final UserHandle user) { + mPermissionMonitor.onUserStarted(user); + } + + @Override + public void onUserStopped(@NonNull final UserHandle user) { + mPermissionMonitor.onUserStopped(user); + } + + @Override + public void onUidRemoved(int uid) { + mPermissionMonitor.onUidRemoved(uid); + } + @Override public void onPackageAdded(@NonNull final String packageName, final int uid) { handlePackageChanged(packageName); diff --git a/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java b/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java index 5fb060a0ed..4eefb04dc5 100644 --- a/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java +++ b/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java @@ -48,6 +48,7 @@ public class BroadcastReceiveHelper { private final DeferredBroadcastReceiver mPackageIntentReceiver; private final DeferredBroadcastReceiver mUserIntentReceiver; private final DeferredBroadcastReceiver mExternalAppIntentReceiver; + private final DeferredBroadcastReceiver mUidIntentReceiver; /** * Interface defining the callback methods for package and user related events. @@ -69,6 +70,13 @@ public interface Delegate { */ void onPackageRemoved(String packageName, int uid); + /** + * Called when a uid has been removed (all packages using that uid have been removed). + * + * @param uid The uid that has been removed. + */ + void onUidRemoved(int uid); + /** * Called when an existing package has been replaced with a new version. * @@ -103,6 +111,20 @@ public interface Delegate { * @param userHandle The {@link UserHandle} of the user that was removed. */ void onUserRemoved(UserHandle userHandle); + + /** + * Called when a user has been started. + * + * @param userHandle The {@link UserHandle} of the user that was started. + */ + void onUserStarted(UserHandle userHandle); + + /** + * Called when a user has been stopped. + * + * @param userHandle The {@link UserHandle} of the user that was stopped. + */ + void onUserStopped(UserHandle userHandle); } /** @@ -118,6 +140,7 @@ public BroadcastReceiveHelper(@NonNull Context context, mHandler = handler; mCallback = callback; mPackageIntentReceiver = new DeferredBroadcastReceiver(mHandler, this::handlePackageIntent); + mUidIntentReceiver = new DeferredBroadcastReceiver(mHandler, this::handleUidIntent); mExternalAppIntentReceiver = new DeferredBroadcastReceiver(mHandler, this::handleExternalAppIntent); mUserIntentReceiver = new DeferredBroadcastReceiver(mHandler, this::handleUserIntent); @@ -135,6 +158,8 @@ public void registerReceivers() { final IntentFilter userIntentFilter = new IntentFilter(); userIntentFilter.addAction(Intent.ACTION_USER_ADDED); userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); + userIntentFilter.addAction(Intent.ACTION_USER_STARTED); + userIntentFilter.addAction(Intent.ACTION_USER_STOPPED); userAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter, NETWORK_STACK, mHandler); @@ -147,6 +172,11 @@ public void registerReceivers() { userAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter, NETWORK_STACK, mHandler); + final IntentFilter uidIntentFilter = new IntentFilter(); + uidIntentFilter.addAction(Intent.ACTION_UID_REMOVED); + userAllContext.registerReceiver(mUidIntentReceiver, uidIntentFilter, + NETWORK_STACK, mHandler); + // For PermissionMonitor, listen to EXTERNAL_APPLICATIONS_AVAILABLE is that an app // becoming available means it may need to gain a permission. But an app that becomes // unavailable can neither gain nor lose permissions on that account, it just can no @@ -244,6 +274,24 @@ private void handlePackageIntent(Intent intent) { } } + private void handleUidIntent(Intent intent) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + if (uid == -1) { + throw new IllegalArgumentException(); + } + + switch (intent.getAction()) { + case Intent.ACTION_UID_REMOVED: + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + mCallback.onUidRemoved(uid); + } + default: + Log.wtf(TAG, "received unexpected intent: " + intent.getAction()); + } + } + private void handleExternalAppIntent(Intent intent) { HandlerUtils.ensureRunningOnHandlerThread(mHandler); switch (intent.getAction()) { @@ -261,18 +309,23 @@ private void handleExternalAppIntent(Intent intent) { private void handleUserIntent(Intent intent) { HandlerUtils.ensureRunningOnHandlerThread(mHandler); final String action = intent.getAction(); - final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); - - // User should be filled for below intents, check the existence. + UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); if (user == null) { - Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER"); - return; + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userId == -1) { + throw new IllegalArgumentException(); + } + user = UserHandle.of(userId); } if (Intent.ACTION_USER_ADDED.equals(action)) { mCallback.onUserAdded(user); } else if (Intent.ACTION_USER_REMOVED.equals(action)) { mCallback.onUserRemoved(user); + } else if (Intent.ACTION_USER_STARTED.equals(action)) { + mCallback.onUserStarted(user); + } else if (Intent.ACTION_USER_STOPPED.equals(action)) { + mCallback.onUserStopped(user); } else { Log.wtf(TAG, "received unexpected intent: " + action); } diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java index bf68eba401..e75ccb9f66 100755 --- a/service/src/com/android/server/connectivity/PermissionMonitor.java +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -67,10 +67,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.ContentObserver; +import android.ext.ConnectivityUtil; import android.net.ConnectivitySettingsManager; import android.net.INetd; import android.net.UidRange; @@ -106,6 +108,8 @@ import com.android.server.LocalManagerRegistry; import com.android.server.permission.PermissionBpfMap; import com.android.server.permission.PermissionManagerLocal; +import com.android.server.pm.PackageManagerLocal; +import com.android.server.pm.PackageManagerLocal.GosPackageStateChangeCallback; import java.util.ArrayList; import java.util.HashSet; @@ -194,6 +198,10 @@ public class PermissionMonitor { @GuardedBy("this") private final Map mUsersUidsTrafficPermissions = new ArrayMap<>(); + // For each user, stores the uids that have app strict leak blocking disabled. + private final Map> mUsersUidsAppStrictLeakBlockingDisabled = + new ArrayMap<>(); + private static final int SYSTEM_APPID = SYSTEM_UID; private static final int MAX_PERMISSION_UPDATE_LOGS = 40; @@ -417,6 +425,10 @@ public void removeUser(int userId) { public boolean isAccessLocalNetworkPermissionEnabled() { return accessLocalNetworkPermissionEnabled(); } + + public GosPackageState getGosPackageState(String packageName, int userId) { + return GosPackageState.get(packageName, userId); + } } private boolean shouldEnforceLocalNetRestrictions(int uid) { @@ -970,7 +982,159 @@ public synchronized void onUserAddedWithInstalledPackageList(@NonNull UserHandle mPermissionUpdateLogs.log("New user(" + user.getIdentifier() + ") added: nPerm uids=" + uids + ", tPerm appIds=" + addedUserAppIds); } + } + + public void registerGosPackageStateChangeCallback() { + GosPackageStateChangeCallback callback = new GosPackageStateChangeCallback( + new Handler(mThread.getLooper())) { + @Override + public void onGosPackageStateChanged(int uid, @NonNull GosPackageState state, + int userId) { + PermissionMonitor.this.onGosPackageStateChanged(uid, state, userId); + } + }; + PackageManagerLocal pml = LocalManagerRegistry.getManager(PackageManagerLocal.class); + if (pml != null) { + pml.addGosPackageStateChangeCallback(callback); + } + } + + public void onGosPackageStateChanged(int uid, @NonNull GosPackageState ps, + int userId) { + ensureRunningOnHandlerThread(); + updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, ps); + } + + public void updateAppStrictLeakBlockingBpf(UserHandle user, int uid, + @Nullable GosPackageState ps) { + // Core uids have strict leak blocking unconditionally disabled in eBPF, so ignore them. + if (UserHandle.isCore(uid)) { + return; + } + + boolean wasDisabled = wasAppStrictLeakBlockingDisabled(user, uid); + boolean isDisabled = isAppStrictLeakBlockingDisabled(user.getIdentifier(), uid, ps); + + if (wasDisabled == isDisabled) { + return; + } + + if (isDisabled) { + disableAppStrictLeakBlocking(user, uid); + } else { + enableAppStrictLeakBlocking(user, uid); + } + } + + private boolean wasAppStrictLeakBlockingDisabled(UserHandle user, int uid) { + Set disabledUids = mUsersUidsAppStrictLeakBlockingDisabled.get(user); + if (disabledUids == null) { + return false; + } + return disabledUids.contains(uid); + } + private boolean isAppStrictLeakBlockingDisabled(int userId, int uid, + @Nullable GosPackageState ps) { + String[] packageNames = mPackageManager.getPackagesForUid(uid); + if (packageNames == null) { + // All packages for this uid have been uninstalled, so there's no need to disable strict + // leak blocking. + return false; + } + + if (ps == null) { + // GosPackageState is same for all packages that share a uid, so just use 0th. + ps = mDeps.getGosPackageState(packageNames[0], userId); + } + + ApplicationInfo ai = null; + // Iterate all packageNames in search of an applicationInfo. For AswSocketBindToDevice it + // doesn't matter which package's applicationInfo we use, only that the packages all share a + // uid. + for (String packageName : packageNames) { + try { + ai = mPackageManager.getPackageInfoAsUser(packageName, 0, userId).applicationInfo; + } catch (NameNotFoundException ignored) {} + + if (ai != null) { + // Technically a package name could have been reinstalled with a different uid, so + // ensure the uids match. + if (ai.uid == uid) { + break; + } + ai = null; + } + } + + if (ai == null) { + // No applicationInfo implies no executable code, so there's no need to disable strict + // leak blocking. + return false; + } + + return !ConnectivityUtil.isAppStrictLeakBlockingEnabled(mContext, userId, ai, ps); + } + + private void disableAppStrictLeakBlocking(UserHandle user, int uid) { + Set disabledUids = mUsersUidsAppStrictLeakBlockingDisabled.get(user); + if (disabledUids == null) { + disabledUids = new ArraySet<>(); + mUsersUidsAppStrictLeakBlockingDisabled.put(user, disabledUids); + } + for (int associatedUid : getAssociatedUids(uid)) { + disabledUids.add(associatedUid); + mBpfNetMaps.updateAppStrictLeakBlockingDisabledRule(associatedUid, true); + } + } + + private void enableAppStrictLeakBlocking(UserHandle user, int uid) { + Set disabledUids = mUsersUidsAppStrictLeakBlockingDisabled.get(user); + for (int associatedUid : getAssociatedUids(uid)) { + disabledUids.remove(associatedUid); + mBpfNetMaps.updateAppStrictLeakBlockingDisabledRule(associatedUid, false); + } + } + + private List getAssociatedUids(int uid) { + ArrayList associatedUids = new ArrayList<>(List.of(uid)); + if (hasSdkSandbox(uid)) { + associatedUids.add(Process.toSdkSandboxUid(uid)); + } + return associatedUids; + } + + public void onUserStarted(@NonNull UserHandle user) { + ensureRunningOnHandlerThread(); + final List packages = mContext.getPackageManager() + .getInstalledPackagesAsUser(0, user.getIdentifier()); + + for (PackageInfo pi : packages) { + if (pi.applicationInfo != null) { + updateAppStrictLeakBlockingBpf(user, pi.applicationInfo.uid, null); + } + } + } + + public void onUserStopped(@NonNull UserHandle user) { + ensureRunningOnHandlerThread(); + Set disabledUids = mUsersUidsAppStrictLeakBlockingDisabled.get(user); + for (Integer disabledUid : disabledUids) { + // Remove the rule so that the BPF UidOwnerValue can potentially be marked for removal. + mBpfNetMaps.updateAppStrictLeakBlockingDisabledRule(disabledUid, false); + } + + mUsersUidsAppStrictLeakBlockingDisabled.remove(user); + } + + public void onUidRemoved(int uid) { + ensureRunningOnHandlerThread(); + UserHandle user = UserHandle.getUserHandleForUid(uid); + if (wasAppStrictLeakBlockingDisabled(user, uid)) { + // The uid is guaranteed not to be reused until next boot, but enabling it allows the + // BPF UidOwnerValue to potentially be marked for removal. + enableAppStrictLeakBlocking(user, uid); + } } /** diff --git a/service/src/com/android/server/connectivity/SkBindToDeviceEventListener.java b/service/src/com/android/server/connectivity/SkBindToDeviceEventListener.java new file mode 100644 index 0000000000..ad75ef64ee --- /dev/null +++ b/service/src/com/android/server/connectivity/SkBindToDeviceEventListener.java @@ -0,0 +1,87 @@ +package com.android.server.connectivity; + +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.Looper; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalManagerRegistry; +import com.android.server.am.ActivityManagerLocal; + +import java.io.FileDescriptor; +import java.util.Objects; + +// Based on com.android.server.connectivity.LocalNetEventListener. +public class SkBindToDeviceEventListener { + private static final String TAG = SkBindToDeviceEventListener.class.getSimpleName(); + + private final ActivityManagerLocal mActivityManager; + private final Dependencies mDeps; + private final FileDescriptor mRingbufFd; + private final Looper mLooper; + + public SkBindToDeviceEventListener(@NonNull Context context, @NonNull Looper looper) { + this(new Dependencies(), context, looper); + } + + @VisibleForTesting + public SkBindToDeviceEventListener(@NonNull final Dependencies deps, @NonNull Context context, + @NonNull Looper looper) { + Objects.requireNonNull(context); + mActivityManager = LocalManagerRegistry.getManager(ActivityManagerLocal.class); + mDeps = deps; + mRingbufFd = mDeps.getFileDescriptor(); + mLooper = looper; + } + + public static class Dependencies { + public FileDescriptor getFileDescriptor() { + return nativeGetSkBindToDeviceEventRingbufFd(); + } + + public int[] consumeEvents() { + return nativeConsumeAllSkBindToDeviceEvents(); + } + } + + public void start() { + mLooper.getQueue().addOnFileDescriptorEventListener(mRingbufFd, + EVENT_INPUT | EVENT_ERROR, this::consumeEvents); + } + + /** + * @return The events to listen for next, or 0 to stop listening. + */ + private int consumeEvents(FileDescriptor fd, int events) { + if (!mRingbufFd.equals(fd)) { + Log.w(TAG, "Received event for unexpected FD"); + return 0; + } + + if ((events & EVENT_ERROR) != 0) { + Log.e(TAG, "Error event on the ring buffer FD"); + return 0; + } + + if ((events & EVENT_INPUT) != 0) { + int[] uidsPids = mDeps.consumeEvents(); + if (uidsPids.length % 2 != 0) { + Log.e(TAG, "Received malformed UID/PID list from native layer"); + return 0; + } + for (int i = 0; i < uidsPids.length; i += 2) { + mActivityManager.showAppStrictLeakBlockingNotification(uidsPids[i], uidsPids[i+1]); + } + } + + return EVENT_INPUT | EVENT_ERROR; + } + + public static native FileDescriptor nativeGetSkBindToDeviceEventRingbufFd(); + + public static native int[] nativeConsumeAllSkBindToDeviceEvents(); +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 8cc92aa87c..bf64c5f241 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -104,6 +104,7 @@ android_library { "service-connectivity-tiramisu-pre-jarjar", "testables", "cts-net-utils", + "services.core", ], libs: [ "android.net.ipsec.ike.stubs.module_lib", diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 78fef0d0f0..e0b69ea9de 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -427,6 +427,7 @@ import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; import com.android.server.connectivity.QuicConnectionCloser; +import com.android.server.connectivity.SkBindToDeviceEventListener; import com.android.server.connectivity.TcpKeepaliveController; import com.android.server.connectivity.UidRangeUtils; import com.android.server.net.NetworkPinner; @@ -504,7 +505,7 @@ * Tests for {@link ConnectivityService}. * * Build, install and run with: - * runtest frameworks-net -c com.android.server.ConnectivityServiceTest + * atest -c ConnectivityCoverageTests:android.net.connectivity.com.android.server.ConnectivityServiceTest */ // TODO : move methods from this test to smaller tests in the 'connectivityservice' directory // to enable faster testing of smaller groups of functionality. @@ -662,6 +663,8 @@ public class ConnectivityServiceTest { @Mock QuicConnectionCloser mQuicConnectionCloser; @Mock LocalNetEventListener mLocalNetEventListener; + @Mock + SkBindToDeviceEventListener mSkBindToDeviceEventListener; // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the // underlying binder calls. @@ -2353,6 +2356,12 @@ public LocalNetEventListener getLocalNetEventListener( return mLocalNetEventListener; } + @Override + public SkBindToDeviceEventListener getSkBindToDeviceEventListener( + Context context, Looper looper) { + return mSkBindToDeviceEventListener; + } + final ArrayTrackRecord> mRateLimitHistory = new ArrayTrackRecord<>(); final Map mActiveRateLimit = new HashMap<>(); diff --git a/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt b/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt index e671257c60..0b99e76797 100644 --- a/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt +++ b/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt @@ -63,6 +63,9 @@ private inline fun any() = org.mockito.Mockito.any(T::class.java) @DevSdkIgnoreRunner.MonitorThreadLeak @RunWith(DevSdkIgnoreRunner::class) +/** + * atest -c ConnectivityCoverageTests:android.net.connectivity.com.android.server.connectivity.BroadcastReceiveHelperTest + */ class BroadcastReceiveHelperTest { private val mockContext = mock(Context::class.java) private val testDelegate = TestDelegate() @@ -326,6 +329,9 @@ class BroadcastReceiveHelperTest { data class OnExternalApplicationsAvailable(val pkgList: Array) : CallbackEvent() data class OnUserAdded(val userHandle: UserHandle) : CallbackEvent() data class OnUserRemoved(val userHandle: UserHandle) : CallbackEvent() + data class OnUserStarted(val userHandle: UserHandle) : CallbackEvent() + data class OnUserStopped(val userHandle: UserHandle) : CallbackEvent() + data class OnUidRemoved(val uid: Int) : CallbackEvent() } override fun onPackageAdded(packageName: String, uid: Int) { @@ -351,5 +357,17 @@ class BroadcastReceiveHelperTest { override fun onUserRemoved(userHandle: UserHandle) { history.add(CallbackEvent.OnUserRemoved(userHandle)) } + + override fun onUserStarted(userHandle: UserHandle) { + history.add(CallbackEvent.OnUserStarted(userHandle)) + } + + override fun onUserStopped(userHandle: UserHandle) { + history.add(CallbackEvent.OnUserStopped(userHandle)) + } + + override fun onUidRemoved(uid: Int) { + history.add(CallbackEvent.OnUidRemoved(uid)) + } } } diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java index 16dec474bf..6f06142687 100644 --- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -106,6 +106,8 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.ContentObserver; @@ -161,6 +163,9 @@ import java.util.Set; import java.util.function.Consumer; +/** + * atest -c ConnectivityCoverageTests:android.net.connectivity.com.android.server.connectivity.PermissionMonitorTest + */ @RunWith(DevSdkIgnoreRunner.class) @SmallTest @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @@ -216,6 +221,7 @@ public class PermissionMonitorTest { private static final String MOCK_PACKAGE3 = "appName3"; private static final String SYSTEM_PACKAGE1 = "sysName1"; private static final String SYSTEM_PACKAGE2 = "sysName2"; + private static final String PARTITION_DATA = "data"; private static final String PARTITION_SYSTEM = "system"; private static final String PARTITION_OEM = "oem"; private static final String PARTITION_PRODUCT = "product"; @@ -311,6 +317,11 @@ private boolean hasSdkSandbox(final int uid) { return SdkLevel.isAtLeastT() && Process.isApplicationUid(uid); } + private static PackageInfo nonSystemPackageInfoWithPermissions(String... permissions) { + return packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_DATA); + } + private static PackageInfo systemPackageInfoWithPermissions(String... permissions) { return packageInfoWithPermissions( REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM); @@ -344,12 +355,30 @@ private static PackageInfo packageInfoWithPermissions(int permissionsFlags, break; } packageInfo.applicationInfo.privateFlags = privateFlags; + if (privateFlags != 0 || partition.equals(PARTITION_SYSTEM)) { + packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + } return packageInfo; } + private enum IsSystemPackage { + TRUE, + FALSE + } + private static PackageInfo buildPackageInfo(String packageName, int uid, String... permissions) { - final PackageInfo pkgInfo = systemPackageInfoWithPermissions(permissions); + return buildPackageInfo(packageName, uid, IsSystemPackage.FALSE, permissions); + } + + private static PackageInfo buildPackageInfo(String packageName, int uid, + IsSystemPackage isSystemPackage, String... permissions) { + PackageInfo pkgInfo; + if (isSystemPackage == IsSystemPackage.TRUE) { + pkgInfo = systemPackageInfoWithPermissions(permissions); + } else { + pkgInfo = nonSystemPackageInfoWithPermissions(permissions); + } pkgInfo.packageName = packageName; pkgInfo.applicationInfo.uid = uid; return pkgInfo; @@ -369,10 +398,19 @@ private static PackageInfo buildPackageInfo(String packageName, int uid, private PackageInfo buildAndMockPackageInfoWithPermissions(String packageName, int uid, String... permissions) throws Exception { - final PackageInfo packageInfo = buildPackageInfo(packageName, uid, permissions); + return buildAndMockPackageInfoWithPermissions(packageName, uid, IsSystemPackage.FALSE, + permissions); + } + + private PackageInfo buildAndMockPackageInfoWithPermissions(String packageName, int uid, + IsSystemPackage isSystemPackage, String... permissions) throws Exception { + final PackageInfo packageInfo = buildPackageInfo(packageName, uid, isSystemPackage, + permissions); // This will return the wrong UID for the package when queried with other users. doReturn(packageInfo).when(mPackageManager) .getPackageInfo(eq(packageName), anyInt() /* flag */); + doReturn(packageInfo).when(mPackageManager) + .getPackageInfoAsUser(eq(packageName), anyInt() /* flag */, eq(UserHandle.getUserId(uid))); if (isAtLeastB()) { // Runtime permission checks for local net restrictions were introduced in 25Q2 for (String permission : permissions) { @@ -404,6 +442,14 @@ private void onUserRemoved(UserHandle user) { processOnHandlerThread(() -> mPermissionMonitor.onUserRemoved(user)); } + private void onUserStopped(UserHandle user) { + processOnHandlerThread(() -> mPermissionMonitor.onUserStopped(user)); + } + + private void onUidRemoved(int uid) { + processOnHandlerThread(() -> mPermissionMonitor.onUidRemoved(uid)); + } + private void onPackageAdded(String packageName, int uid) { processOnHandlerThread(() -> mPermissionMonitor.onPackageAdded(packageName, uid)); } @@ -2814,4 +2860,178 @@ public void testUserAdded_Removed_Added_UidMigrationEnabled() { addUserAndVerifyUidsPermissions(MOCK_USER2, pkgs2, PERMISSION_UNINSTALLED, PERMISSION_UNINSTALLED, PERMISSION_UNINSTALLED); } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_CoreUid_DoesNothing() throws Exception { + int userId = MOCK_USER_ID1; + int uid = SYSTEM_APP_UID11; + String packageName = SYSTEM_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid, IsSystemPackage.TRUE); + doReturn(GosPackageState.NONE).when(mDeps).getGosPackageState(packageName, userId); + + assertTrue(UserHandle.isCore(uid)); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + + verify(mBpfNetMaps, never()).updateAppStrictLeakBlockingDisabledRule(anyInt(), anyBoolean()); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_WasAndIsEnabled_DoesNothing() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + doReturn(GosPackageState.NONE).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + + verify(mBpfNetMaps, never()).updateAppStrictLeakBlockingDisabledRule(anyInt(), anyBoolean()); + } + + private GosPackageState createStrictLeakBlockingNonDefaultState(String packageName, int uid) { + GosPackageState state = GosPackageState.NONE; + GosPackageState.Editor stateEditor = state.createEditor(packageName, uid); + stateEditor.addFlag(GosPackageStateFlag.STRICT_LEAK_BLOCKING_NON_DEFAULT); + return stateEditor.toState(); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_WasEnabledIsDisabled_Disables() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(uid, true); + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), true); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_WasAndIsDisabled_DoesNothing() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + // Pass state manually to simulate a more realistic scenario where the second call comes + // from OnGosPackageStateChanged. Doesn't matter either way. + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, state); + + // Should only be called 2 times for the initial disable (as per + // testUpdateAppStrictLeakBlockingBpf_WasEnabledIsDisabled_Disables). + verify(mBpfNetMaps, times(2)).updateAppStrictLeakBlockingDisabledRule(anyInt(), + anyBoolean()); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_WasDisabledIsEnabled_Enables() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, + GosPackageState.NONE); + + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(uid, false); + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), + false); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_DisableEnableTwice_DisableEnablesTwice() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, state); + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, + GosPackageState.NONE); + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, + state); + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, + GosPackageState.NONE); + + verify(mBpfNetMaps, times(2)).updateAppStrictLeakBlockingDisabledRule(uid, true); + verify(mBpfNetMaps, times(2)).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), + true); + verify(mBpfNetMaps, times(2)).updateAppStrictLeakBlockingDisabledRule(uid, false); + verify(mBpfNetMaps, times(2)).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), + false); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_UidNoPackage_DoesNothing() { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + + verify(mBpfNetMaps, never()).updateAppStrictLeakBlockingDisabledRule(anyInt(), anyBoolean()); + } + + @Test + public void testUpdateAppStrictLeakBlockingBpf_PackageNoAppInfo_DoesNothing() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + PackageInfo pkg = buildAndMockPackageInfoWithPermissions(packageName, uid); + pkg.applicationInfo = null; + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + + verify(mBpfNetMaps, never()).updateAppStrictLeakBlockingDisabledRule(anyInt(), anyBoolean()); + } + + @Test + public void testOnUserStopped_LeakBlockingDisabled_RemovesBpfRules() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + onUserStopped(UserHandle.of(userId)); + + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(uid, false); + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), + false); + } + + @Test + public void testOnUidRemoved_LeakBlockingDisabled_EnablesLeakBlocking() throws Exception { + int userId = MOCK_USER_ID1; + int uid = MOCK_UID11; + String packageName = MOCK_PACKAGE1; + buildAndMockPackageInfoWithPermissions(packageName, uid); + GosPackageState state = createStrictLeakBlockingNonDefaultState(packageName, uid); + doReturn(state).when(mDeps).getGosPackageState(packageName, userId); + + mPermissionMonitor.updateAppStrictLeakBlockingBpf(UserHandle.of(userId), uid, null); + onUidRemoved(uid); + + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(uid, false); + verify(mBpfNetMaps).updateAppStrictLeakBlockingDisabledRule(Process.toSdkSandboxUid(uid), + false); + } }