Skip to content

Commit 3fdb9d0

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
Add ELF-based API and revision detection from libbinder.so
Three-layer version detection, no binder transactions needed: 1. Parse .note.android.ident from /system/lib64/libbinder_ndk.so to read the Android API level (primary method) 2. Read RELEASE_PLATFORM_SDK_VERSION from /etc/build_flags.json (fallback, handles both API 35 and 36 JSON schemas) 3. Parse BpServiceManager symbols from /system/lib64/libbinder.so to determine which IServiceManager methods exist on the device, then filter revision candidates to those matching the method set This fixes the emulator (API 35) where probing alone matched the wrong revision because isUserAMonkey at one revision's code coincidentally returned 8 bytes from a different method.
1 parent ddacb68 commit 3fdb9d0

2 files changed

Lines changed: 239 additions & 3 deletions

File tree

binder/versionaware/apilevel.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,93 @@
11
package versionaware
22

33
import (
4+
"debug/elf"
5+
"encoding/binary"
46
"encoding/json"
57
"os"
68
"strconv"
9+
"strings"
710
)
811

912
// detectAPILevel returns the Android API level of the running device.
10-
// Reads /etc/build_flags.json (world-readable, no root needed, no fork).
13+
// Tries multiple detection methods in order:
14+
// 1. Parse .note.android.ident from /system/lib64/libbinder_ndk.so (ELF note, always readable)
15+
// 2. Read RELEASE_PLATFORM_SDK_VERSION from /etc/build_flags.json
1116
// Returns 0 if detection fails (e.g. when running outside Android).
1217
func detectAPILevel() int {
18+
if n := detectViaBinderNDKNote(); n > 0 {
19+
return n
20+
}
1321
return detectViaBuildFlags()
1422
}
1523

24+
// binderNDKPaths lists candidate locations for libbinder_ndk.so.
25+
var binderNDKPaths = []string{
26+
"/system/lib64/libbinder_ndk.so",
27+
"/system/lib/libbinder_ndk.so",
28+
}
29+
30+
// detectViaBinderNDKNote reads the .note.android.ident ELF note from
31+
// libbinder_ndk.so to extract the Android API level. This note is
32+
// present in all Android system libraries and is always readable.
33+
func detectViaBinderNDKNote() int {
34+
for _, path := range binderNDKPaths {
35+
n := parseELFAndroidAPILevel(path)
36+
if n > 0 {
37+
return n
38+
}
39+
}
40+
return 0
41+
}
42+
43+
// parseELFAndroidAPILevel reads the .note.android.ident section from
44+
// an ELF binary and returns the API level stored in it.
45+
func parseELFAndroidAPILevel(path string) int {
46+
f, err := elf.Open(path)
47+
if err != nil {
48+
return 0
49+
}
50+
defer f.Close()
51+
52+
section := f.Section(".note.android.ident")
53+
if section == nil {
54+
return 0
55+
}
56+
57+
data, err := section.Data()
58+
if err != nil {
59+
return 0
60+
}
61+
62+
// Parse ELF note format: namesz(4) + descsz(4) + type(4) + name(aligned) + desc(aligned)
63+
for len(data) >= 12 {
64+
namesz := binary.LittleEndian.Uint32(data[0:4])
65+
descsz := binary.LittleEndian.Uint32(data[4:8])
66+
// noteType := binary.LittleEndian.Uint32(data[8:12])
67+
68+
nameOff := uint32(12)
69+
nameEnd := nameOff + namesz
70+
// Align to 4 bytes.
71+
descOff := (nameEnd + 3) &^ 3
72+
descEnd := descOff + descsz
73+
nextOff := (descEnd + 3) &^ 3
74+
75+
if uint32(len(data)) < descEnd {
76+
break
77+
}
78+
79+
name := strings.TrimRight(string(data[nameOff:nameEnd]), "\x00")
80+
if name == "Android" && descsz >= 4 {
81+
apiLevel := binary.LittleEndian.Uint32(data[descOff : descOff+4])
82+
return int(apiLevel)
83+
}
84+
85+
data = data[nextOff:]
86+
}
87+
88+
return 0
89+
}
90+
1691
// buildFlagsPaths lists candidate locations for the build flags file.
1792
var buildFlagsPaths = []string{
1893
"/etc/build_flags.json",

binder/versionaware/transport.go

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package versionaware
22

33
import (
44
"context"
5+
"debug/elf"
56
"fmt"
67
"sort"
8+
"strings"
79

810
"github.com/facebookincubator/go-belt/tool/logger"
911
"github.com/xaionaro-go/aidl/binder"
@@ -60,6 +62,12 @@ func NewTransport(
6062
return nil, fmt.Errorf("versionaware: API level %d is not supported; supported API levels: %v", targetAPI, supportedAPILevels())
6163
}
6264

65+
// Narrow revision candidates by checking which methods exist in
66+
// libbinder.so's BpServiceManager. This distinguishes old-style
67+
// (no getService2/checkService2) from new-style method ordering
68+
// without any binder transactions.
69+
revisions = filterRevisionsBySOMethodSet(revisions)
70+
6371
var version string
6472
switch len(revisions) {
6573
case 1:
@@ -114,11 +122,164 @@ func supportedAPILevels() []int {
114122
}
115123

116124
const (
117-
serviceManagerHandle = uint32(0)
118-
serviceManagerDescriptor = "android.os.IServiceManager"
125+
serviceManagerHandle = uint32(0)
126+
serviceManagerDescriptor = "android.os.IServiceManager"
119127
activityManagerDescriptor = "android.app.IActivityManager"
120128
)
121129

130+
// filterRevisionsBySOMethodSet reads BpServiceManager symbols from
131+
// /system/lib64/libbinder.so to determine which methods exist on
132+
// the device, then filters revision candidates to those whose method
133+
// set matches.
134+
func filterRevisionsBySOMethodSet(revisions []string) []string {
135+
deviceMethods := readBpServiceManagerMethods()
136+
if len(deviceMethods) == 0 {
137+
return revisions // can't read .so, don't filter
138+
}
139+
140+
var filtered []string
141+
for _, rev := range revisions {
142+
table, ok := Tables[rev]
143+
if !ok {
144+
continue
145+
}
146+
smMethods := table[serviceManagerDescriptor]
147+
if smMethods == nil {
148+
continue
149+
}
150+
if methodSetMatches(smMethods, deviceMethods) {
151+
filtered = append(filtered, rev)
152+
}
153+
}
154+
155+
if len(filtered) == 0 {
156+
return revisions // no match found, don't filter
157+
}
158+
return filtered
159+
}
160+
161+
// methodSetMatches returns true if the version table's method set for
162+
// an interface matches the methods found in the device's .so.
163+
// A match means: every method in the table exists in the device methods,
164+
// and no device methods are missing from the table.
165+
func methodSetMatches(
166+
tableMethods map[string]binder.TransactionCode,
167+
deviceMethods map[string]bool,
168+
) bool {
169+
// Check that every method in the table exists on the device.
170+
for method := range tableMethods {
171+
if !deviceMethods[method] {
172+
return false
173+
}
174+
}
175+
// Check that every device method exists in the table.
176+
for method := range deviceMethods {
177+
if _, ok := tableMethods[method]; !ok {
178+
return false
179+
}
180+
}
181+
return true
182+
}
183+
184+
// readBpServiceManagerMethods reads libbinder.so and extracts the
185+
// method names from BpServiceManager symbols.
186+
func readBpServiceManagerMethods() map[string]bool {
187+
paths := []string{
188+
"/system/lib64/libbinder.so",
189+
"/system/lib/libbinder.so",
190+
}
191+
192+
for _, path := range paths {
193+
methods := parseBpMethods(path, "BpServiceManager")
194+
if len(methods) > 0 {
195+
return methods
196+
}
197+
}
198+
return nil
199+
}
200+
201+
// parseBpMethods reads an ELF shared library and extracts method names
202+
// from the BpXxx class's exported symbols. Returns a set of method names.
203+
func parseBpMethods(
204+
path string,
205+
className string,
206+
) map[string]bool {
207+
f, err := elf.Open(path)
208+
if err != nil {
209+
return nil
210+
}
211+
defer f.Close()
212+
213+
symbols, err := f.DynamicSymbols()
214+
if err != nil {
215+
return nil
216+
}
217+
218+
// Match demangled C++ symbols like:
219+
// _ZN7android2os16BpServiceManager10getServiceE...
220+
// The method name is after the class name length+name prefix.
221+
prefix := className
222+
methods := map[string]bool{}
223+
224+
for _, sym := range symbols {
225+
if sym.Info&0xf != uint8(elf.STT_FUNC) {
226+
continue
227+
}
228+
name := sym.Name
229+
// Look for mangled name containing the class name.
230+
idx := findMangledMethod(name, prefix)
231+
if idx == "" {
232+
continue
233+
}
234+
methods[idx] = true
235+
}
236+
237+
return methods
238+
}
239+
240+
// findMangledMethod extracts the method name from a C++ mangled symbol
241+
// that belongs to the given class. Returns "" if not a match.
242+
// Handles Itanium name mangling: _ZN<len><namespace><len><class><len><method>E...
243+
func findMangledMethod(
244+
mangled string,
245+
className string,
246+
) string {
247+
// Quick filter: must contain the class name.
248+
classLen := fmt.Sprintf("%d%s", len(className), className)
249+
idx := strings.Index(mangled, classLen)
250+
if idx < 0 {
251+
return ""
252+
}
253+
254+
// Skip constructors/destructors (C1, C2, D0, D1, D2).
255+
rest := mangled[idx+len(classLen):]
256+
if len(rest) < 2 {
257+
return ""
258+
}
259+
if rest[0] == 'C' || rest[0] == 'D' {
260+
return ""
261+
}
262+
263+
// Parse the method name length + name.
264+
nameLen := 0
265+
i := 0
266+
for i < len(rest) && rest[i] >= '0' && rest[i] <= '9' {
267+
nameLen = nameLen*10 + int(rest[i]-'0')
268+
i++
269+
}
270+
if nameLen == 0 || i+nameLen > len(rest) {
271+
return ""
272+
}
273+
274+
methodName := rest[i : i+nameLen]
275+
// Convert to camelCase (first letter lowercase) to match AIDL method names.
276+
if len(methodName) > 0 && methodName[0] >= 'A' && methodName[0] <= 'Z' {
277+
methodName = string(methodName[0]+32) + methodName[1:]
278+
}
279+
280+
return methodName
281+
}
282+
122283
// probeRevision determines which revision of the given API level matches
123284
// the running device by calling a distinguishing method on IActivityManager.
124285
//

0 commit comments

Comments
 (0)