Skip to content

playscan: 64-bit ABI parity should compare libraries, not just ABI directories #23

Description

@ethanzhoucool

Background

greenlight playscan --apk / --aab checks Google Play's 64-bit requirement: an app that ships native code for a 32-bit ABI must also ship the matching 64-bit ABI.

The check currently works at ABI-directory granularity. It collects the set of ABIs present in the archive and flags armeabi-v7a with no arm64-v8a, or x86 with no x86_64.

internal/playscan/archive.go:157 (check64BitParity):

present := make(map[string]bool)
for _, lib := range libs {
    present[lib.ABI] = true
}
for abi32, abi64 := range abi64For {
    if present[abi32] && !present[abi64] {
        // flag
    }
}

The gap

A partial gap passes clean. This archive is reported as fine:

lib/armeabi-v7a/libfoo.so
lib/armeabi-v7a/libbar.so
lib/arm64-v8a/libfoo.so      <- libbar.so has no 64-bit build

arm64-v8a exists, so the ABI-level check is satisfied. But on a 64-bit-only device System.loadLibrary("bar") throws UnsatisfiedLinkError at runtime. This is a common real-world shape: one native dependency in the tree gets built for fewer ABIs than the rest.

What to change

Compare the set of library basenames in each 32-bit ABI against its 64-bit counterpart, and report the names that are missing.

Suggested behaviour:

  • Whole ABI missing: keep the current CRITICAL. Play rejects this at upload.
  • Some libraries missing: report at HIGH, and name them. Play's automated rejection is less certain here, but it is a runtime crash on 64-bit-only devices.
  • List the specific missing basenames in Detail so the fix is obvious. Sort them, since map iteration order is random and findings should be deterministic.

Everything you need is local to check64BitParity. nativeLib already carries Path and ABI, and path.Base gives the basename.

Acceptance criteria

  • armeabi-v7a/{libfoo,libbar}.so + arm64-v8a/libfoo.so produces a HIGH finding naming libbar.so.
  • armeabi-v7a/libfoo.so + arm64-v8a/libfoo.so stays clean.
  • armeabi-v7a with no arm64-v8a at all stays CRITICAL.
  • 64-bit-only, and no native code at all, both stay clean.
  • Config splits stay exempt. ScanArchive already skips this rule when the manifest sets android:split, since a split legitimately carries one ABI. Do not remove that guard.
  • Findings are deterministic across runs.

Testing

Tests live in internal/playscan/archive_test.go. TestScanArchive64BitParity at line 854 is the existing table and is the natural place to add cases. buildTestAPK (line 311) builds an APK fixture from a map of entry path to bytes, and buildTestELF(align, relro) produces a valid ELF, so no real APK or Android SDK is needed:

apk := buildTestAPK(t, map[string][]byte{
    "lib/armeabi-v7a/libfoo.so": buildTestELF(16384, true),
    "lib/armeabi-v7a/libbar.so": buildTestELF(16384, true),
    "lib/arm64-v8a/libfoo.so":   buildTestELF(16384, true),
})

Run with:

go test ./internal/playscan/
gofmt -l internal/    # CI gates on this

Notes for a first-time contributor

This is self-contained: one function, one test file, no new dependencies, and the fixture helpers already exist. internal/playscan/archive.go is worth skimming end to end first, it is short. Comments in this codebase explain why rather than restating the code, so a line on why partial gaps are HIGH rather than CRITICAL is worth more than a line describing the loop.

Happy to answer questions in this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions