Skip to content

get_dsp() aborts the process on well-formed JSON that is not a NAM model #328

Description

@sgaldeano

Summary

nam::get_dsp() aborts the process when handed well-formed JSON that is not a
NAM model. It does not throw, so consumers cannot recover — the whole application
dies.

This is distinct from #314, which was about malformed JSON leaking
nlohmann::json::parse_error. Here the JSON parses fine; it just lacks the keys
Core expects.

It matters for any consumer that accepts files from the user. Ours loads whatever
gets dropped on the window, so a stray .json — a config file, a package.json,
anything — takes the application down with it.

Reproduction

#include <iostream>
#include "NAM/get_dsp.h"
#include "json.hpp"

int main() {
  const nlohmann::json j = nlohmann::json::parse(R"({"hello": "world"})");
  std::cout << "about to call get_dsp()\n" << std::flush;
  try {
    auto dsp = nam::get_dsp(j);
    std::cout << "loaded (unexpected)\n";
  } catch (const std::exception& e) {
    std::cout << "threw, as one would hope: " << e.what() << "\n";
  }
  return 0;
}

Observed on macOS 15 with AppleClang, -O2:

about to call get_dsp()
Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 22188.
Abort trap: 6

Exit code 134. Adding -DNDEBUG to the get_dsp.cpp translation unit did not
change the outcome in my testing.

Reproduced on v0.5.4; the same unguarded accesses are on main (2563c0f).

Root cause

populate_dsp_data in NAM/get_dsp.cpp indexes three keys with operator[]
without checking that they exist:

verify_config_version(config["version"].get<std::string>());
nlohmann::json config_json = config["config"];
returnedConfig.version = config["version"].get<std::string>();
returnedConfig.architecture = config["architecture"].get<std::string>();

On a const nlohmann::json, operator[] with a missing key asserts, and where
assertions are compiled out it is undefined behaviour. Either way it cannot be
relied on to throw.

weights is handled correctly a few lines up — j.find("weights") followed by a
std::runtime_error — so the fix is to bring the other three in line.

Suggested fix

for (const char* key : {"version", "architecture", "config"}) {
  if (!config.contains(key))
    throw std::runtime_error(std::string("Corrupted model file is missing ") + key + ".");
}

Or config.at(key), which throws nlohmann::json::out_of_range rather than
asserting — though a std::runtime_error would match the surrounding style and
what consumers already catch.

A type check on version and architecture would help too: {"version": 7, ...}
reaches .get<std::string>() and throws a json::type_error, which is a
different exception type from the std::runtime_error the rest of the loading
path uses.

Workaround for consumers

Validate before calling in:

if (!config.is_object() ||
    !config.contains("version")      || !config["version"].is_string() ||
    !config.contains("architecture") || !config["architecture"].is_string() ||
    !config.contains("config")) {
  return /* your own load error */;
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions