Skip to content

Carry the accessibility service count the dump states - #946

Merged
besendorf merged 4 commits into
mvt-project:mainfrom
va-resident:fix/accessibility-installed-service-count
Sep 25, 2026
Merged

besendorf merged 4 commits into
mvt-project:mainfrom
va-resident:fix/accessibility-installed-service-count

Conversation

@va-resident

Copy link
Copy Markdown
Contributor

Fixes #744

Two halves of the same parser, in the order they matter.

Change, artifacts/dumpsys_accessibility.py and modules/bugreport/dumpsys_accessibility.py:

  1. Parse installedServiceCount=N per user. For a user whose services the dump did not list,
    append one record with component / package_name / service_name None and
    installed_service_count=N, and raise it as LOW — a count is a coverage statement, not a
    running service, so it must not compete with one the dump says is enabled. The module's summary
    line reports how many services the dump stated but did not name, so a stated count can no
    longer read as "a total of 0". (You said "at least informational if not higher, could even be
    low" — this is LOW. One line and one assertion if you want it back at informational.)
  2. Name the state in the alert (installed, not enabled / enabled / enabled and bound /
    installed, enabled state not stated) and split its severity, as you asked on this issue: a
    service the dump states is switched off is LOW, enabled or bound is MEDIUM, and a
    dump that does not state the enabled state stays MEDIUM — "not stated" is not "not enabled".
    IOC matching is untouched by the state: check_app_id() runs before any of this and still
    returns CRITICAL for a disabled service, which a new test pins.
  3. A state section the dump never printed yields None instead of False, so "not stated" is not
    recorded as "not enabled". Bound services:{} (printed, empty) stays False.

Both record shapes — a service record and a count-only record — are built from one _FIELDS list,
so they cannot drift apart.

Measured on 50 bug reports (40 that state a count without listing, 10 that list), upstream vs
patched, running the artifact code directly:

upstream patched
records 40 80
records with a component 40 40
MEDIUM alerts 40 4
LOW alerts 0 76

The 40 new records land one per report on the 40 that produced nothing before. No record is
dropped and no IOC match changes; what moves is the severity of the switched-off services.

Over the wider set of 163 reports the same run gives MEDIUM 305 → 4 and LOW 0 → 407 (301
switched-off services plus 106 stated counts). The 4 records that stay MEDIUM are the only ones
in the whole set with enabled=True
— both belong to the same device, and both are the
accessibility services of an app that is malicious. Every other accessibility record in 163 bug
reports is a service that is installed and switched off.

Tests: two new files — a stated count with no listed services produces one record and one LOW
alert, a listed service is unchanged, two users are kept apart, a zero count adds nothing; an
absent section reads None while a printed-empty one reads False, and the alert text carries
the state.

Two existing assertions change, both in test_artifact_dumpsys_accessibility.py: the v14 fixture
never prints an installed services: section, so installed becomes None instead of False;
and every service in the Samsung fixture is installed-and-off, so test_ioc_check's three
non-IOC findings are LOW rather than MEDIUM (the IOC match itself is unchanged).

Applies to main@777db67 with no other change; pytest tests/android there goes from 99 passed /
1 skipped to 110 passed / 1 skipped; ruff check, ruff format --check and
mypy --platform linux (156 files) pass on the branch.

Related open work (checked 2026-09-22):

  • Show which accessibility services are enabled #773 "Show which accessibility services are enabled" (open since 2026-05-04, for this issue)
    is the only other work on this file. Its diff is against the pre-008844b parser that no longer
    exists in main, it defaults enabled to False, and it does not touch the count. This PR
    covers the same ground on the current parser; which of the two to take is yours to decide, and I
    have said so on Show which accessibility services are enabled #773 rather than leaving it unmentioned.
  • Run extraction modules in parallel #839 "Run extraction modules in parallel" rewrites modules/bugreport/dumpsys_accessibility.py's
    __init__; this PR only edits run(), so they should not collide.
  • Add versioned output schemas #885 "Add versioned output schemas" gives every module an output_model; the new count-only
    record is a new shape in this artifact's output and would need to be in that schema.

Most builds never print the `installed services: {...}` block. They state
installedServiceCount=N in the user's attributes line and list nothing, so the
parser recorded no service at all and MVT logged "Identified a total of 0
accessibility services" — an artifact that says "no accessibility services"
about a dump that said there are five. The dump pasted in mvt-project#744 is itself an
example: it states installedServiceCount=6 and names none.

Parse the count per user and, for a user whose services the dump did not list,
keep one record carrying it, raised as LOW: a count is a coverage statement,
not a running service.

Name the service state in the alert and split its severity, as asked on mvt-project#744:
a service the dump states is switched off is LOW, enabled or bound is MEDIUM,
and a dump that does not state the enabled state stays MEDIUM, because "not
stated" is not "not enabled". A state section the dump never printed now reads
None rather than False. IOC matching is unaffected by the state: a disabled
service still returns CRITICAL on a match.

Measured over 163 bug reports carrying an accessibility section: MEDIUM alerts
305 -> 4, LOW 0 -> 407. The four that stay MEDIUM are the only records in the
set with enabled=True.

Fixes mvt-project#744

@besendorf besendorf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reproduced two issues on commit bdbc07a that should be fixed before merge:

  1. Track state-section presence per user. In the normalization loop, seen_states is global. With user 0 listing one installed service but no enabled services section and user 10 listing one enabled service, user 0 is incorrectly assigned enabled=False and gets a LOW installed, not enabled alert. User 10 is also incorrectly assigned installed=False. Each user needs its own set of printed sections; please test a multiuser dump with named services and different sections.

  2. Report the unnamed part of a partial count. This condition suppresses the count-only record once any component was named. The existing Android 14 fixture states installedServiceCount=2 but names only one enabled component. Parsing yields one service record, no count coverage alert, and the summary says one identified with zero more unnamed. Please account for the per-user difference between the stated count and distinct named components, and add a test for that fixture.

The first case incorrectly downgrades an unknown enabled state; the second makes an incomplete service listing look complete.

@va-resident

va-resident commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks, both fixed in ad6caa1.

  1. State sections are now tracked per user. A section printed for one user no longer affects another user. Your example is now a test (test_printed_sections_are_tracked_per_user): user 0 gets enabled=None instead of False, so no more "installed, not enabled" alert, and user 10 gets installed=None.

  2. Partial counts are reported. If a user's stated count is higher than the number of services the dump names, the rest gets a count-only record and a LOW alert. The Android 14 fixture (count 2, one named) now gives one service record plus one count-only record. I updated the two existing tests for it and added a new one.

This adds one new field, unnamed_service_count. If you'd prefer not to change the output shape, I can remove it and compute the difference instead.

Lint, mypy and tests pass.

Two issues from review:

* The set of printed state sections was global, so a section printed for
  one user decided the flags of another. A user with an installed list and
  no enabled section was marked enabled=False and got a LOW "installed, not
  enabled" alert. Sections are now tracked per user.

* A count-only record was added only when a user had no named component at
  all. A dump stating installedServiceCount=2 and naming one service read
  as complete. The count-only record is now added whenever the stated count
  exceeds the distinct named components for that user, and carries the
  difference in a new unnamed_service_count field. The module summary adds
  up that remainder.
@va-resident
va-resident force-pushed the fix/accessibility-installed-service-count branch from ec7d03b to ad6caa1 Compare September 25, 2026 16:55
@besendorf
besendorf merged commit 9c3db57 into mvt-project:main Sep 25, 2026
7 checks passed
2B-4G10 pushed a commit to 2B-4G10/MVT-GUI that referenced this pull request Oct 28, 2026
…-installed-service-count

Carry the accessibility service count the dump states
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Accessibility services: Show which services are enabled

2 participants