HTC Desire Z GPS: the Ghidra fix that landed 235 km away

For one night, this looked like the best result of the whole project. My HTC Desire Z, the 2010 Android slider that by then had its camera button wired to a waypoint logger, had spent hours by a window failing to find itself. Then I opened its 60 KB GPS library in Ghidra, found a switch that throws away "intermediate" positions, turned the switch on and rebooted. Two seconds after I pressed the camera button, a coordinate landed in the waypoint file. Six decimal places. Accuracy: zero metres.

I celebrated. I would like that on the record, because the rest of this post is the correction.

The goal was modest: a GPS tracker of my own on CyanogenMod 7, the community build of Android 2.3, that writes down where I am whenever I press the camera button, from any screen. Every location request on this phone ends up in a HAL (hardware abstraction layer), the native library Android's location service calls to talk to the GPS engine. Here that library is gps.vision.so, a prebuilt, stripped 32-bit ARM binary of 60,624 bytes with no symbols. Ghidra, the NSA's open-source reverse-engineering suite, was the only way to ask it what it does with a position.

GPS by the window: one real fix, then nothing#

The tracker started fine and then sat there. Thirteen minutes in, it showed one satellite and no position:

Tracker app screen reading ACQUIRING GPS, duration 00:13:23, 1 satellite, 0 waypoints, waiting for satellites

The system log, logcat, showed the GPS side alive: libloc, the tag this HAL logs under, opened a fix session, received satellite and NMEA reports every second, closed the session and opened the next one. /etc/gps.conf listed the XTRA and SUPL assistance servers, and assisted GPS was enabled in settings. Nothing looked broken. It looked slow, so the phone stayed by the window.

Later that night it worked. A tracker session that had been running for an hour and nineteen minutes showed three satellites, a position and 50 m accuracy. I am keeping those coordinates to myself: they are my window. Remember that fix, because it is the only honest one in this post.

Then came a long stretch of tracker rebuilds and reboots, and after that, about half an hour of polling by the same window produced nothing. The framework was not the obstacle: in CM7's Java code, GpsLocationProvider.reportLocation() and LocationManagerService filter nothing by accuracy, they only need the native layer to hand over a latitude and longitude. The native layer had nothing to hand over:

D/libloc  ( 1338): Session status: RPC_LOC_SESS_STATUS_GENERAL_FAILURE   Valid mask: 0x1
D/libloc  ( 1338): Latitude:  0.0000000 (intermediate)

General failure, nothing valid except the status itself, and a latitude of zero with a label. The label is what sent me into the binary.

Reading gps.vision.so in Ghidra: from a log string to the gate#

Symbols get stripped; log strings stay, which is a kindness I have learned to depend on. The string table held Latitude: %.7f (%s) at 0x1ac78, the word intermediate at 0x1ac6b, and, at 0x1a255, this:

loc_eng_report_position: ignore intermediate position with error %.2f > %ld meters

One function references it, FUN_00015afc. Its first real decision, line 22 of Ghidra's decompiler output, is the gate:

if (((*param_1 & 1) != 0) && ((param_1[2] == 0 || ((param_1[2] == 1 && (gps_conf._0_4_ != 0))))))

Ghidra decompiler showing FUN_00015afc in gps.vision.so, with the session-status gate on line 22 and the accuracy-threshold filter on lines 68 to 75

*param_1 is the report's valid mask, and bit 0 says the session status is present. param_1[2] is that status. CyanogenMod's headers for this location API number the statuses: 0 is RPC_LOC_SESS_STATUS_SUCCESS, a final fix; 1 is RPC_LOC_SESS_STATUS_IN_PROGESS (sic), an intermediate one; 2 is GENERAL_FAILURE. Final fixes always pass. Intermediate ones pass only if gps_conf._0_4_ is non-zero. My general failures pass under no setting at all, which I noted and then, in the excitement, stopped thinking about.

gps_conf is filled by loc_read_gps_conf(), which sets its defaults before it opens the file:

gps_conf._0_4_ = 0;
gps_conf._4_4_ = 0;
gps_conf._8_4_ = 0;
gps_conf._12_4_ = 4;
__stream = fopen("/etc/gps.conf","r");

The names come from a parameter table at 0x1e7c0: 44-byte entries, each a 36-byte name, a pointer into gps_conf and a type byte n. Four entries: INTERMEDIATE_POS, ACCURACY_THRES, ENABLE_WIPER, DEBUG_LEVEL. So intermediate positions are off by default, there is no accuracy threshold, and the log level is 4. My gps.conf mentioned none of the four.

CyanogenMod's open copy of this Qualcomm HAL, the gingerbread branch of android_hardware_qcom_gps, has the same four fields in the same order in loc_eng_cfg.h, and the readable version of the gate:

if (location_report_ptr->session_status == RPC_LOC_SESS_STATUS_SUCCESS ||
    (location_report_ptr->session_status == RPC_LOC_SESS_STATUS_IN_PROGESS &&
    gps_conf.INTERMEDIATE_POS))

An older AOSP copy of the same file, from October 2010, has no intermediate path at all: it reports a position only when the status is SUCCESS.

The silent drop, and the one that logs#

The string I followed in belongs to a second filter, lines 68 to 75. It applies only to intermediate reports that already made it past the gate, and only when the report carries a circular accuracy value (mask bit 0x1000) larger than ACCURACY_THRES. That drop is logged at level 2 and up, so at the default level 4 it would have shown up in logcat. (Ghidra passes a third argument to that __android_log_print; the format string uses two.)

The first gate logs nothing. In this binary the reject branch has no print at all, so an intermediate report under the default config disappears without a word at any DEBUG_LEVEL. For a while I took that personally. It felt like proof: a function quietly eating my positions.

Three lines in gps.conf and a position in two seconds#

I appended three lines to /etc/gps.conf. The third only restates the default:

INTERMEDIATE_POS=1
ACCURACY_THRES=200
DEBUG_LEVEL=4

After a reboot the HAL read them back:

D/libloc  ( 1336): loc_read_gps_conf: PARAM INTERMEDIATE_POS = 1
D/libloc  ( 1336): loc_read_gps_conf: PARAM ACCURACY_THRES = 200
D/libloc  ( 1336): loc_read_gps_conf: PARAM DEBUG_LEVEL = 4

I pressed the camera button. The tracker logged "no fix", started a GPS session, and this came through:

D/libloc  ( 1336): sv count: 0
D/libloc  ( 1336): Event RPC_LOC_EVENT_PARSED_POSITION_REPORT (client 0)
D/libloc  ( 1336): Session status: RPC_LOC_SESS_STATUS_IN_PROGESS   Valid mask: 0x6069
D/libloc  ( 1336): Latitude:  48.3699971 (intermediate)
D/libloc  ( 1336): Longitude: 31.1600000
D/libloc  ( 1336): Accuracy: 0.0000000

The waypoint file got its row two seconds after the "no fix" row. A tracker session about ten minutes later showed the same place at 47 seconds:

Tracker app recording: REC, 0.0 km/h, distance 0.00 km, duration 00:00:47, altitude 0 m, 1 satellite, position 48.369997, 31.160000 with accuracy 0 m

What valid mask 0x6069 says about 48.37, 31.16#

The satellite report just before that position counted zero satellites in view. Zero satellites, zero metres of error: the GPS equivalent of a dashboard that is green because the monitoring is down.

The valid mask says more. In CyanogenMod's headers for this API, and in the bit layout the decompiled function itself uses, 0x6069 is session status, leap seconds, latitude, longitude and the two axes of an uncertainty ellipse. No timestamp, no altitude, no speed, no heading, and no circular accuracy, bit 0x1000, the only uncertainty this HAL reads. So the position reached Android with no accuracy at all, the tracker displayed that as 0 m, and my shiny new ACCURACY_THRES=200 never ran, because its check requires the very bit that was missing.

The next two waypoints carried the same latitude to seven decimals. A minute later on the tracker screen, nothing had moved:

Tracker app recording: duration 00:01:49, distance 0.00 km, altitude 0 m, 1 satellite, still 48.369997, 31.160000 with accuracy 0 m

And 48.37, 31.16 is about 235 km from the fix the same phone had produced by the same window earlier that night, the one with three satellites and 50 m accuracy. Where the modem got this point, I never found out. What it was is clear enough: an in-progress guess with no satellites behind it, the exact kind of report the default config refuses to pass on.

Qualcomm's own reference gps.conf in AOSP, the Android 4.2.2 version, documents the switch and ships it off:

# Intermediate position report, 1=enable, 0=disable
INTERMEDIATE_POS=0

My project notes, meanwhile, still call INTERMEDIATE_POS=1 critical for this phone. I wrote those notes. They are the most confident document in the repository.

What the binary knew#

The gate was never eating real fixes. Before the change, every position report I captured had a latitude of zero, and the ones whose status made it into my logs were general failures. No setting passes either: the gate refuses a general failure, and lines 57 to 67 of the same function drop any position at exactly 0, 0 before it is reported. After the change, the only new thing that got through was a position from nowhere with the accuracy field missing. The one real fix of the night came the slow way: an hour and more by a window, three satellites, and a final report the default config was always happy to deliver.

The tracker itself works. Press the camera button and it writes a row to /sdcard/tracks/waypoints.csv: a position when the HAL hands one over, a single "no fix" row when it doesn't. Nothing in my records shows the three lines coming back out of gps.conf, though, so the HAL still hands over guesses, and a row can be as confident and as wrong as 48.37, 31.16. Deleting the INTERMEDIATE_POS=1 line brings back the default that refuses them.

I owe you an apology, gps.vision.so. Sixty kilobytes of undocumented blob, one honest default, and I spent the night with a decompiler working out how to switch it off. This phone came to me as spare parts and has spent the series teaching me the same lesson in different file formats: the thing that refuses to answer is usually the only thing in the stack that knows it doesn't know.

Filed under: android, reverse-engineering, ghidra, gps, qualcomm

Sources#