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.

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)

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.