microg/GmsCore · error · SecurityException

Caller must hold $permission for location bypass

Error message

Caller must hold $permission for location bypass

What it means

LocationManager.getLastLocation throws this SecurityException when LastLocationRequest.isBypass is true but the calling process does not hold the required bypass permission: android.permission.LOCATION_BYPASS on Android 13+ (SDK 33+), or android.permission.WRITE_SECURE_SETTINGS on older versions. Bypass is a privileged escape hatch that skips location permission checks, so microG requires the caller to hold a system-level permission before honoring it. It is thrown synchronously inside the suspend function before any location is returned.

Solutions

  1. Remove setBypass(true) from the LastLocationRequest and request normal location permissions (ACCESS_FINE_LOCATION) instead.
  2. If bypass is genuinely needed (system app), grant the required permission: adb shell pm grant <pkg> android.permission.WRITE_SECURE_SETTINGS (SDK<33) or add android.permission.LOCATION_BYPASS to the privapp-permissions whitelist (SDK>=33).
  3. Runtime-check before calling: context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED, mirroring the library's own logic.

Example fix

// before
val request = LastLocationRequest.Builder().setBypass(true).build()
val location = fusedLocationClient.getLastLocation(request)
// after
val request = LastLocationRequest.Builder().build() // no bypass
val location = fusedLocationClient.getLastLocation(request)
Defensive patterns

Strategy: try-catch

Validate before calling

val needed = if (Build.VERSION.SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else android.Manifest.permission.WRITE_SECURE_SETTINGS
val ok = context.checkPermission(needed, android.os.Process.myPid(), android.os.Process.myUid()) == PackageManager.PERMISSION_GRANTED

Type guard

fun canBypass(ctx: Context): Boolean {
    val p = if (Build.VERSION.SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else android.Manifest.permission.WRITE_SECURE_SETTINGS
    return ctx.checkPermission(p, android.os.Process.myPid(), android.os.Process.myUid()) == PackageManager.PERMISSION_GRANTED
}

Try / catch

try {
    client.getLastLocation(request)
} catch (e: SecurityException) {
    if (e.message?.contains("bypass") == true) fallbackToNonBypassRequest()
    else throw e
}

Prevention

When it happens

Trigger: Calling FusedLocationProviderApi.getCurrentLocation/LastLocation with a LastLocationRequest built via setBypass(true) (e.g. LocationRequest.Builder#setBypass) while the app's uid/pid lacks android.permission.LOCATION_BYPASS (SDK>=33) or android.permission.WRITE_SECURE_SETTINGS (SDK<33).

Common situations: Apps enabling bypass after copying sample code that sets isBypass; apps testing on Android 12 with WRITE_SECURE_SETTINGS granted via adb then moving to Android 13 where LOCATION_BYPASS is needed; system/privileged apps whose privileges were not declared in the platform's privapp-permissions whitelist.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/7fde216ba1eb6db9. Report an issue: GitHub.

Appendix: source

Thrown at play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt:76

    private var activePermissionRequest: Deferred<Boolean>? = null
    private var lastGpsLocation: Location? = null
    private var lastNetworkLocation: Location? = null

    private var currentGpsInterval: Long = -1
    private var currentNetworkInterval: Long = -1

    val deviceOrientationManager = DeviceOrientationManager(context, lifecycle) { updateLocationRequests() }

    var started: Boolean = false
        private set

    suspend fun getLastLocation(clientIdentity: ClientIdentity, request: LastLocationRequest): Location? {
        if (request.maxUpdateAgeMillis < 0) throw IllegalArgumentException()
        GranularityUtil.checkValidGranularity(request.granularity)
        if (request.isBypass) {
            val permission = if (SDK_INT >= 33) "android.permission.LOCATION_BYPASS" else Manifest.permission.WRITE_SECURE_SETTINGS
            if (context.checkPermission(permission, clientIdentity.pid, clientIdentity.uid) != PackageManager.PERMISSION_GRANTED) {
                throw SecurityException("Caller must hold $permission for location bypass")
            }
        }
        if (request.impersonation != null) {
            Log.w(TAG, "${clientIdentity.packageName} wants to impersonate ${request.impersonation!!.packageName}. Ignoring.")
        }
        val permissionGranularity = context.granularityFromPermission(clientIdentity)
        var effectiveGranularity = getEffectiveGranularity(request.granularity, permissionGranularity)
        if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(clientIdentity.packageName) && !clientIdentity.isSelfUser()) effectiveGranularity = GRANULARITY_COARSE
        val returnedLocation = if (effectiveGranularity > permissionGranularity) {
            // No last location available at requested granularity due to lack of permission
            null
        } else {
            ensurePermissions()
            val preLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis)
            val processedLocation = postProcessor.process(preLocation, effectiveGranularity, clientIdentity.isGoogle(context))
            if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) {
                // App Op denied
                null

View on GitHub (pinned to 157c9d86ac)