---
title: "CVE-2024-0044: How Installer Metadata Forged an Android App Identity"
description: "A reader-first variant analysis of Android CVE-2024-0044: how a crafted installer name could corrupt packages.list, mislead run-as, and why the first fix needed a follow-up."
date: 2026-08-26
analysisType: "Variant Analysis"
cves: ["CVE-2024-0044"]
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/research/android-package-name-forgery-cve-2024-0044/
---

## 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:

```text
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.

<figure class="diagram">
<svg viewBox="0 0 760 326" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="An ADB-supplied installer name passes through PackageInstallerService into a line-oriented packages list record. Run-as parses that record and uses it for an application identity decision.">
<defs><marker id="and-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="24" class="dg-crit">IDENTITY DATA BECOMES RECORD SYNTAX</text>
<rect x="0" y="52" width="150" height="70" rx="8" class="dg-box" />
<text x="16" y="80" class="dg-label">ADB install</text><text x="16" y="102" class="dg-muted">caller-controlled name</text>
<line x1="152" y1="87" x2="204" y2="87" class="dg-line" marker-end="url(#and-a)" />
<rect x="208" y="52" width="184" height="70" rx="8" class="dg-box-crit" />
<text x="224" y="80" class="dg-label">Package installer</text><text x="224" y="102" class="dg-crit">validation boundary</text>
<line x1="394" y1="87" x2="446" y2="87" class="dg-line" marker-end="url(#and-a)" />
<rect x="450" y="52" width="158" height="70" rx="8" class="dg-box-crit" />
<text x="466" y="80" class="dg-label">packages.list</text><text x="466" y="102" class="dg-muted">line-delimited record</text>
<line x1="529" y1="124" x2="529" y2="182" class="dg-line dg-crit-line" marker-end="url(#and-a)" />
<rect x="450" y="188" width="158" height="70" rx="8" class="dg-box" />
<text x="466" y="216" class="dg-label">run-as parser</text><text x="466" y="238" class="dg-muted">reads identity fields</text>
<line x1="448" y1="223" x2="394" y2="223" class="dg-line" marker-end="url(#and-a)" />
<rect x="208" y="188" width="184" height="70" rx="8" class="dg-box-crit" />
<text x="224" y="216" class="dg-label">App identity</text><text x="224" y="238" class="dg-crit">privileged decision</text>
<text x="0" y="304" class="dg-muted">A field that is safe as an opaque value can be unsafe as grammar, then security-critical as identity.</text>
</svg>
<figcaption>The dangerous transition is not string to string. It is untrusted value to record grammar to privileged identity.</figcaption>
</figure>

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:

```text
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.

<figure class="diagram">
<svg viewBox="0 0 760 318" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The first patch validates the requested installer package name in session parameters, but a separate installer package name argument can still reach the shared selection point without that validation.">
<defs><marker id="and-b" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker></defs>
<text x="0" y="24" class="dg-crit">FIRST PATCH: UNEVEN COVERAGE</text>
<rect x="0" y="52" width="220" height="68" rx="8" class="dg-box" />
<text x="16" y="80" class="dg-label">params.installerPackageName</text><text x="16" y="102" class="dg-muted">requested identity</text>
<line x1="222" y1="86" x2="286" y2="86" class="dg-line" marker-end="url(#and-b)" />
<rect x="290" y="52" width="154" height="68" rx="8" class="dg-box" />
<text x="306" y="80" class="dg-label">syntax check</text><text x="306" y="102" class="dg-ok">invalid → null</text>
<line x1="446" y1="86" x2="516" y2="146" class="dg-line" marker-end="url(#and-b)" />
<rect x="0" y="198" width="220" height="68" rx="8" class="dg-box-crit" />
<text x="16" y="226" class="dg-label">installerPackageName</text><text x="16" y="248" class="dg-crit">separate argument</text>
<line x1="222" y1="232" x2="516" y2="172" class="dg-line dg-crit-line" marker-end="url(#and-b)" />
<rect x="520" y="126" width="208" height="74" rx="8" class="dg-box-crit" />
<text x="536" y="154" class="dg-label">selected installer</text><text x="536" y="176" class="dg-muted">one semantic identity</text>
<text x="0" y="302" class="dg-muted">The sink does not care which producer supplied the value. The security boundary has to care.</text>
</svg>
<figcaption>The first check covers the preferred input, while the fallback still joins the same trusted value.</figcaption>
</figure>

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:

```text
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

| Claim | Direct evidence | What it does not prove | Confidence |
| --- | --- | --- | --- |
| Installer metadata could alter `packages.list` record structure | Meta Red Team X traces unsanitized installer input to the newline- and space-delimited file | Exploitability on every OEM build | High |
| `run-as` consumes security-relevant identity fields from that file | Original research links the parser to debuggability, UID, SELinux inputs, and data path | Unrestricted `system` UID or original privileged SELinux domain | High |
| The initial fix validates session-carried package names | AOSP commits `65bd134b0a82` and `954b2874b85b` add and call `isValidPackageName()` | Equivalent validation of every producer | High |
| A separate fallback remained outside the first new check | Initial diffs select `installerPackageName` when the requested value is null | A weaponized bypass on every patched branch | High |
| The later fix validates both producers | AOSP commit `836750619a8b` validates the argument before selection | Behavior of OEM code that diverges from AOSP | High |
| The applicable vendor patch level is October 2024 | Android bulletin maps the CVE to the 2024-10-01 Framework group and versions 12–15 | Whether a specific device actually received the required backport | High |
| Exploitation occurred in the wild | No primary source reviewed here confirms in-the-wild exploitation | Absence of private or undisclosed activity | Limited |

## 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

- [CVE.org: CVE-2024-0044 record](https://www.cve.org/CVERecord?id=CVE-2024-0044)
- [Android Security Bulletin — October 2024](https://source.android.com/docs/security/bulletin/2024-10-01)
- [AOSP initial fix, Android branch: `65bd134b0a82`](https://android.googlesource.com/platform/frameworks/base/+/65bd134b0a82c51a143b89821d5cdd00ddc31792)
- [AOSP initial fix, later branch: `954b2874b85b`](https://android.googlesource.com/platform/frameworks/base/+/954b2874b85b6cd0d6bb12cd677cdf22e5dbd77b)
- [AOSP follow-up fix: `836750619a8b`](https://android.googlesource.com/platform/frameworks/base/+/836750619a8bce0bf78fe0549f9990e294671563)
- [Meta Red Team X: Android `run-as` forgery research](https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html)
- [Meta Red Team external disclosure: CVE-2024-0044](https://github.com/metaredteam/external-disclosures/security/advisories/GHSA-m7fh-f3w4-r6v2)