Skip to content

Add Unsafe array access sanitizer #932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Marcono1234
Copy link
Contributor

@Marcono1234 Marcono1234 commented Jul 3, 2025

Adds a sanitizer which looks for invalid array access performed by sun.misc.Unsafe. Unlike native memory access performed by Unsafe, array access can be sanitized in a stateless way just based on the arguments passed to the Unsafe method. And multiple Java libraries have used Unsafe for arrays in the past to improve performance, see related security advisories GHSA-8wh2-6qhj-h7j9 and GHSA-973x-65j7-xcf4.

Note that fuzzing likely cannot find all such invalid accesses because some of it occurs for numeric overflow respectively multiple MB of processed data, which the fuzzer might be unable to generate.

See related #891

I used #915 as reference for how to implement a sanitizer. However, I am not very familiar with Bazel and this project setup here, and also have / had issues with building it locally on Windows. Therefore I have marked this PR as Draft for now.

Any feedback is appreciated!

@@ -48,6 +48,15 @@ java_library(
],
)

java_library(
name = "unsafe_array_out_of_bounds",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The implementation currently actually detects a bit more than just "out-of-bounds access" (e.g. it also detects unaligned object array access, and reading / writing bytes from an object array and the other way around).

So maybe a name like "unsafe_array_invalid_access" or similar would be better?

Comment on lines +377 to +381
if (!componentType.isPrimitive()) {
// Reading or writing bytes to an array of references; might be possible but seems
// rather unreliable and might mess with the garbage collector?
report("Reading or writing bytes from a " + objClass.getTypeName());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this is too strict. On the other hand there might be no guarantees on how large object references are so any access where object references are treated as bytes seems error-prone.

"com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical",
],
target_class = "com.example.UnsafeArrayOutOfBounds",
verify_crash_reproducer = False,
Copy link
Contributor Author

@Marcono1234 Marcono1234 Jul 3, 2025

Choose a reason for hiding this comment

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

During my testing when reproducer verification was enabled no sanitizer exception was thrown. Are sanitizers generally not enabled when running in reproducer / regression (?) mode?

That might be rather problematic; in the case of Unsafe that might crash the JVM, and I guess for the other sanitizers (such as the path traversal one) it could also have undesired consequences (e.g. if due to path traversal files are placed at random paths on the machine).

Comment on lines +233 to +244
java_fuzz_target_test(
name = "UnsafeArrayOutOfBoundsValid",
srcs = [
"UnsafeArrayOutOfBoundsValid.java",
],
allowed_findings = [],
fuzzer_args = [
# Test does not depend on fuzzing input; just run it once
"-runs=1",
],
target_class = "com.example.UnsafeArrayOutOfBoundsValid",
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this is a good approach to ensure that the Unsafe sanitizer does not report errors for valid access.

Comment on lines +545 to +546
// TODO: Is this a proper way to implement this?
Method testMethod = testMethods[data.consumeInt(0, testMethods.length - 1)];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if this is a good / proper way to ensure that all of the methods cause a sanitizer error.

Once a fuzzing test run finds the first expected failing input (in this case here basically any input), does it keep reusing that? In that case it would never run any of the other test methods here.

Is there a better way to solve this, other than adding dozens of separate classes each with their own fuzzerTestOneInput?

@Marcono1234
Copy link
Contributor Author

I think this PR is now ready for an initial review; any feedback is appreciated! Please also let me know what you think about the points I mentioned above in my review comments.

Also, is there a way to let Bazel install the Maven artifacts (especially the Jazzer JUnit artifact and its dependencies) as SNAPSHOT to the local Maven repository (similar to what mvn install does)? I hope I have integrated the sanitizer correctly but I wasn't able to verify this yet by running Jazzer for a separate project.

@Marcono1234 Marcono1234 marked this pull request as ready for review July 5, 2025 13:43
@fmeum
Copy link
Contributor

fmeum commented Jul 5, 2025

bazel run //deploy:deploy_local should deploy into your local maven repo with the version 0.0.0-dev.

@Marcono1234
Copy link
Contributor Author

Marcono1234 commented Jul 5, 2025

Thanks! That only works for Linux though because deploy/deploy_local.sh is a Bash script, right? Unfortunately I am on Windows. And even if I try to run the commands there individually, it then fails at a later point, apparently because the jazzer.publish-publisher script it generates is again a Bash script.

I will see if I can maybe get it working with WSL 2 or a Docker container. But if that then only produces the Linux and not the Windows native libraries for Jazzer (not completely sure how the native build for Jazzer works) it will also be a bit cumbersome to then use these artifacts.

(Side note: I had to update locally some of the bazel_dep entries in MODULE.bazel especially rules_kotlin to be able to build the project on Windows 11. But I don't think this is related to the deploy_local issues I am facing now.)

Edit: Was able to build it now in a Docker container, but had to copy the native libraries which I had previously built on Windows into the JAR. The sanitizer seems to work.

@fmeum
Copy link
Contributor

fmeum commented Jul 5, 2025

If you follow the steps at https://bazel.build/install/windows#install-compilers to install MSYS2 and Visual Studio, sh_binarys should run just fine on Windows (via bazel run) and the native libraries are built for Windows. You can build your own release jars that way.

The publishing script for java_export is unfortunately hardcoded to a Bash script without a launcher even on Windows, so it won't work. I will see whether I can fix that in rules_jvm_external.

}

private static void report(String message) {
Jazzer.reportFindingFromHook(new FuzzerSecurityIssueCritical(message));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might be nice to report also for which Unsafe method this occurred. Since the hooks are run before the actual Unsafe method, it does not appear in the stack trace. For code like unsafe.putLong(dest, destOffset, unsafe.getLong(src, srcOffset)) that makes it a bit more difficult to tell which of the Unsafe calls caused the exception, and you cannot debug inside the Unsafe call either since sanitization already happens before.

Any suggestions for how reporting could be improved (in case you consider this an issue) are welcome.

@Marcono1234
Copy link
Contributor Author

Thanks for the hints!

The publishing script for java_export is unfortunately hardcoded to a Bash script without a launcher even on Windows, so it won't work. I will see whether I can fix that in rules_jvm_external.

I worked around it now by building within a Docker container, but maybe other users would benefit from that as well, so thanks for your work on this.

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.

2 participants