Skip to content

JaggedTensor.from_data_offsets_and_list_ids accepts malformed joffsets, causing OOB reads in downstream ops #722

Description

@swahtz

Summary

JaggedTensor.from_data_offsets_and_list_ids is public in both C++ (src/fvdb/JaggedTensor.cpp:496) and Python (fvdb/jagged_tensor.py:702, bound at src/python/JaggedTensorBinding.cpp:73), but it does not validate the offsets it is handed. It checks only:

  • list_ids.dim() == 2
  • list_ids.numel() == 0 || list_ids.size(0) == offsets.size(0) - 1
  • offsets.dim() == 1

It does not check that offsets are non-negative, non-decreasing, or within the bounds of data. check_valid() (src/fvdb/JaggedTensor.h:661) doesn't either — it only validates shapes, devices and dtypes.

The result is that a JaggedTensor with a structurally invalid joffsets can be constructed from pure Python without error, and the failure surfaces much later as an out-of-bounds read inside whichever op consumes it.

Reproducer

import torch
from fvdb import GridBatch, JaggedTensor

data = torch.randn(100, 3, device="cuda")
offs = torch.tensor([0, 80, 20, 100], dtype=torch.int64, device="cuda")  # decreasing
lids = torch.arange(3, dtype=torch.int32, device="cuda").reshape(-1, 1)

jt = JaggedTensor.from_data_offsets_and_list_ids(data, offs, lids)
torch.cuda.synchronize()
print(jt.joffsets.tolist())   # [0, 80, 20, 100] -- constructed and synchronized, no error

GridBatch.from_points(jt, voxel_sizes=0.1, origins=0.0)

Observed, with GridBatch.from_points as the consumer:

joffsets result
[0, 80, 20, 100] (decreasing) CUDA error 2: out of memory (nanovdb/cuda/DeviceResource.h:35)
[0, -50, 100] (negative) CUDA error 2: out of memory
[0, 50, 10000000] (past end of data) CUDA error 700: an illegal memory access was encountered (nanovdb/tools/cuda/PointsToGrid.cuh:629)

The mechanism for the first two: consumers derive a per-item count as offsets[i+1] - offsets[i], which goes negative and then converts to a huge value when passed to an API taking a size_t count. The third reads past the end of the data buffer.

Note the construction itself completes and survives a cuda.synchronize() — the tensor looks fine until something uses it.

Scope

This is not specific to from_points. Any op that slices jdata using consecutive joffsets entries is affected; _createNanoGridFromIJK (src/fvdb/detail/ops/BuildGridFromIjk.cu:65-75) has the identical startIdx / nVoxels pattern, and that shape is common across the ops. Verified reproducible on main — this is long-standing, not a regression from any recent change.

Where the fix belongs

Raised during review of #719, where the suggestion was to add host-side bounds validation inside the op. That was reverted deliberately: individual ops should be able to trust that a JaggedTensor they are handed is structurally valid, rather than each one re-deriving that check. Pushing it into every consumer multiplies the same logic across the codebase and still leaves any op that forgets it exposed.

That from_jdata_joffsets_jidx_and_lidx_unsafe exists and is explicitly named "unsafe" (and is not exposed to Python) suggests the intent is already that the non-unsafe constructors are safe — they just aren't yet.

Design consideration

Validating offsets means inspecting their values, and offsets typically lives on the device, so a naive check adds a device-to-host sync to every construction. Worth deciding between:

  1. Validate in the public constructors only (from_data_offsets_and_list_ids, from_data_indices_and_list_ids), accepting one sync at construction, and keep ..._unsafe as the no-check escape hatch for internal hot paths.
  2. Validate with a small device-side kernel that sets an error flag, avoiding the sync but deferring the error.
  3. Add an explicit validate() and document that the raw-structure constructors are unchecked — cheapest, but leaves the public Python API able to produce a tensor that crashes the process.

Option 1 seems most consistent with the existing unsafe naming, but the sync cost on batched construction paths is worth measuring first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcore libraryCore fVDB library. i.e. anything in the _Cpp module (C++) or fvdb python moduletriageNeeds team review

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions