Skip to content

docs(f8): a permission the application puts on itself, refused by a rule meant for asks - #116

Merged
lessevv merged 1 commit into
mainfrom
docs/f8-a-permission-that-grants-nothing-outward
Sep 13, 2026
Merged

lessevv merged 1 commit into
mainfrom
docs/f8-a-permission-that-grants-nothing-outward

Conversation

@lessevv

@lessevv lessevv commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

The Android release job refuses the build over
com.beatrax.mobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION — requested by the
artefact and named nowhere in this product
. It is right about the facts and
wrong about the conclusion, and the specification is the reason.

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 (protectionLevel=0x2, no modifiers). 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 Android shell — and refusing the artefact would refuse a reduction in
exposure rather than a request for one.

So F8-R18 as written mandated a refusal that is incorrect, and the guard
implementing it had no honest way to pass.

The id is F8-R30, not F8-R29

F8-R29 is 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 beside
F8-R18 for the same reason.

The carve-out

F8-R30 | A permission the shipped artefact requests satisfies F8-R18
without a named consumer in shipped code only where all three hold in the
merged artefact: the artefact declares the permission rather than only
requesting it; the name lies within the artefact's own package namespace; and
its declared protectionLevel is exactly plain signature, carrying no
modifier. Such a permission grants nothing outward — only a build signed with
the same key can hold it — so refusing it would refuse a reduction in exposure
rather than a request for one. A permission the artefact declares at any other
protection level MUST be refused whether or not anything requests it, a
declaration omitting the attribute included: the platform reads an omitted
protectionLevel as normal, which any application may hold. The test is
what the permission grants and never where it came from — one that arrives
from a dependency and grants anything outward is F8-R18's, unchanged.

Every condition is read out of the merged artefact, not asserted. 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, readable from the artefact instead of
assumed.

F8-R18 gains the pointer back, so a reader of either finds the other:

… verified against the merged artefact rather than the source manifest. The
one exception is a permission the artefact declares to protect its own
components, which grants nothing outward and which F8-R30 defines
exhaustively.

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 protectionLevel is refused
(the platform reads the omission as normal, which any application may hold —
a component protected by one is protected against nobody), and signature with
any 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 minted
before the implementing change merges, so that change can cite it in a
trailer rather than needing a record afterwards.

No decision record. GOV-R7 wants one where a competent engineer could
reasonably 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-R29 is not a v2.0.0 goal either, and
adding 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

…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>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions github-actions Bot added the awaiting-maintainer Green PR with no review — a maintainer needs to act label Sep 13, 2026
@lessevv
lessevv merged commit e25234e into main Sep 13, 2026
13 checks passed
@lessevv
lessevv deleted the docs/f8-a-permission-that-grants-nothing-outward branch September 13, 2026 18:18
@github-actions github-actions Bot added awaiting-maintainer Green PR with no review — a maintainer needs to act and removed awaiting-maintainer Green PR with no review — a maintainer needs to act labels Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-maintainer Green PR with no review — a maintainer needs to act documentation Docs only functional

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant