Fix bad reads of nested cmpd/vlen types in JNI#6413
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes multiple JNI translation bugs affecting H5DreadVL/H5DwriteVL for nested datatype combinations (notably compound members containing VLEN, and VLEN of nested compounds), which previously could yield empty Java ArrayList results or crash (SIGSEGV). It also adds regression tests in the Java JNI test suite to cover these cases.
Changes:
- Fix compound read translation to always append members via
ArrayList.add()(instead of incorrectly attempting array element assignment when the caller pre-allocates row slots). - Fix VLEN write translation to convert
ArrayListinputs viatoArray()before performing array-style operations. - Add new JUnit tests for compound-of-VLEN reads (preallocated and non-preallocated), VLEN-of-compound reads with null slots, and VLEN-of-nested-compound reads; update expected test output.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
java/src-jni/test/TestH5D.java |
Adds helper + new regression tests covering nested compound/VLEN read/write scenarios. |
java/src-jni/test/testfiles/JUnit-TestH5D.txt |
Updates expected JUnit output list/count for the added tests. |
java/src-jni/jni/h5util.c |
Adjusts JNI buffer translation logic for compound/VLEN read/write paths to correctly use ArrayList semantics and correct member offsets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
e65f573 to
af4e258
Compare
1837946 to
2adb07a
Compare
|
After looking over this for a while, I have several comments, some related to the changes in this PR and some unrelated:
|
Review ChecklistThis PR touches the following areas. Each needs a sign-off
|
`translate_rbuf`'s H5T_VLEN case had a similar bug where when `found_jList` was set to false due to an entyr in `ret_buf` being null, `ret_buf.add()` would be invoked on an array of objects without the list .add() method. This would occur whenever a read was invoked of a vlen sequence with a null (non-preallocated) entry. The pre-existing tests only tested the pre-allocated cases. I removed the use of the `found_jList` flag, since it conflated the passing of an unallocated slot with `ret_buf` not being an array. Instead use `ret_buflen == 0` as the check to match the pattern in H5T_INTEGER and other branches. The test for this fix is testH5Dread_vlen_of_compound_nullslot. --- `translate_atomic_rebuf` had two issues related to handling of nested compounds. First, it discarded recursive returns, resulting in the construction of empty lists. Secondly, its member offset (`char_buf + i * typeSize + memb_offset`) was incorrect. In this case, `i` was the member index and `memberSize` was the entire cmpd size, so the offset would be erroneously large. It seems like this came from copying of the offset computation from `translate_rbuf`, which had to advance over entire elements of compound data. This error was duplicated on the write side in `translate_atomic_wbuf`'s H5T_COMPOUND case (h5util.c:4611). I changed `translate_atomic_rbuf` to capture the resultant object, and dropped the `i * typeSize` term in both routines. The new test verifying the fix works is `testH5Dread_vlen_of_nested_compound`.
54f0c88 to
8c5c5f4
Compare
|
@jhendersonHDF - 10294 |
jhendersonHDF
left a comment
There was a problem hiding this comment.
A couple minor comments but I think this is a solid improvement over the previous logic. The comments about future changes can be ignored for now, but should probably be dealt with eventually. There are also a few minor CI failures to be fixed.
| /* Convert element to a vlen element */ | ||
| hvl_t vl_elem; | ||
|
|
||
| jsize jnelmts = ENVPTR->GetArrayLength(ENVONLY, in_obj); |
There was a problem hiding this comment.
As a future minor optimization, if it's known that this will be a flat ArrayList, just calling .size() would be a little better than converting to an array just to get the length. This also looks like another case of losing track of / not validating the expected format of the buffer before operating on it.
| if (mToArray == NULL) | ||
| CHECK_JNI_EXCEPTION(ENVONLY, JNI_FALSE); | ||
| jobjectArray array = (jobjectArray)ENVPTR->CallObjectMethod(ENVONLY, in_obj, mToArray); | ||
| jsize jnelmts = ENVPTR->GetArrayLength(ENVONLY, array); |
There was a problem hiding this comment.
Another future note, this particular case looks like the data model should be changed, as Object[] should cover this case without needing to involve ArrayList.
| * and the parameter <i>data</i> can be any multi-dimensional array of numbers, such as float[][], or | ||
| * int[][][], or Double[][]. | ||
| * <p> | ||
| * <b>Buffer data model</b> |
There was a problem hiding this comment.
Could you also leave a similar header comment on the translate_wbuf() / translate_rbuf() functions so that the data model is documented right where the data is transformed? For those working in the JNI that will probably be easier to find.
| * | ||
| * Caller-side pre-allocation of per-element slots is not supported. | ||
| * Any object already present in a Java array slot is overwritten. */ | ||
| retIsList = ENVPTR->IsInstanceOf(ENVONLY, ret_buf, arrCList); |
There was a problem hiding this comment.
Future note: ideally, this should be split out so that translate_rbuf() isn't called recursively and doesn't add to the type confusion; separate methods should exist for separate types, even if those only shape the data into something appropriate for a common helper function.
| } | ||
| case H5T_INTEGER: | ||
| case H5T_ENUM: | ||
| case H5T_BITFIELD: |
There was a problem hiding this comment.
Opaque types probably need to be left out of this particular verification or the checks need changed for them. Since they can be arbitrary size, you're not likely to find a matching Java type for them (they're just written as a byte[] for each element). It looks like translate_atomic_wbuf() is handling this incorrectly as well. I believe the logic should be the same as for H5T_REFERENCE.
When H5DreadVL is called with a
H5T_COMPOUNDmemory type whose members include variable-length data, and the caller pre-allocates each row slot in the outerObject[]buffer, then the read completes without throwing an error, but each pre-allocated row ArrayList comes back empty.If the caller follows the alternate calling pattern, leaving each row slot null and letting the native code allocate the per-row record, then the same read works correctly.
The root cause is the
H5T_COMPOUNDcase intranslate_rbuf. This function walks each row's compound, decodes each member into a Java object viatranslate_atomic_rbuf, and stores the result in the per-row ArrayList. It branches on afound_jListflag based on the result ofGetObjectArrayElement(), i.e. whether the caller pre-allocated the row slot.Later on, this flag is used to decide between using the
.add()method of ArrayList (iffound_jListis false), or trying to directly set an element on a builtin java array (if found_jList is true).The latter case is wrong in two ways.
jListis always an ArrayList, either fetched from ret_buf[i] (where each slot is an ArrayList) or newly created a few lines earlier. The attempt to directly set an element using the built-in array syntax silently no-ops.There are currently no tests covering
H5DreadVL()for H5T_COMPOUND in the src-jni test tree. This was discovered in the context of HDFView attempting to read compound-of-sequence datasets. HDFView's H5Datatype.allocateArray pre-fills each row slot with new ArrayList<>() before handing the buffer to H5DreadVL, which triggered the bug.My fix is to replace the use the ArrayList.add() method in every case, and drop the found_jList tracking since jList is always an ArrayList and we always want to use the .add() method. This also causes it to match the pattern of the H5T_VLEN branch, which works for both pre-allocated and non-pre-allocated slots.
To test this case, I added
test.TestH5D.testH5Dread_compound_of_vlen_preallocated. For the sake of regression testing, I also added testH5Dread_compound_of_vlen which tests the same thing with non-pre-allocated row slots, although this test passed even before I made any changes to the source code.I also fixed a very similar problem in H5DwriteVL that led to a SIGSEGV when writing compound of variable-length sequences. Specifically,
translate_atomic_wbuftreatsin_objas a java array when it is an ArrayList, producing a garbage length value that eventually leads to a crash.I changed the H5T_VLEN case to behave similarly to the H5T_ARRAY and H5T_COMPOUND cases and use
mToArrayon the provided ArrayList to get an array object for the array operations. The test verifying the fix istestH5Dwrite_compound_of_vlen.translate_rbuf's H5T_VLEN case had a similar bug where whenfound_jListwas set to false due to an entry inret_bufbeing null.ret_buf.add()would then be invoked on an array of objects without the list .add() method. This would occur whenever a read was invoked of a vlen sequence with a null (non-preallocated) entry. The pre-existing tests only tested the pre-allocated cases.I removed the use of the
found_jListflag, since it conflated the passing of an unallocated slot withret_bufnot being an array. Instead, it now usesret_buflen == 0as the check to match the pattern in H5T_INTEGER and other branches.The test for this fix is
testH5Dread_vlen_of_compound_nullslot.translate_atomic_rebufhad two issues related to handling of nested compounds. First, it discarded recursive return values, resulting in the construction of empty lists. Secondly, its member offset (char_buf + i * typeSize + memb_offset) was incorrect. In this case,iwas the member index andmemberSizewas the entire cmpd size, so the offset would be erroneously large. It seems like this came from copying of the offset computation fromtranslate_rbuf, which had to advance over entire elements of compound data. This error was duplicated on the write side intranslate_atomic_wbuf's H5T_COMPOUND case (h5util.c:4611).I changed
translate_atomic_rbufto capture the resultant object, and dropped thei * typeSizeterm in both routines.The new test verifying the fix is
testH5Dread_vlen_of_nested_compound.Fixes #6467