A worked AOSP investigation

Follow a controlled defect from symptom to source, test, repair, and evidence.

10 min readChapter 14 of 50Guided platform development
In this chapter

This bounded exercise connects a symptom to an Android 14 source function, owning module, existing test, controlled fault, and minimal repair. The source fault is injected only in a disposable local worktree and manually reverted; it does not itself alter a device image or configuration. The selected test is root-required and may build, install, and execute test code on the chosen disposable userdebug target. You will run it only if that matching test setup is available; otherwise, complete the source reasoning and mark execution not run.

Before you start#

Use a local checkout pinned to android-14.0.0_r1. Do not run this experiment in a shared worktree containing edits you do not own. This is a test-isolation lab, not a claim that a release has a production bug.

Learning goals#

  • Trace a controlled failure from observable symptom to implementation, caller/owner, and test.
  • Derive the test module and selector from the actual pinned Android.bp and GTest source.
  • Use a negative control to show that the test detects the injected regression.
  • Restore the intended implementation and distinguish test evidence from device-runtime evidence.

Practice#

1. Establish baseline and ownership

At the checkout root, record state before touching anything:

bash
repo manifest -r | head
repo status
git -C system/core status --short

Confirm the source is the requested tag (repo manifest -r saves revisions as current HEAD commit hashes; preserve that output, see the Repo tool source). Continue only if system/core is clean or every existing change is yours and documented. If not, stop; do not reset, stash, or overwrite other work.

2. Follow the symptom to the tested behavior

The controlled symptom is: an init resource written with the Android init parser's uppercase RLIM_ prefix is rejected as an unknown resource, while its lowercase spelling is accepted. This is a hypothetical regression for this exercise, not a claim about unmodified Android 14. The pinned parser strips RLIM_ (not RLIMIT_), and its test table includes RLIM_CPU with expected resource value 0.

The source trail is verifiable in the pinned checkout:

  1. rlimit_parser_test.cpp defines TEST(rlimit, RlimitSuccess) and includes RLIM_CPU; its expected success records resource 0 (the cpu resource).
  2. rlimit_parser.cpp, in android::init::ParseRlimit, removes the RLIM_ prefix and looks up a resource name using android::base::EqualsIgnoreCase against the resource table.
  3. In init/Android.bp, the cc_test module CtsInitTestCases lists rlimit_parser_test.cpp, declares require_root: true, and links libinit; rlimit_parser.cpp is part of libinit's init_common_sources. Thus the test is declared in CtsInitTestCases and exercises parser code provided by the linked library; the parser is not itself a source of the test module.
  4. The function has production callers in builtins.cpp (do_setrlimit, which calls setrlimit) and service_parser.cpp (ParseProcessRlimit, which stores a service process limit). Trace local callers (rg -n 'ParseRlimit' system/core/init) before asserting a particular runtime effect. This is init directive/service parsing; it is not an app process or kernel system call.

Run the local checks to reproduce the source trail, opening the surrounding blocks rather than treating grep output as proof:

bash
rg -n 'ParseRlimit|EqualsIgnoreCase|RLIM_CPU' system/core/init/rlimit_parser.cpp system/core/init/rlimit_parser_test.cpp system/core/init/Android.bp
rg -n 'ParseRlimit' system/core/init

The behavioral expectation comes from the test’s explicit input/output table. It is stronger than guessing from the helper name, but it proves only the parser contract covered by that test—not that every init config or boot path is correct.

To distinguish source ownership from runtime ownership, inspect the exact ParseRlimit callers and the nearby init configuration parser. Record whether a call is in production code or test-only code, then follow the configuration path that supplies the arguments. A log line that says “init” is not by itself proof that this helper rejected a particular directive; correlate its timestamp/message with the parser call and test contract. A command named init may be an executable path, while the running process is PID 1; those are related but not interchangeable facts. The test gives a controlled way to isolate the helper without rebooting a device or changing boot configuration.

Also check whether any source you inspect is generated. Here, rlimit_parser.cpp and its test are checked-in C++ sources; the cc_test declaration lists the test source directly. If your real bug instead lands in generated Java/C++/AIDL bindings, trace the Android.bp generator/module and edit the checked-in interface or generator input. Never inject a fault into a generated build output: it may be overwritten during the next build and would not test the source change you intend to contribute.

3. Run the unmodified test (baseline)

Prove the test catches the fault
  1. Baseline: EqualsIgnoreCase

    RlimitSuccess should accept the supplied uppercase resource cases. Run and record it first.

  2. Negative control: case-sensitive predicate

    After removing RLIM_, CPU should fail to match cpu; the intended assertion must detect it.

  3. Restore the original predicate

    Manually restore EqualsIgnoreCase and rerun the same test; expected pass.

  4. Inspect the final diff

    No injected defect remains; parser-test evidence still does not prove a whole boot path.

Source map. Predicted outcomes for the controlled RLIM_ exercise, not recorded Atest results. Build/setup failure is not the expected assertion failure. Do not commit the injected fault.

After selecting the pinned Cuttlefish target and confirming a matching disposable userdebug device:

bash
source build/envsetup.sh
lunch aosp_cf_x86_64_phone-userdebug
adb devices -l
SERIAL='replace-with-the-intended-disposable-cuttlefish-serial'
atest -s "$SERIAL" CtsInitTestCases:rlimit#RlimitSuccess

Replace SERIAL with the exact intended disposable device serial from adb devices -l; Atest's -s/--serial option selects that device (Atest docs). The module/test selector is derived from the tagged Android.bp and GTest TEST(rlimit, RlimitSuccess) declaration. Expected output shape, not a promised transcript: Atest resolves/builds the module, prepares the test device, runs the selected case, then reports a test summary with that case PASSED, FAILED, or a setup/infra error. Record the actual output and exit code. No result is asserted here; the lab and commands have not been executed in this environment.

If Atest cannot run due to missing device/root/test infrastructure, stop and record that exact blocker. You can continue reading and performing the controlled source fault only if you can restore your edit safely; do not represent unrun tests as results.

When classifying output, separate these cases: (a) Soong/build failure before test execution; (b) Atest cannot connect or prepare the root-required target; (c) the intended RlimitSuccess assertion reports failure after the injected code runs; (d) the method reports pass. Only (c) is evidence that the injected behavior was detected, and only (d), after restoring the source, is a repair-check pass. Save the first actionable error and the test summary; a process exit code alone cannot tell product failure from setup failure. Do not filter away the test name or runner context when collecting evidence.

4. Inject one reversible fault as a negative control

Only after a passing baseline, open system/core/init/rlimit_parser.cpp at the text_to_resources lookup. The original comparison is android::base::EqualsIgnoreCase(resource_string, entry.first). Temporarily replace only that predicate with a case-sensitive comparison:

cpp
return resource_string == entry.first;

This is the deliberate fault: after stripping RLIM_, uppercase CPU no longer matches lowercase table entry cpu. Leave all other source and test code untouched. Check the exact diff before building:

bash
git -C system/core diff --check
git -C system/core diff -- init/rlimit_parser.cpp

Run the same targeted test again:

bash
atest -s "$SERIAL" CtsInitTestCases:rlimit#RlimitSuccess

Expected failure shape: the selected case fails on an uppercase resource entry (for example, an assertion that parsing succeeded), while the failure is in the test case rather than Atest failing to find/build/prepare it. This is a prediction from the source/test inputs, not an observed output. If the test passes, the negative control did not establish the hypothesis: inspect the exact patch, selected source revision, target device/build, and test selector. If setup fails, it is not a product-level negative result.

5. Repair, rerun, and use a negative control

Restore the exact original EqualsIgnoreCase comparison manually. Do not use git checkout, git restore, or reset commands. Confirm that the intentional regression is gone and only your lab’s intended diff remains:

bash
git -C system/core diff --check
git -C system/core diff -- init/rlimit_parser.cpp

Run the same Atest selector a third time. The expected success shape is that RlimitSuccess passes; retain the actual result. Then inspect the test table’s invalid-resource cases and run the full class/module if required by your reviewer. A meaningful negative control is the temporary case-sensitive implementation failing the uppercase cases; a repair pass alone does not prove the test would catch the bug.

Do not commit the injected fault. If you began from a clean project and manually restored the original line, the project diff should be empty. If you began with known work, verify only your own path and leave unrelated changes untouched.

6. Separate parser test evidence from runtime evidence

This test exercises parser behavior; it does not boot init with a changed image or prove that an entire boot configuration behaves correctly. To make a runtime claim would require integrating the repaired source into the correct target, building the relevant image, launching the matching image on disposable Cuttlefish, and observing the behavior with matching fingerprint/logs. That larger end-to-end test is deliberately not required here.

For a production investigation, expand the evidence chain rather than generalizing: reproduce on the device, identify the process/config and source call path, confirm build ownership, find the narrow test, run a baseline, make the smallest repair, run the same test plus a negative control, then verify the runtime symptom. Keep source-only, test, build, and runtime results as separate claims.

Knowledge check#

  1. What observation makes uppercase RLIM_CPU a useful input for this regression?
  2. Why is the test failure after the temporary edit a negative control rather than a real Android defect?
  3. What does CtsInitTestCases:rlimit#RlimitSuccess encode?
  4. Does a passing parser test prove init booted successfully on Cuttlefish?
  5. What should happen if the target/test runner fails before executing the test case?

Answers#

  1. The test table includes RLIM_CPU while the implementation strips RLIM_ and compares the remaining resource name case-insensitively; changing only the comparison predicts a failure.
  2. The learner intentionally introduced the bug in a disposable local checkout. It demonstrates that the test detects a specific regression; it does not establish a defect in the pinned upstream source.
  3. Test module name from Android.bp, GTest case name, and one method name, respectively.
  4. No. It verifies assertions in the parser test only. A boot/runtime claim needs a matching built image and observed device evidence.
  5. Record it as setup/infrastructure failure, not pass or product failure; resolve the runner/device issue before interpreting test behavior.

Checkpoint#

Source-trace pass: point to the tagged parser, caller, cc_test declaration, test case/method, and expected case-insensitive behavior. Investigation pass: additionally capture actual baseline pass, injected-regression failure, exact restoration, and repair pass with commands/exit status. If not run, label each outcome not run and still complete the source reasoning; do not invent a transcript. A remaining project diff must be explained and limited to work you own.

If the test fails before entering the case, use Debugging and Contributing to distinguish test setup from product behavior. If a search result is generated or unclear, return to Finding your way in AOSP. Do not progress to runtime claims until the build and image identity are established.

Sources#

  • Android 14 rlimit_parser.cpp — ParseRlimit, RLIM_ prefix handling, and case-insensitive resource lookup.
  • Android 14 rlimit_parser_test.cpp — RlimitSuccess RLIM_CPU input/expected-result table and failure assertions.
  • Android 14 init/Android.bp — CtsInitTestCases, test sources, and root requirement.
  • Run tests (Atest) — actual GTest selector format: module, test case, and method.
  • Android 14 Cuttlefish README — matching source-era virtual device setup, HOME=$PWD ./bin/launch_cvd / ./bin/stop_cvd launch and shutdown, and host-package pairing.
  • Repo tool manifest subcommand — -r saves revisions as HEAD (moving master branch, not the Android 14 tag).
A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.