Summary
Tensor::from_blob creates a non-owning tensor that wraps a raw pointer to slice data. If the tensor outlives its source data (e.g., source is mutated or freed), this is use-after-free / data corruption.
Locations
trading_bots/src/torch/ppo.rs:105-115 — cpu_tensor_from_f32 free function
trading_bots/src/torch/env/vec_env.rs:60-69 — VecEnv::tensor_from_f32 method
Analysis
Most call sites immediately consume the non-owning tensor via .copy_(), which is safe in practice:
ppo.rs:468-480 — all four uses pass the result directly to .copy_()
vec_env.rs:497-506 (in step_into_ring) — all uses pass to .copy_()
However, vec_env.rs:232-233 (in step_into) returns the non-owning tensors as (reward, is_done). These reference self.reward_buf and self.is_done_buf which are long-lived Vec fields, so the backing memory is stable — but the data can be silently overwritten on the next step_into or step_into_ring call. This is safe only as long as the caller processes these tensors before the next env step.
The pattern is fragile and relies on implicit ordering guarantees. Any future refactor that defers consumption of the returned tensors would introduce silent data corruption with no compiler warning.
Suggested Fix
Replace Tensor::from_blob with Tensor::from_slice() (or Tensor::of_slice()) which copies and owns the data. The performance cost is negligible for these small CPU tensors (NPROCS-sized buffers).
Severity
Low — currently safe due to immediate consumption patterns, but fragile and unsafe-block-dependent.
Summary
Tensor::from_blobcreates a non-owning tensor that wraps a raw pointer to slice data. If the tensor outlives its source data (e.g., source is mutated or freed), this is use-after-free / data corruption.Locations
trading_bots/src/torch/ppo.rs:105-115—cpu_tensor_from_f32free functiontrading_bots/src/torch/env/vec_env.rs:60-69—VecEnv::tensor_from_f32methodAnalysis
Most call sites immediately consume the non-owning tensor via
.copy_(), which is safe in practice:ppo.rs:468-480— all four uses pass the result directly to.copy_()vec_env.rs:497-506(instep_into_ring) — all uses pass to.copy_()However,
vec_env.rs:232-233(instep_into) returns the non-owning tensors as(reward, is_done). These referenceself.reward_bufandself.is_done_bufwhich are long-lived Vec fields, so the backing memory is stable — but the data can be silently overwritten on the nextstep_intoorstep_into_ringcall. This is safe only as long as the caller processes these tensors before the next env step.The pattern is fragile and relies on implicit ordering guarantees. Any future refactor that defers consumption of the returned tensors would introduce silent data corruption with no compiler warning.
Suggested Fix
Replace
Tensor::from_blobwithTensor::from_slice()(orTensor::of_slice()) which copies and owns the data. The performance cost is negligible for these small CPU tensors (NPROCS-sized buffers).Severity
Low — currently safe due to immediate consumption patterns, but fragile and unsafe-block-dependent.