Skip to content

gmscompat: use fresh file proxy fds for app opens#388

Open
inthewaves wants to merge 3 commits into
GrapheneOS:17from
inthewaves:gmscompat-fix-file-proxy-fds
Open

gmscompat: use fresh file proxy fds for app opens#388
inthewaves wants to merge 3 commits into
GrapheneOS:17from
inthewaves:gmscompat-fix-file-proxy-fds

Conversation

@inthewaves

Copy link
Copy Markdown
Member

Closes GrapheneOS/os-issue-tracker#7978

Normal Java file opens such as FileInputStream and ParcelFileDescriptor.open() expect each
open to return a fresh descriptor owned and closed by the caller. The regular GMS data file
hooks instead returned dup()s of the cached descriptor used for Dynamite /gmscompat_fd_N
paths. dup() shares the same open file description, so repeated app reads can inherit another
reader's offset and parse truncated Phenotype snapshots. Keep the cache only for Dynamite and
open fresh read-only proxy fds for normal Java file opens.

Maestro / Pixel Buds app errors:

E Maestro_kph: Failed to parse snapshot from shared storage for com.google.android.apps.wearables.maestro.companion#com.google.android.apps.wearables.maestro.companion
E Maestro_kph: myz: While parsing a protocol message, the input ended unexpectedly in the middle of a field.  This could mean either that the input has been truncated or that an embedded message misreported its own length.
E Maestro_kph: 	at mxq.Y(PG:29)
E Maestro_kph: 	at mxq.j(PG:9)
E Maestro_kph: 	at mxq.o(PG:1)
E Maestro_kph: 	at kpy.b(PG:1)
E Maestro_kph: 	at krz.a(PG:377)
E Maestro_kph: 	at fmq.apply(PG:16)
E Maestro_kph: 	at lzn.e(PG:3)
E Maestro_kph: 	at lzo.run(PG:38)
E Maestro_kph: 	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:520)
E Maestro_kph: 	at java.util.concurrent.FutureTask.run(FutureTask.java:328)
E Maestro_kph: 	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:323)
E Maestro_kph: 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
E Maestro_kph: 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
E Maestro_kph: 	at java.lang.Thread.run(Thread.java:1572)

@inthewaves inthewaves requested a review from muhomorr June 23, 2026 20:04
if (fd != null) {
String fdPath = "/proc/self/fd/" + fd.getInt$();
return new File(fdPath).lastModified();
try (ParcelFileDescriptor pfd = openFreshParcelFileDescriptor(path)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was getFileLastModified() switched to using fresh file descriptors?

@inthewaves inthewaves Jun 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GmsCore updates Phenotype shared snapshots by writing to a temp file then renaming it to replace, so caching those fds could be stale. After looking at client code again, I don't think any of them currently use lastModified. I guess we can still just scope the cached path here to Dynamite modules

return new File(fdPath).lastModified();
}
} catch (IOException e) {
throw new RuntimeException(e);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File.lastModified() returns 0 on failure.

The GmsCore file proxy only opens files with O_RDONLY. Avoid intercepting write, append or
read-write opens so callers keep normal open semantics for unsupported access modes.
@inthewaves inthewaves force-pushed the gmscompat-fix-file-proxy-fds branch from dc5887e to 12c31a8 Compare June 25, 2026 09:30
@inthewaves

Copy link
Copy Markdown
Member Author

Sample app output doing DynamiteModule.load(context, DynamiteModule.PREFER_REMOTE,"com.google.android.gms.tflite_dynamite") with an extra log in getFileLastModified to log the branch taken

D GmsCoreFileServerClientHooks: cached path /data/user_de/0/com.google.android.gms/app_chimera/m/00000003/dl-TfliteDynamiteDynamite.integ_252130102100400.apk
D GmsCoreFileServerClientHooks: java.lang.Throwable
D GmsCoreFileServerClientHooks: 	at com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks.openCachedFile(GmsCoreFileServerClientHooks.java:133)
D GmsCoreFileServerClientHooks: 	at com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks.getFileLastModified(GmsCoreFileServerClientHooks.java:78)
D GmsCoreFileServerClientHooks: 	at com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks$$ExternalSyntheticLambda0.applyAsLong(D8$$SyntheticClass:0)
D GmsCoreFileServerClientHooks: 	at java.io.File.lastModified(File.java:975)
D GmsCoreFileServerClientHooks: 	at m2.ab.h(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):10)
D GmsCoreFileServerClientHooks: 	at m2.aa.e(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):1)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamiteloader.DynamiteLoaderV2.loadModule2NoCrashUtils(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):88)
D GmsCoreFileServerClientHooks: 	at m2.cn.a(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):73)
D GmsCoreFileServerClientHooks: 	at m2.p.onTransact(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):21)
D GmsCoreFileServerClientHooks: 	at android.os.Binder.transact(Binder.java:1256)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.internal.common.zza.zzB(com.google.android.gms:play-services-basement@@18.10.0:2)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamite.zzq.zzf(com.google.android.gms:play-services-basement@@18.10.0:6)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamite.DynamiteModule.load(com.google.android.gms:play-services-basement@@18.10.0:46)
D GmsCoreFileServerClientHooks: 	at ...
D GmsCoreFileServerClientHooks: getFileLastModified /data/user_de/0/com.google.android.gms/app_chimera/m/00000003/dl-TfliteDynamiteDynamite.integ_252130102100400.apk: shouldCacheForLastModified branch result 1781860487445
D GmsCoreFileServerClientHooks: java.lang.Throwable
D GmsCoreFileServerClientHooks: 	at com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks.getFileLastModified(GmsCoreFileServerClientHooks.java:80)
D GmsCoreFileServerClientHooks: 	at com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks$$ExternalSyntheticLambda0.applyAsLong(D8$$SyntheticClass:0)
D GmsCoreFileServerClientHooks: 	at java.io.File.lastModified(File.java:975)
D GmsCoreFileServerClientHooks: 	at m2.ab.h(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):10)
D GmsCoreFileServerClientHooks: 	at m2.aa.e(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):1)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamiteloader.DynamiteLoaderV2.loadModule2NoCrashUtils(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):88)
D GmsCoreFileServerClientHooks: 	at m2.cn.a(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):73)
D GmsCoreFileServerClientHooks: 	at m2.p.onTransact(:com.google.android.gms.dynamite_dynamiteloader@262031035@26.20.31 (260400-0):21)
D GmsCoreFileServerClientHooks: 	at android.os.Binder.transact(Binder.java:1256)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.internal.common.zza.zzB(com.google.android.gms:play-services-basement@@18.10.0:2)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamite.zzq.zzf(com.google.android.gms:play-services-basement@@18.10.0:6)
D GmsCoreFileServerClientHooks: 	at com.google.android.gms.dynamite.DynamiteModule.load(com.google.android.gms:play-services-basement@@18.10.0:46)
D GmsCoreFileServerClientHooks: 	at com.example.sampleapp.MainActivityKt.loadPreferRemoteDynamiteModule(MainActivity.kt:241)
D GmsCoreFileServerClientHooks: 	at ...

@inthewaves inthewaves requested a review from muhomorr June 25, 2026 09:38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is outdated, /gmscompat_fd is used instead of /proc/self/fd

}

private static boolean isApkContainerPath(String path) {
int zipSeparatorIdx = path.indexOf(ZIP_FILE_SEPARATOR);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of checking for ZIP_FILE_SEPARATOR here? Strings with ZIP_FILE_SEPARATOR aren't paths in this context.

@inthewaves inthewaves Jun 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally changed the checks in GmsDynamiteClientHooks to also use this so both the Dynamite hooks and the lastModified hooks shared the same check, but decided against it and forgot to revert this part, will remove it

Normal Java file opens such as FileInputStream and ParcelFileDescriptor.open() expect each
open to return a fresh descriptor owned and closed by the caller. The regular GMS data file
hooks instead returned dup()s of the cached descriptor used for Dynamite /gmscompat_fd_N
paths. dup() shares the same open file description, so repeated app reads can inherit another
reader's offset and parse truncated Phenotype snapshots. Keep the cache only for Dynamite and
open fresh read-only proxy fds for normal Java file opens.

Maestro / Pixel Buds app errors:

```
E Maestro_kph: Failed to parse snapshot from shared storage for com.google.android.apps.wearables.maestro.companion#com.google.android.apps.wearables.maestro.companion
E Maestro_kph: myz: While parsing a protocol message, the input ended unexpectedly in the middle of a field.  This could mean either that the input has been truncated or that an embedded message misreported its own length.
E Maestro_kph: 	at mxq.Y(PG:29)
E Maestro_kph: 	at mxq.j(PG:9)
E Maestro_kph: 	at mxq.o(PG:1)
E Maestro_kph: 	at kpy.b(PG:1)
E Maestro_kph: 	at krz.a(PG:377)
E Maestro_kph: 	at fmq.apply(PG:16)
E Maestro_kph: 	at lzn.e(PG:3)
E Maestro_kph: 	at lzo.run(PG:38)
E Maestro_kph: 	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:520)
E Maestro_kph: 	at java.util.concurrent.FutureTask.run(FutureTask.java:328)
E Maestro_kph: 	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:323)
E Maestro_kph: 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
E Maestro_kph: 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
E Maestro_kph: 	at java.lang.Thread.run(Thread.java:1572)
```

Test: Maestro no longer has parsing errors for Phenotype flags, and uses the fresh fd path
Test: An app with `DynamiteModule.load(
  context, DynamiteModule.PREFER_REMOTE,"com.google.android.gms.tflite_dynamite"
)` uses `openCachedFile` and the cached file branch in `getFileLastModified`
@inthewaves inthewaves force-pushed the gmscompat-fix-file-proxy-fds branch from 12c31a8 to 98d1729 Compare June 25, 2026 21:33
@inthewaves inthewaves requested a review from muhomorr June 25, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pixel Buds app has broken again

2 participants