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 11 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!

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).

Copy link
Contributor

Choose a reason for hiding this comment

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

Are sanitizers generally not enabled when running in reproducer / regression (?) mode?

They are, but the bazel rule java_fuzz_target_test in the Jazzer repo cannot handle FuzzerSecurityIssueCritical findings in regression mode (we correctly get exit code 77, but no finding information). Testing with maven/gradle + JUnit, everything works as expected (each @FuzzTest throws findings in regression mode).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the clarification! So keeping verify_crash_reproducer = False here is fine, right?

A bit related: Maybe the comments "not clear why reproducer doesn't work TODO -- fix this" for the FilePathTraversal tests was then also a misunderstanding of how this works?
(though maybe those comments should be removed in a separate PR and not as part of this one)

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.

@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.

@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.

Copy link
Contributor

@oetr oetr left a comment

Choose a reason for hiding this comment

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

Thanks for this awesome contribution!
I am not familiar with the unsafe, but this PR looks good already!

} else if (c == long.class || c == double.class) {
bytesCount = 8;
} else {
throw new AssertionError("Unexpected type: " + c);
Copy link
Contributor

Choose a reason for hiding this comment

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

Sanitizers are only allowed to throw/report findings.
If the program reaches here, the hook should simply return.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean the hook should return and do not perform any sanitization?

Though if this AssertionError occurs here it would indicate a bug in this sanitizer implementation. And it is unlikely that it will occur given that the method hooks explicitly define the names of the Unsafe methods to intercept; this error would only occur if new sun.misc.Unsafe method overloads are added which break the assumptions of this sanitizer implementation.

So is it fine to keep the AssertionError here?

// Assumes that `Unsafe.ARRAY_BOOLEAN_INDEX_SCALE > 0`
bytesCount = 1;
} else if (c == byte.class) {
bytesCount = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to hard-code these? Why not use Unsafe.ARRAY_BOOLEAN_INDEX_SCALE, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess technically they should be identical1, though this method is more about "how large is an int, ...".

So would you prefer usage of the ..._INDEX_SCALE values here then?

Footnotes

  1. If for some reason the ..._INDEX_SCALE was larger than the corresponding size of the primitive (meaning there are gaps between array elements), then probably lots of Unsafe usage would be broken anyway. So I doubt that this is the case for any JDK implementation, and it might not be permitted by the Unsafe documentation in the first place.

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

Choose a reason for hiding this comment

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

Are sanitizers generally not enabled when running in reproducer / regression (?) mode?

They are, but the bazel rule java_fuzz_target_test in the Jazzer repo cannot handle FuzzerSecurityIssueCritical findings in regression mode (we correctly get exit code 77, but no finding information). Testing with maven/gradle + JUnit, everything works as expected (each @FuzzTest throws findings in regression mode).

@Marcono1234
Copy link
Contributor Author

Side note: The current test failure for the GitHub workflow seems to be unrelated to my changes:

 [5,521 / 5,709] 123 / 336 tests; Testing //sanitizers/src/test/java/com/example:OsCommandInjectionProcessBuilder; 300s local ... (4 actions running)

TIMEOUT: //sanitizers/src/test/java/com/example:OsCommandInjectionProcessBuilder (Summary)
      C:/users/runneradmin/_bazel_runneradmin/2gbstkcc/execroot/_main/bazel-out/x64_windows-opt/testlogs/sanitizers/src/test/java/com/example/OsCommandInjectionProcessBuilder/test.log

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.

3 participants