From edbe6a6f74ac0927b7276efe01c8487d7d9d5009 Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Wed, 10 Jun 2026 06:06:12 -0700 Subject: [PATCH] Fix memory and resource leaks in failing resource provider open callbacks When an mjpResourceProvider's open callback returns 0 (failure), MuJoCo does not invoke the corresponding close callback. Previously, several resource provider implementations allocated heap memory or system file handles before encountering an error, failing to clean them up before returning 0. This change addresses these leaks across the codebase and clarifies the API contract. PiperOrigin-RevId: 929803942 Change-Id: Ib7344033726895cdf037a8d9356ffc183c78c2d3 --- doc/APIreference/APItypes.rst | 5 ++++- include/mujoco/mjplugin.h | 5 ++++- python/mujoco/experimental/studio/native_viewer.cc | 8 +++++--- src/experimental/studio/main.cc | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index eda4dc70..355eb3a7 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -1871,7 +1871,10 @@ mjfOpenResource typedef int (*mjfOpenResource)(mjResource* resource); -This callback is for opening a resource; returns zero on failure. +This callback is for opening a resource; returns zero on failure. Note that +if this callback returns zero, the ``close`` callback will not be called. +Therefore, the ``open`` callback is responsible for cleaning up any allocated +memory or resources before returning zero to avoid memory leaks. .. _mjfReadResource: diff --git a/include/mujoco/mjplugin.h b/include/mujoco/mjplugin.h index a0ee0076..e1656d34 100644 --- a/include/mujoco/mjplugin.h +++ b/include/mujoco/mjplugin.h @@ -33,7 +33,10 @@ struct mjResource_ { }; typedef struct mjResource_ mjResource; -// callback for opening a resource, returns zero on failure +// callback for opening a resource, returns zero on failure. +// Note: If opening fails, the close callback will not be called. Therefore, the +// open callback is responsible for cleaning up any allocated memory before +// returning 0. typedef int (*mjfOpenResource)(mjResource* resource); // callback for reading a resource diff --git a/python/mujoco/experimental/studio/native_viewer.cc b/python/mujoco/experimental/studio/native_viewer.cc index 71d6cf51..3cc587d4 100644 --- a/python/mujoco/experimental/studio/native_viewer.cc +++ b/python/mujoco/experimental/studio/native_viewer.cc @@ -79,6 +79,10 @@ class Viewer { resource_provider.open = [](mjResource* resource) { auto* data = new ResourceData(); data->bytes = LoadAsset(resource->name); + if (data->bytes.empty()) { + delete data; + return 0; + } resource->data = data; return static_cast(data->bytes.size()); }; @@ -135,9 +139,7 @@ class Viewer { bytes_per_pixel); } - std::string GetDropFile() { - return window_->GetDropFile(); - } + std::string GetDropFile() { return window_->GetDropFile(); } void Present(const mujoco::python::MjModelWrapper& model, mujoco::python::MjDataWrapper& data, diff --git a/src/experimental/studio/main.cc b/src/experimental/studio/main.cc index ca84aa4f..c6bc03e8 100644 --- a/src/experimental/studio/main.cc +++ b/src/experimental/studio/main.cc @@ -84,6 +84,10 @@ int main(int argc, char** argv, char** envp) { resource_provider.open = [](mjResource* resource) { const std::string resolved_path = Resolve(resource->name); FileResource* f = new FileResource(resolved_path); + if (f->Size() == 0) { + delete f; + return 0; + } resource->data = f; return f->Size(); };