Skip to content

Commit 363b83f

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
Make transaction code caching configurable and disabled by default
Add OptionCachePath to versionaware.NewTransport that enables caching of the fully resolved VersionTable (not just DEX extraction) to a user-specified path. Cache is fingerprinted and auto-invalidated on firmware changes. Previously caching was always on and hardcoded to /data/local/tmp/.binder_cache/codes.json, only covering DEX results. Now it covers all resolution strategies and is opt-in.
1 parent b8b391c commit 363b83f

3 files changed

Lines changed: 116 additions & 39 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,16 @@ Each binder method has a numeric transaction code that can differ between Androi
17501750

17511751
Methods 2 and 3 exist only for extra reliability in edge cases (e.g. no read access to `/system/framework/`). The `genversions` tool builds the version tables by checking out AOSP revision tags and recording method→code mappings.
17521752

1753+
The resolved table can be cached to disk for fast subsequent startups by passing `OptionCachePath`:
1754+
1755+
```go
1756+
transport, err := versionaware.NewTransport(ctx, driver, 0,
1757+
versionaware.OptionCachePath("/data/local/tmp/binder-codes.json"),
1758+
)
1759+
```
1760+
1761+
Caching is disabled by default. The cache is fingerprinted and automatically invalidated when the device firmware changes.
1762+
17531763
## Testing and Verification
17541764

17551765
The project is verified at four levels:

binder/versionaware/option.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package versionaware
2+
3+
// Option configures a version-aware Transport.
4+
type Option interface {
5+
apply(*config)
6+
}
7+
8+
// Options is a slice of Option.
9+
type Options []Option
10+
11+
func (opts Options) config() config {
12+
cfg := config{}
13+
for _, o := range opts {
14+
o.apply(&cfg)
15+
}
16+
return cfg
17+
}
18+
19+
type config struct {
20+
// CachePath is the file path where the resolved VersionTable
21+
// is cached. Empty means caching is disabled.
22+
CachePath string
23+
}
24+
25+
type optionCachePath struct{ path string }
26+
27+
func (o optionCachePath) apply(c *config) { c.CachePath = o.path }
28+
29+
// OptionCachePath enables caching of the resolved transaction code
30+
// table to the given file path. The cache includes a fingerprint
31+
// so it is automatically invalidated when the OS is updated.
32+
//
33+
// When not set (default), no caching is performed.
34+
func OptionCachePath(path string) Option { return optionCachePath{path: path} }

binder/versionaware/transport.go

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9+
"path/filepath"
910
"sort"
1011
"strings"
1112

@@ -52,25 +53,65 @@ func NewTransport(
5253
ctx context.Context,
5354
inner binder.Transport,
5455
targetAPI int,
56+
opts ...Option,
5557
) (*Transport, error) {
58+
cfg := Options(opts).config()
59+
5660
if targetAPI <= 0 {
5761
targetAPI = detectAPILevel()
5862
}
5963
if targetAPI <= 0 {
6064
return nil, fmt.Errorf("versionaware: unable to detect Android API level; pass --target-api explicitly or ensure /etc/build_flags.json is readable; supported API levels: %v", supportedAPILevels())
6165
}
6266

67+
// Try loading from cache if configured.
68+
if cfg.CachePath != "" {
69+
fingerprint := resolvedTableFingerprint(targetAPI)
70+
if table := loadCachedTable(cfg.CachePath, fingerprint); table != nil {
71+
logger.Debugf(ctx, "versionaware: loaded cached transaction codes from %s (%d interfaces)", cfg.CachePath, len(table))
72+
return &Transport{
73+
inner: inner,
74+
apiLevel: targetAPI,
75+
table: table,
76+
version: fmt.Sprintf("%d.cached", targetAPI),
77+
}, nil
78+
}
79+
}
80+
81+
table, version, err := resolveTable(ctx, inner, targetAPI)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
// Save to cache if configured.
87+
if cfg.CachePath != "" {
88+
fingerprint := resolvedTableFingerprint(targetAPI)
89+
saveCachedTable(cfg.CachePath, fingerprint, table)
90+
logger.Debugf(ctx, "versionaware: cached resolved transaction codes to %s", cfg.CachePath)
91+
}
92+
93+
return &Transport{
94+
inner: inner,
95+
apiLevel: targetAPI,
96+
table: table,
97+
version: version,
98+
}, nil
99+
}
100+
101+
// resolveTable performs the full transaction code resolution:
102+
// DEX extraction first, then compiled tables with revision detection.
103+
func resolveTable(
104+
ctx context.Context,
105+
inner binder.Transport,
106+
targetAPI int,
107+
) (VersionTable, string, error) {
63108
// Primary: extract transaction codes directly from the device's
64109
// framework JAR files. This is definitive — no version guessing.
65110
table := extractTransactionCodesFromDevice()
66111
if table != nil {
112+
version := fmt.Sprintf("%d.device", targetAPI)
67113
logger.Debugf(ctx, "versionaware: extracted transaction codes from device framework JARs (%d interfaces)", len(table))
68-
return &Transport{
69-
inner: inner,
70-
apiLevel: targetAPI,
71-
table: table,
72-
version: fmt.Sprintf("%d.device", targetAPI),
73-
}, nil
114+
return table, version, nil
74115
}
75116

76117
// Fallback: use compiled version tables with revision detection.
@@ -80,7 +121,7 @@ func NewTransport(
80121

81122
revisions := Revisions[targetAPI]
82123
if len(revisions) == 0 {
83-
return nil, fmt.Errorf("versionaware: API level %d is not supported and framework JARs not readable; supported API levels: %v", targetAPI, supportedAPILevels())
124+
return nil, "", fmt.Errorf("versionaware: API level %d is not supported and framework JARs not readable; supported API levels: %v", targetAPI, supportedAPILevels())
84125
}
85126

86127
// Narrow revision candidates by checking which methods exist in
@@ -97,23 +138,25 @@ func NewTransport(
97138
var err error
98139
version, err = probeRevision(ctx, inner, targetAPI)
99140
if err != nil {
100-
return nil, fmt.Errorf("versionaware: probing revision for API %d: %w", targetAPI, err)
141+
return nil, "", fmt.Errorf("versionaware: probing revision for API %d: %w", targetAPI, err)
101142
}
102143
}
103144

104145
table, ok := Tables[version]
105146
if !ok {
106-
return nil, fmt.Errorf("versionaware: no transaction code table for version %q", version)
147+
return nil, "", fmt.Errorf("versionaware: no transaction code table for version %q", version)
107148
}
108149

109150
logger.Debugf(ctx, "versionaware: using compiled version table %s (%d interfaces)", version, len(table))
151+
return table, version, nil
152+
}
110153

111-
return &Transport{
112-
inner: inner,
113-
apiLevel: targetAPI,
114-
table: table,
115-
version: version,
116-
}, nil
154+
// resolvedTableFingerprint returns a fingerprint that changes when
155+
// the device's firmware changes, invalidating cached transaction codes.
156+
// Combines API level with framework JAR fingerprint (if available).
157+
func resolvedTableFingerprint(apiLevel int) string {
158+
fp := frameworkFingerprint()
159+
return fmt.Sprintf("api=%d;jars=%s", apiLevel, fp)
117160
}
118161

119162
// ResolveCode resolves an AIDL method name to the correct transaction code
@@ -152,25 +195,10 @@ const (
152195
// with AIDL $Stub classes and TRANSACTION_* constants.
153196
const frameworkJARDir = "/system/framework"
154197

155-
// cacheDir is where bindercli stores cached transaction code tables.
156-
const cacheDir = "/data/local/tmp/.binder_cache"
157-
158198
// extractTransactionCodesFromDevice scans all JARs in /system/framework/
159199
// and extracts definitive transaction codes from DEX bytecode.
160-
// Caches the result to disk for fast subsequent startups.
161200
// Returns nil if the directory is not readable or no codes are found.
162201
func extractTransactionCodesFromDevice() VersionTable {
163-
fingerprint := frameworkFingerprint()
164-
if fingerprint == "" {
165-
return nil
166-
}
167-
168-
// Try loading from cache.
169-
if table := loadCachedTable(fingerprint); table != nil {
170-
return table
171-
}
172-
173-
// Scan all JARs.
174202
entries, err := os.ReadDir(frameworkJARDir)
175203
if err != nil {
176204
return nil
@@ -200,8 +228,6 @@ func extractTransactionCodesFromDevice() VersionTable {
200228
return nil
201229
}
202230

203-
// Cache for next time.
204-
saveCachedTable(fingerprint, table)
205231
return table
206232
}
207233

@@ -227,10 +253,12 @@ func frameworkFingerprint() string {
227253
return b.String()
228254
}
229255

230-
// loadCachedTable reads a cached VersionTable from disk.
256+
// loadCachedTable reads a cached VersionTable from the given path.
231257
// Returns nil if cache is missing, corrupted, or fingerprint doesn't match.
232-
func loadCachedTable(fingerprint string) VersionTable {
233-
cachePath := cacheDir + "/codes.json"
258+
func loadCachedTable(
259+
cachePath string,
260+
fingerprint string,
261+
) VersionTable {
234262
data, err := os.ReadFile(cachePath)
235263
if err != nil {
236264
return nil
@@ -257,9 +285,14 @@ func loadCachedTable(fingerprint string) VersionTable {
257285
return table
258286
}
259287

260-
// saveCachedTable writes a VersionTable to disk for future use.
261-
func saveCachedTable(fingerprint string, table VersionTable) {
262-
_ = os.MkdirAll(cacheDir, 0o755)
288+
// saveCachedTable writes a VersionTable to the given path.
289+
func saveCachedTable(
290+
cachePath string,
291+
fingerprint string,
292+
table VersionTable,
293+
) {
294+
dir := filepath.Dir(cachePath)
295+
_ = os.MkdirAll(dir, 0o755)
263296

264297
raw := make(map[string]map[string]uint32)
265298
for desc, methods := range table {
@@ -281,7 +314,7 @@ func saveCachedTable(fingerprint string, table VersionTable) {
281314
if err != nil {
282315
return
283316
}
284-
_ = os.WriteFile(cacheDir+"/codes.json", data, 0o644)
317+
_ = os.WriteFile(cachePath, data, 0o644)
285318
}
286319

287320
// filterRevisionsBySOMethodSet reads BpServiceManager symbols from

0 commit comments

Comments
 (0)