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#
- C/C++ source reading — function calls, strings, conditionals, and GTest names.
- Linux files and processes — process identity and filesystem paths.
- Find your way around AOSP — Repo, source-to-module navigation, and generated files.
- Build and run AOSP — Android 14
userdebugtarget and matching Cuttlefish setup. - Debugging and contributing — Atest, logs, and branch safety.
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.bpand 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:
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:
rlimit_parser_test.cppdefinesTEST(rlimit, RlimitSuccess)and includesRLIM_CPU; its expected success records resource0(thecpuresource).rlimit_parser.cpp, inandroid::init::ParseRlimit, removes theRLIM_prefix and looks up a resource name usingandroid::base::EqualsIgnoreCaseagainst the resource table.- In
init/Android.bp, thecc_testmoduleCtsInitTestCaseslistsrlimit_parser_test.cpp, declaresrequire_root: true, and linkslibinit;rlimit_parser.cppis part oflibinit'sinit_common_sources. Thus the test is declared inCtsInitTestCasesand exercises parser code provided by the linked library; the parser is not itself a source of the test module. - The function has production callers in
builtins.cpp(do_setrlimit, which callssetrlimit) andservice_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:
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)
- Baseline: EqualsIgnoreCase
RlimitSuccess should accept the supplied uppercase resource cases. Run and record it first.
- Negative control: case-sensitive predicate
After removing RLIM_, CPU should fail to match cpu; the intended assertion must detect it.
- Restore the original predicate
Manually restore EqualsIgnoreCase and rerun the same test; expected pass.
- Inspect the final diff
No injected defect remains; parser-test evidence still does not prove a whole boot path.
After selecting the pinned Cuttlefish target and confirming a matching disposable userdebug device:
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:
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:
git -C system/core diff --check
git -C system/core diff -- init/rlimit_parser.cpp
Run the same targeted test again:
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:
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#
- What observation makes uppercase
RLIM_CPUa useful input for this regression? - Why is the test failure after the temporary edit a negative control rather than a real Android defect?
- What does
CtsInitTestCases:rlimit#RlimitSuccessencode? - Does a passing parser test prove init booted successfully on Cuttlefish?
- What should happen if the target/test runner fails before executing the test case?
Answers#
- The test table includes
RLIM_CPUwhile the implementation stripsRLIM_and compares the remaining resource name case-insensitively; changing only the comparison predicts a failure. - 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.
- Test module name from
Android.bp, GTest case name, and one method name, respectively. - No. It verifies assertions in the parser test only. A boot/runtime claim needs a matching built image and observed device evidence.
- 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—RlimitSuccessRLIM_CPUinput/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_cvdlaunch and shutdown, and host-package pairing. - Repo tool
manifestsubcommand —-rsaves revisions as HEAD (moving master branch, not the Android 14 tag).