gradle / gradle/native-platform
PosixFileSystemFunctions.listFileSystems throws NPE instead of NativeException when a mount point contains non-ASCII bytes under C/POSIX locale
- Dominant language
- Java
- Stars
- 126
- Forks
- 44
- Avg merge
- 7h 44m
- Merged PRs (30d)
- 9
Description
## Summary
On Linux, when the JVM's `LC_CTYPE` is `C` or `POSIX` and `/etc/mtab` contains a mount entry whose path includes non-ASCII bytes (e.g. a removable volume named `新加卷`), `PosixFileSystemFunctions.listFileSystems` crashes with a bare `NullPointerException` originating from JNI. The real underlying error — a locale conversion failure — is recorded but never surfaced, because the NPE is thrown first and bypasses the `NativeException` path that callers expect.
This breaks Gradle builds that would otherwise be saved by the lenient handling added in gradle/gradle#16899, since that code only catches `NativeException`.
Related: #28 reports a different surface symptom ("file does not exist" from `getMode`) that traces back to the same underlying defect in `char_to_java` / `java_to_char`.
## Stack trace
Observed with Gradle 9.1.0 on Android Studio (JBR 21), Linux:
```
* What went wrong:
java.lang.NullPointerException (no error message)
* Exception is:
java.lang.NullPointerException
at net.rubygrapefruit.platform.internal.FileSystemList.add(FileSystemList.java:31)
at net.rubygrapefruit.platform.internal.jni.PosixFileSystemFunctions.listFileSystems(Native Method)
at net.rubygrapefruit.platform.internal.PosixFileSystems.getFileSystems(PosixFileSystems.java:30)
at org.gradle.internal.watch.vfs.impl.DefaultWatchableFileSystemDetector.detectUnsupportedFileSystems(DefaultWatchableFileSystemDetector.java:62)
... (Gradle frames omitted)
```
## Root cause
Two issues combine to produce this crash.
**1. `char_to_java` returns `NULL` on locale conversion failure (expected) — but only after marking the failure on `result`.**
In
https://github.com/gradle/native-platform/blob/eec52804b02b4366089c0cf2536725e7eb873d40/native-platform/src/shared/cpp/unix_strings.cpp#L51-L68
`mbstowcs` depends on `LC_CTYPE`. Under the `C` / `POSIX` locale, any byte `> 0x7F` is treated as an invalid multibyte sequence, so `mbstowcs` returns `(size_t) -1` and `char_to_java` correctly returns `NULL` after calling `mark_failed_with_message`.
**2. `Java_..._listFileSystems` does not check for `NULL` before passing the result back to Java.**
In
https://github.com/gradle/native-platform/blob/eec52804b02b4366089c0cf2536725e7eb873d40/native-platform/src/main/cpp/linux.cpp#L34-L55
When any of those is `NULL`, it gets passed straight into `FileSystemList.add(...)` on the Java side, which has non-nullable parameters and throws NPE.
The damaging side effect is that the NPE **shadows** the meaningful error already recorded via `mark_failed_with_message`. Callers never see `"could not convert string from current locale"`; they only see a `NullPointerException` with no message and a stack trace that points into a `Native Method`, which makes this very hard to diagnose from the build log alone.
It also defeats Gradle's lenient fallback in `WatchingVirtualFileSystem` (added in gradle/gradle#16899), because that code catches `NativeException`, not `RuntimeException`.
## The same defect exists symmetrically in `java_to_char` / `wcstombs`
`unix_strings.cpp` also exposes `java_to_char`, the inverse of `char_to_java`, which uses `wcstombs` instead of `mbstowcs`. `wcstombs` has the same `LC_CTYPE` dependency and the same `(size_t) -1` failure mode — see [`wcstombs(3)`](https://www.man7.org/linux/man-pages/man3/wcstombs.3.html): *"The behavior of wcstombs() depends on the LC_CTYPE category of the current locale."*
This means **any** Java-side path containing characters not representable in the daemon's current locale — typically a user project under `~/projects/我的项目/` or similar — will produce a `NULL` C string when crossing into native code under C/POSIX locale. Depending on whether the caller null-checks, the result is either a `NativeException`, an NPE, a misleading errno-based error (e.g. "file does not exist", which is what #28 reports), or — in the worst case, if the `NULL` reaches a C library function that dereferences it — a daemon crash with no diagnostic at all.
The `listFileSystems` NPE reported here and the `getMode` "file does not exist" reported in #28 are two surface symptoms of the same underlying defect: **`char_to_java` / `java_to_char` rely on the JVM process's `LC_CTYPE`, but Linux filesystem paths are byte sequences whose encoding is not guaranteed to match that locale** (and in practice often doesn't, especially in CI containers and minimal Docker images which default to C/POSIX).
## Reproduction
1. Linux host.
2. A mount point whose path contains non-ASCII bytes. Easiest way to set one up:
```bash
sudo mkdir -p /mnt/新加卷
sudo mount -t tmpfs tmpfs /mnt/新加卷
```
3. Launch a Gradle build with the daemon's `LC_CTYPE` set to `C` or `POSIX`:
```bash
LC_ALL=C ./gradlew help
```
Expected: a `NativeException` with the message `"could not convert string from current locale"`, which Gradle can catch and degrade gracefully (disable file-system watching, continue the build).
Actual: bare `NullPointerException`, build fails.
## Suggested fix
A complete fix has two layers.
**Short term (this issue):** null-check every `char_to_java` / `java_to_char` call site so failures surface as `NativeException` with the message already set by `mark_failed_with_message`, instead of NPEs, segfaults, or misleading errnos. For the immediate crash:
```cpp
while (getmntent_r(fp, &mount_info, buf, sizeof(buf)) != NULL) {
jstring mount_point = char_to_java(env, mount_info.mnt_dir, result);
if (mount_point == NULL) { endmntent(fp); return; }
jstring file_system_type = char_to_java(env, mount_info.mnt_type, result);
if (file_system_type == NULL) { endmntent(fp); return; }
jstring device_name = char_to_java(env, mount_info.mnt_fsname, result);
if (device_name == NULL) { endmntent(fp); return; }
env->CallVoidMethod(info, method, mount_point, file_system_type, device_name,
JNI_FALSE, JNI_TRUE, JNI_TRUE);
}
```
The same pattern should be applied to all other call sites of `char_to_java` and `java_to_char` across the POSIX/Linux/macOS sources — a sweep is warranted, since `mark_failed_with_message` is already being called inside the helpers and the convention clearly intends the caller to bail out.
**Long term:** stop routing filesystem paths through the process locale at all. Treat Linux paths as opaque byte sequences and exchange them with Java as `byte[]` (decoded on the Java side using `sun.jnu.encoding`, which is what the JDK itself does for `java.io.File`), or force a UTF-8 conversion via `mbstowcs_l` / `wcstombs_l` against a fixed `C.UTF-8` locale independent of the process locale. This would resolve the root cause behind both this issue and #28.
## Environment
- Gradle: 9.1.0
- OS: Linux
- JVM: JetBrains Runtime 21 (bundled with Android Studio)
- Daemon `LC_CTYPE`: `C` (no `LC_ALL` / `LANG` set; daemon was started with `-Duser.country=US -Duser.language=en` only, which does not affect the C-side locale)
- Mount point triggering the crash: a removable volume labeled `新加卷`
- $ locale
LANG=zh_CN.UTF-8
LC_CTYPE=C.UTF-8
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.