CVE-2024-0044 in 60 seconds

Android keeps a compact file named packages.list so native tools can look up application identities. During an ADB-driven install, CVE-2024-0044 allowed attacker-controlled installer metadata to contain the separators used by that file. A crafted value could therefore create a forged-looking package record. The privileged run-as utility later trusted the record and could be misled into assuming another application’s identity.

The attack path was:

ADB shell access
  -> install a package with a crafted installer name
  -> inject record structure into packages.list
  -> make run-as parse a forged application identity
  -> cross another app's data boundary within the remaining UID and SELinux limits

This was a local elevation-of-privilege path, not a remote drive-by attack and not an automatic route to Android’s system UID. It required the ability to reach the relevant ADB/shell installation path. The exact result also depended on the device build, target application, and the controls that run-as and SELinux still enforced.

What are packages.list and run-as?

Android’s Package Manager owns the rich package database. It also writes selected fields into packages.list, a line- and space-delimited projection for native consumers. Each line is data, but its separators are grammar: a newline starts another record and spaces divide identity fields.

run-as is a privileged debugging utility. For an eligible debuggable app, it uses package metadata to enter that app’s execution context. It therefore needs an authoritative answer to “which UID, data directory, SELinux inputs, and debuggability state belong to this package?” In the vulnerable flow, part of that answer came from a text projection whose record structure an installer-controlled value could change.

What happened?

An installer package name supplied through an ADB installation path was escaped in packages.xml but not equivalently protected when written to packages.list. A value that looked like ordinary metadata at the API boundary became record syntax at serialization time. run-as then interpreted the resulting fields as identity data.

The first AOSP correction introduced a real package-name validator and applied it to params.installerPackageName. A later patch applied the same validator to a separate installerPackageName method argument before Android selected between the two. In other words, one semantic value had two producers, but only one crossed the first new check. The follow-up closed that producer gap and added the android.security.cts.CVE_2024_0044 regression test.

Who was actually affected?

Google’s October 2024 Android bulletin lists Android 12, 12L, 13, 14, and 15 and classifies the issue as a High-severity Framework elevation of privilege. Devices carrying the applicable platform fix at security patch level 2024-10-01 or later address this bulletin entry; OEM backports still need to be evaluated against the device’s actual branch and patch level.

Exposure did not mean that any website or ordinary app could immediately invoke the chain. A useful assessment asks whether an attacker could obtain the required ADB/shell installation position, whether the build contained the vulnerable producer path, and what identity the remaining run-as and SELinux checks would permit.

What I verified

  • The initial AOSP changes replace length-only handling with a package-name syntax check for params.appPackageName and params.installerPackageName.
  • That first change still allows the selected installer identity to fall back to the separate installerPackageName method argument without applying the new validator to that argument.
  • The later AOSP change validates both inputs before choosing between them and names the regression test android.security.cts.CVE_2024_0044.
  • The Android October 2024 bulletin classifies CVE-2024-0044 as a High-severity Framework elevation of privilege and lists updated AOSP versions 12, 12L, 13, 14, and 15.
  • The current CVE record describes the flaw in createSessionInternal() as improper input validation that can allow run-as to assume another app identity.
  • This is static reproduction of the data flow and patch logic. I verified neither exploitability on a specific OEM build nor in-the-wild exploitation.

Deep dive: the value crossed three different trust domains

The vulnerable path is easier to understand as a sequence of format and authority changes:

  1. an ADB shell caller supplies installer metadata during a package installation;
  2. PackageInstallerService accepts that value as a package name;
  3. Package Manager serializes package metadata into packages.list;
  4. run-as parses the record and uses its fields to make an application-identity decision.

The value begins as a Java string. It becomes syntax when written into a delimiter-based record. It becomes authority when a privileged consumer treats the parsed fields as the UID, debuggability, SELinux inputs, and data directory of an application.

IDENTITY DATA BECOMES RECORD SYNTAX ADB installcaller-controlled name Package installervalidation boundary packages.listline-delimited record run-as parserreads identity fields App identityprivileged decision A field that is safe as an opaque value can be unsafe as grammar, then security-critical as identity.
The dangerous transition is not string to string. It is untrusted value to record grammar to privileged identity.

Meta Red Team X documented the format mismatch precisely: special characters were escaped in packages.xml, but not in packages.list. That difference matters because a defense in one serialization format says nothing about another format with different delimiters and consumers.

run-as trusted a projection, not the package database

packages.list is a projection of selected package metadata. It exists so native consumers can read a compact record without loading Package Manager’s richer internal state. That convenience creates a trust question: which fields can the consumer safely accept from the projection, and which should it derive independently?

The public research shows that run-as obtained the target package’s debuggability, UID-related metadata, SELinux inputs, and data-directory path from this record. The tool did retain additional checks: it would not assume non-application UIDs, and its SELinux transition did not simply copy a privileged application’s original domain. Those controls limited the outcome, but they did not restore the violated application-data boundary.

This distinction is important for severity analysis. “Run as any app” does not mean “become the Android system UID with every original SELinux permission.” It means the forged record could make the tool authorize an application context it should have rejected, exposing data and capabilities available from that resulting context. The exact impact still depends on Android version, build, target application, and the remaining run-as and SELinux controls.

The first patch fixed the visible input

The initial commits, 65bd134b0a82 and 954b2874b85b, are branch-specific forms of the same security change. They add an isValidPackageName() helper that enforces the maximum length and uses Android’s package parser to reject invalid name syntax. They then apply it to two values carried in the session parameters:

if session.appName is invalid: clear it
if session.requestedInstallerName is invalid: clear it

selectedInstallerName =
  session.requestedInstallerName ?? installerNameArgument

That is a meaningful improvement. A delimiter is not a valid package-name character, so the documented injection form no longer survives through the validated session parameter.

The control-flow shape nevertheless leaves an asymmetry. The fallback installerNameArgument reaches selectedInstallerName without the same check. The variable names make the values look interchangeable; the validation coverage shows they were not yet equivalent.

FIRST PATCH: UNEVEN COVERAGE params.installerPackageNamerequested identity syntax checkinvalid → null installerPackageNameseparate argument selected installerone semantic identity The sink does not care which producer supplied the value. The security boundary has to care.
The first check covers the preferred input, while the fallback still joins the same trusted value.

I treat “the first patch left a second path open” as a patch-diff conclusion, not as a claim that I reproduced an exploit against every March-patched build. The evidence is the unchanged fallback in the first diff, followed by a later same-CVE patch that validates that exact fallback and adds a CVE-specific regression test.

The October patch closes the producer gap

Commit 836750619a8b changes the comment on the existing check to distinguish the requested installer package name. It then adds the same validity check for the separate method argument before the null-coalescing selection occurs:

validate session.requestedInstallerName
validate installerNameArgument
selectedInstallerName = requested ?? argument

The important property is not that two if statements now appear together. It is that every value that can become selectedInstallerName must satisfy the same package-name grammar first. The security invariant now follows the semantic value across both producers.

The October bulletin places CVE-2024-0044 in the 2024-10-01 Framework group and lists Android 12, 12L, 13, 14, and 15 as updated AOSP versions. That current vendor scope is broader than the Android 12 and 13 attack scenario in the original March write-up. The two statements answer different questions: the research describes the versions on which its published chain was demonstrated; the current bulletin identifies platform branches receiving the applicable CVE correction. I use the vendor’s current 12–15 range for the frontmatter and do not extrapolate the original exploit chain to every listed version.

Safe static reproduction

The central claim can be checked without installing a package or constructing a delimiter payload. Use the public diffs as a bounded control-flow exercise:

  1. In either initial commit, identify the two inputs to the expression that selects the installer package name.
  2. Mark which input passes through isValidPackageName() before that expression.
  3. In the follow-up commit, repeat the marking exercise.
  4. Confirm that both producers are validated before the selection in the later version.
  5. Confirm that the later commit names a CVE-specific Android security regression test.

This reproduces the validation-coverage failure and its correction. It does not reproduce record injection, an unauthorized run-as transition, or access to application data.

Negative controls

The negative controls are values and paths that should not be conflated with the vulnerable case:

  • A syntactically valid installer package name remains accepted. If all installer attribution is cleared, the fix is broader than the published diff.
  • An invalid name arriving through params.installerPackageName is rejected by the initial and follow-up versions. This distinguishes the original repaired path from the later producer gap.
  • An invalid name arriving through the separate installerPackageName argument is rejected only after the follow-up change. This is the discriminating control for patch coverage.
  • A value safely represented in packages.xml does not prove it is safe in packages.list; each serializer and downstream parser is a separate control surface.
  • A device reporting an October patch date is not automatically equivalent to an AOSP branch. OEM backports must be assessed through the device’s published bulletin or branch evidence.

Evidence matrix

ClaimDirect evidenceWhat it does not proveConfidence
Installer metadata could alter packages.list record structureMeta Red Team X traces unsanitized installer input to the newline- and space-delimited fileExploitability on every OEM buildHigh
run-as consumes security-relevant identity fields from that fileOriginal research links the parser to debuggability, UID, SELinux inputs, and data pathUnrestricted system UID or original privileged SELinux domainHigh
The initial fix validates session-carried package namesAOSP commits 65bd134b0a82 and 954b2874b85b add and call isValidPackageName()Equivalent validation of every producerHigh
A separate fallback remained outside the first new checkInitial diffs select installerPackageName when the requested value is nullA weaponized bypass on every patched branchHigh
The later fix validates both producersAOSP commit 836750619a8b validates the argument before selectionBehavior of OEM code that diverges from AOSPHigh
The applicable vendor patch level is October 2024Android bulletin maps the CVE to the 2024-10-01 Framework group and versions 12–15Whether a specific device actually received the required backportHigh
Exploitation occurred in the wildNo primary source reviewed here confirms in-the-wild exploitationAbsence of private or undisclosed activityLimited

The durable fix is an invariant, not a filter location

The October change closes both producers visible at the selection point. That is the immediate patch property. The architecture still suggests two defense-in-depth questions.

First, should a delimiter-based security record ever accept raw strings from multiple upstream callers? A safe serializer should either reject record syntax centrally or encode every field in a format whose parser cannot reinterpret data as structure. Validating package-name grammar is strong here because the business object is already supposed to be a package name; generic escaping would need equally careful agreement between every writer and reader.

Second, should run-as trust reconstructible identity fields from a flat projection? The original research notes that at least some data-path information can be derived from trusted identifiers. A consumer that reconstructs or cross-checks security-critical fields reduces the authority of a single serialized record. That is defense in depth, not a claim about changes present in the three patches analyzed here.

The reusable review rule is simple:

When several inputs can become one trusted value, validate the value after convergence—or prove that every producer enforces the same invariant before it joins.

Defender and reviewer takeaways

  • Use the Android security patch level, vendor bulletin, and OEM backport evidence together. For this CVE, 2024-10-01 or later is the applicable AOSP bulletin level.
  • Do not use “Android 14 was not exploitable in the original public scenario” as a substitute for the current vendor patch scope. Defense-in-depth changes and variant paths can change that scope.
  • In patch review, search both for the named field and for the destination variable that eventually carries it. The second search is what reveals alternate producers.
  • Treat every human-readable metadata export consumed by privileged native code as a protocol. Define its grammar, escaping, versioning, and trust model accordingly.
  • Keep public validation non-destructive. The two-diff control-flow comparison is sufficient to verify the patch lesson without touching app data or operational devices.

Research limits and credit

Tom Hebb of Meta Red Team X discovered and reported the original issue, documented the exploitation chain, and published the associated advisory. Google assigned CVE-2024-0044 and the Android team authored the public fixes. I claim neither discovery nor exploit reproduction.

My contribution is the independent comparison of the initial and follow-up AOSP changes, the producer-coverage model, and the evidence boundaries recorded in this casefile. Public source review cannot establish OEM-specific behavior, the contents of private Android bug records, or in-the-wild exploitation. Those remain outside the claim set.

My conclusion

CVE-2024-0044 is not only a newline-injection story. It is a warning about semantic convergence. Android had two values that could become the installer identity. The first fix enforced the right grammar on one. The later fix applied the same invariant to the other before either could reach the shared decision.

The package name was not dangerous because it was a string. It became dangerous when one component treated it as record syntax and another treated the resulting record as identity authority. The patch history is valuable because it shows exactly where a local input check ends—and where a trust boundary review has to begin.

Sources

Sources & limits

Evidence used for this analysis

Sources checkedAugust 27, 2026

Version and exploitation status can change; follow the linked vendor records.

Review statusPublic sources reviewed

Primary public records were checked. Environment-specific behavior remains outside the claim unless separately reproduced.

◈ Cite This ResearchBibTeX · Markdown

Reference this analysis, root-cause teardown, or vulnerability discovery in your technical reports or academic publications:

@misc{jankesec_android_package_name_forgery_cve_2024_0044_2026,
  author       = {Sevban D\"{o}nmez},
  title        = {CVE-2024-0044: How Installer Metadata Forged an Android App Identity},
  year         = {2026},
  howpublished = {\url{https://jankesec.com/research/android-package-name-forgery-cve-2024-0044/}},
  note         = {jankesec technical security research (CVE-2024-0044)}
}