Skip to content

THttpServer/TRootSniffer: exe.json leaks the object returned by the executed method (e.g. TH2::ProjectionY) #22501

@wiso

Description

@wiso

Description

TRootSnifferFull::ProduceExe (the exe.json handler) executes a registered object's method through the interpreter and serializes the returned value, but it never deletes the object the method returns. For a method that returns a freshly allocated, owned object — e.g. TH2::ProjectionX/ProjectionY, which returns a new TH1D — every exe.json request leaks that result.

It is masked when callers reuse the same target name (ProjectionY resets the existing same-named histogram), but with distinct names (or any method that returns a new owned object on each call) the leak is unbounded.

Reproducer

Short, self-contained (THttpServer + a raw-socket HTTP GET, no extra deps):

// ROOT exe.json memory leak: TRootSnifferFull::ProduceExe never frees the
// object returned by the executed method (here TH2::ProjectionY -> a new TH1D).
// Each exe.json call with a fresh target name leaks the result (~3 allocations).
//
// Build: g++ repro.cc $(root-config --cflags --libs) -lRHTTP -lNet -pthread \
//            -fsanitize=address -g -o repro
// Run:   ./repro 200     # LeakSanitizer reports ~3*N leaked allocations
#include "TH2D.h"
#include "THttpServer.h"
#include "TSystem.h"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <atomic>
#include <string>
#include <thread>

static void http_get(int port, const std::string& path) {
  int fd = socket(AF_INET, SOCK_STREAM, 0);
  sockaddr_in a{}; a.sin_family = AF_INET; a.sin_port = htons(port);
  inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
  if (connect(fd, (sockaddr*)&a, sizeof(a)) == 0) {
    std::string r = "GET " + path + " HTTP/1.0\r\n\r\n";
    (void)!write(fd, r.data(), r.size());
    char b[4096]; while (read(fd, b, sizeof b) > 0) {}
  }
  close(fd);
}

int main(int argc, char** argv) {
  const int N = argc > 1 ? atoi(argv[1]) : 200;
  const int port = 9410;
  auto* h = new TH2D("h2", "h2", 128, 0, 128, 1024, 0, 1024);
  for (int i = 0; i < 5000; ++i) h->Fill(i % 128, (i * 7) % 1024);

  THttpServer serv(Form("http:%d", port));
  serv.SetReadOnly(kFALSE);                 // allow exe.json method calls
  serv.Register("/", h);

  std::atomic<bool> stop{false};
  std::thread pump([&]{ while (!stop) { serv.ProcessRequests(); gSystem->Sleep(5); } });
  gSystem->Sleep(300);

  for (int k = 0; k < N; ++k)               // distinct target names -> unbounded leak
    http_get(port, Form("/h2/exe.json?method=ProjectionY&name=_s%d&firstxbin=1&lastxbin=1", k));

  stop = true; pump.join();
  return 0;                                  // AddressSanitizer reports the leak at exit
}

Build & run:

g++ repro.cc $(root-config --cflags --libs) -lRHTTP -lNet -pthread -fsanitize=address -g -o repro
./repro 200

Result (ROOT 6.38.04, Linux x86_64, gcc 15, AddressSanitizer)

Distinct target names _s0 .. _sN3·N leaked allocations (one TH1D + its two internal arrays per call), i.e. it scales linearly with the number of requests:

requests leaked
./repro 20 60 allocations, ~186 KB
./repro 200 600 allocations, ~1.86 MB

With a fixed name (name=_s for every call) it is instead bounded at 3 allocations, because ProjectionY finds and resets the existing same-named histogram.

Leak stack (ASan):

operator new
TStorage::ObjectAlloc
TH2::DoProjection
TClingCallFunc::exec / TMethodCall::Execute       (interpreted method call)
TRootSnifferFull::ProduceExe
THttpServer::ProcessRequest
THttpServer::ProcessRequests

Expected

ProduceExe should manage the lifetime of the object returned by the executed method (e.g. delete it after serialization when it owns it and does not keep it reachable in a directory), so repeated exe.json method calls do not leak.

Setup

  • ROOT 6.38.04 (system package), Linux x86_64, gcc 15, AddressSanitizer/LeakSanitizer.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

Status
Issues

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions