Repository navigation
docs(f8): a permission the application puts on itself, refused by a rule meant for asks - #116
Merged
Conversation
…ule meant for asks
F8-R18 requires every permission the shipped artefact requests to have a named
consumer in shipped code. It reads a request as an ask — the application wants a
capability it does not have, so it must say what uses it — and the Android
release job has now met a permission that is the opposite of that.
androidx.core both declares and requests
${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION, so after substitution
the name lands in the application's own package namespace at plain signature
level. It stops a receiver registered at runtime being reachable by other
applications on API 33 and above. Only a build signed with the same key can hold
it, and the only such build is this one. There is no consumer in this product to
name because the consumer is the shell, and refusing the artefact would refuse a
reduction in exposure rather than a request for one. F8-R18 as written mandated a
refusal that is incorrect, and the guard implementing it had no honest way to
pass.
F8-R30 is the carve-out, and it is written as three conditions that must hold
together in the merged artefact rather than as an assertion about the permission:
the artefact declares it rather than only requesting it, the name lies within the
artefact's own package namespace, and the declared protectionLevel is exactly
plain signature with no modifier. Each is legible from the built artefact. The
namespace condition is load-bearing rather than tidy — signature is measured
against the signing key of the application that declares the permission, so
self-scoping is what makes that application this one.
The refusal's second half matters more than the carve-out. A permission the
artefact declares at any level other than plain signature is refused whether or
not anything requests it, a declaration carrying no protectionLevel attribute
included: the platform reads the omission as normal, a normal permission is
granted to any application that asks, and a component protected by one is
protected against nobody while reading as a safeguard.
Nothing here exempts a dependency's permissions. The test is what a permission
grants and never where it came from; one that arrives from a dependency and
grants anything outward is F8-R18's, unchanged, and the edge-case table says so
in the row that was already there.
F8-R30 rather than F8-R29: F8-R29 was minted on 2026-09-11 for remote web-view
inspection and sits out of numeric order beside F8-R8. The new row sits beside
F8-R18 for the same reason.
Spec: F8-R30, F8-R18, GOV-R27
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Wessel Verheij <info@nightworks.io>
|
lessevv
deleted the
docs/f8-a-permission-that-grants-nothing-outward
branch
September 13, 2026 18:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



The Android release job refuses the build over
com.beatrax.mobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION— requested by theartefact and named nowhere in this product. It is right about the facts and
wrong about the conclusion, and the specification is the reason.
androidx.coreboth declares and requests${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION, so aftersubstitution the name lands in the application's own package namespace at plain
signaturelevel (protectionLevel=0x2, no modifiers). It stops a receiverregistered at runtime being reachable by other applications on API 33 and above.
Only a build signed with the same key can hold it, and the only such build is
this one. There is no consumer in this product to name because the consumer is
the Android shell — and refusing the artefact would refuse a reduction in
exposure rather than a request for one.
So
F8-R18as written mandated a refusal that is incorrect, and the guardimplementing it had no honest way to pass.
The id is
F8-R30, notF8-R29F8-R29is taken. It was minted on 2026-09-11 for remote web-view inspection(#83) and sits out of numeric order beside
F8-R8. The new row sits besideF8-R18for the same reason.The carve-out
Every condition is read out of the merged artefact, not asserted. The namespace
condition is load-bearing rather than tidy:
signatureis measured against thesigning key of the application that declares the permission, so self-scoping
is what makes that application this one, readable from the artefact instead of
assumed.
F8-R18gains the pointer back, so a reader of either finds the other:Not a provenance exemption
Said three times, in the requirement, in the prose, and in the edge-case table —
because that is the misreading that would cost something. The existing row "a
permission arriving from a dependency's manifest → given a named consumer, or
removed at merge" keeps its answer and gains "where it came from changes
nothing about the test." Three new rows cover the shape instead: the qualifying
declaration is accepted, a declaration with no
protectionLevelis refused(the platform reads the omission as
normal, which any application may hold —a component protected by one is protected against nobody), and
signaturewithany modifier is refused because the modifier widens who may hold it beyond this
build.
Process
This is the first change to exercise
GOV-R27(#115): the requirement is mintedbefore the implementing change merges, so that change can cite it in a
trailer rather than needing a record afterwards.
No decision record.
GOV-R7wants one where a competent engineer couldreasonably have chosen otherwise; here there is one correct behaviour and the
specification simply failed to describe it. ADR-0034 recorded a genuine choice
between rejecting a release, rewriting history and accepting a record. This is a
mis-scoped rule corrected against measured evidence.
The version manifest is untouched —
F8-R29is not a v2.0.0 goal either, andadding one is a goals change (
GOV-R21).Gates run locally:
integrity.py,manifest_check.py,commit_lint.py,dco_check.py,typos,markdownlint-cli2,lychee— all clean.Spec: F8-R30, F8-R18, GOV-R27