Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion NAM/activations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,19 @@ nam::activations::ActivationConfig nam::activations::ActivationConfig::from_json
// If it's an object, parse type and parameters
if (j.is_object())
{
std::string type_str = j["type"].get<std::string>();
// `j` is a const reference, so `j["type"]` would resolve to nlohmann's const
// `operator[]`, which asserts (UB under -DNDEBUG) if "type" is missing. Look it up
// through `find()` instead so a missing field throws instead of crashing.
const auto type_it = j.find("type");
if (type_it == j.end())
{
throw std::runtime_error("Activation config: missing required field 'type'");
}
if (!type_it->is_string())
{
throw std::runtime_error("Activation config: field 'type' must be a string");
}
std::string type_str = type_it->get<std::string>();
auto it = type_map.find(type_str);
if (it == type_map.end())
{
Expand Down
13 changes: 9 additions & 4 deletions NAM/get_dsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "registry.h"
#include "json.hpp"
#include "get_dsp.h"
#include "json_util.h"
#include "model_config.h"

namespace nam
Expand Down Expand Up @@ -141,13 +142,17 @@ std::vector<float> GetWeights(nlohmann::json const& j)

void populate_dsp_data(const nlohmann::json& config, dspData& returnedConfig)
{
verify_config_version(config["version"].get<std::string>());
static constexpr const char* kContext = "Model file";

nlohmann::json config_json = config["config"];
const std::string version = nam::util::RequireValue<std::string>(config, "version", kContext);
verify_config_version(version);

const nlohmann::json& config_json = nam::util::RequireField(config, "config", kContext);
const std::string architecture = nam::util::RequireValue<std::string>(config, "architecture", kContext);
std::vector<float> weights = GetWeights(config);

returnedConfig.version = config["version"].get<std::string>();
returnedConfig.architecture = config["architecture"].get<std::string>();
returnedConfig.version = version;
returnedConfig.architecture = architecture;
returnedConfig.config = config_json;
returnedConfig.metadata = config.value("metadata", nlohmann::json());
returnedConfig.weights = weights;
Expand Down
25 changes: 25 additions & 0 deletions NAM/get_dsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,32 @@ struct DspLoadOptions
std::optional<bool> prewarm = std::nullopt;
};

// A note on exceptions: `.nam` files are untrusted input (they're downloaded from the
// internet), and the functions below are the load path for them. Malformed input is reported
// by throwing--most commonly a `std::runtime_error` raised by a `NAM/json_util.h` validation
// helper, but some code paths still surface a raw `nlohmann::json` parse/type/out-of-range
// exception (`nlohmann::detail::exception`, which derives from `std::exception` but NOT from
// `std::runtime_error`). Callers should therefore `catch (const std::exception&)`, not
// `catch (const std::runtime_error&)`--the latter will miss some malformed-input cases and
// the exception will propagate past the handler (`std::terminate()` if nothing else catches
// it).

/// \brief Get NAM from a .nam file at the provided location
/// \param config_filename Path to the .nam model file
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws std::exception (typically std::runtime_error, but see the note above) if the file
/// doesn't exist or the model file is malformed (missing/invalid required fields,
/// unsupported version, etc.)
std::unique_ptr<DSP> get_dsp(const std::filesystem::path config_filename, DspLoadOptions options = DspLoadOptions());

/// \brief Get NAM from a provided configuration struct
/// \param conf DSP data structure containing model configuration and weights
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws std::exception (typically std::runtime_error, but see the note above) if the model
/// configuration is malformed (missing/invalid required fields, unsupported version,
/// etc.)
std::unique_ptr<DSP> get_dsp(dspData& conf, DspLoadOptions options = DspLoadOptions());

/// \brief Get NAM from a .nam file and store its configuration
Expand All @@ -96,6 +112,9 @@ std::unique_ptr<DSP> get_dsp(dspData& conf, DspLoadOptions options = DspLoadOpti
/// \param returnedConfig Output parameter that will be filled with the model data
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws std::exception (typically std::runtime_error, but see the note above) if the file
/// doesn't exist or the model file is malformed (missing/invalid required fields,
/// unsupported version, etc.)
std::unique_ptr<DSP> get_dsp(const std::filesystem::path config_filename, dspData& returnedConfig,
DspLoadOptions options = DspLoadOptions());

Expand All @@ -104,13 +123,19 @@ std::unique_ptr<DSP> get_dsp(const std::filesystem::path config_filename, dspDat
/// \param returnedConfig Output parameter that will be filled with the model data
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws std::exception (typically std::runtime_error, but see the note above) if the model
/// configuration is malformed (missing/invalid required fields, unsupported version,
/// etc.)
std::unique_ptr<DSP> get_dsp(const nlohmann::json& config, dspData& returnedConfig,
DspLoadOptions options = DspLoadOptions());

/// \brief Get NAM from a provided configuration JSON object (convenience overload)
/// \param config JSON configuration object
/// \param options Loading options
/// \return Unique pointer to a DSP object
/// \throws std::exception (typically std::runtime_error, but see the note above) if the model
/// configuration is malformed (missing/invalid required fields, unsupported version,
/// etc.)
std::unique_ptr<DSP> get_dsp(const nlohmann::json& config, DspLoadOptions options = DspLoadOptions());

/// \brief Get sample rate from a .nam file
Expand Down
110 changes: 110 additions & 0 deletions NAM/json_util.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "json_util.h"

#include <sstream>
#include <stdexcept>

namespace nam
{
namespace util
{
const nlohmann::json& RequireField(const nlohmann::json& j, const char* key, const char* context)
{
if (!j.is_object())
{
throw std::runtime_error(std::string(context) + ": expected a JSON object containing '" + key + "'");
}
const auto it = j.find(key);
if (it == j.end())
{
throw std::runtime_error(std::string(context) + ": missing required field '" + key + "'");
}
return *it;
}

namespace
{
// nlohmann::json's `.get<int>()` silently truncates a stored JSON float (e.g. `4.9` -> `4`)
// rather than rejecting it, which would let a hostile file smuggle a non-integral value
// through a dimension check. Require the underlying value to actually be a JSON integer.
int RequireIntegralValue(const nlohmann::json& value, const char* key, const char* context)
{
if (!value.is_number_integer())
{
throw std::runtime_error(std::string(context) + ": field '" + key + "' must be an integer");
}
return value.get<int>();
}
} // namespace

int RequireDimension(const nlohmann::json& j, const char* key, const char* context, int maxValue)
{
const nlohmann::json& value_json = RequireField(j, key, context);
const int value = RequireIntegralValue(value_json, key, context);
if (value < 1 || value > maxValue)
{
std::stringstream ss;
ss << context << ": field '" << key << "' (" << value << ") must be between 1 and " << maxValue;
throw std::runtime_error(ss.str());
}
return value;
}

int OptionalDimension(const nlohmann::json& j, const char* key, const char* context, int defaultValue, int maxValue)
{
if (!j.is_object())
{
throw std::runtime_error(std::string(context) + ": expected a JSON object containing '" + key + "'");
}
const auto it = j.find(key);
if (it == j.end() || it->is_null())
{
return defaultValue;
}
const int value = RequireIntegralValue(*it, key, context);
if (value < 1 || value > maxValue)
{
std::stringstream ss;
ss << context << ": field '" << key << "' (" << value << ") must be between 1 and " << maxValue;
throw std::runtime_error(ss.str());
}
return value;
}

std::vector<int> RequireIntArray(const nlohmann::json& j, const char* key, const char* context, int minValue,
int maxValue, bool allowEmpty, int maxLength)
{
const nlohmann::json& arr = RequireField(j, key, context);
if (!arr.is_array())
{
throw std::runtime_error(std::string(context) + ": field '" + key + "' must be an array");
}
if (!allowEmpty && arr.empty())
{
throw std::runtime_error(std::string(context) + ": field '" + key + "' must not be empty");
}
if (arr.size() > static_cast<size_t>(maxLength))
{
std::stringstream ss;
ss << context << ": field '" << key << "' has " << arr.size() << " elements, which exceeds the limit of "
<< maxLength;
throw std::runtime_error(ss.str());
}

std::vector<int> values;
values.reserve(arr.size());
for (const auto& element : arr)
{
const int value = RequireIntegralValue(element, key, context);
if (value < minValue || value > maxValue)
{
std::stringstream ss;
ss << context << ": field '" << key << "' contains " << value << ", which must be between " << minValue << " and "
<< maxValue;
throw std::runtime_error(ss.str());
}
values.push_back(value);
}
return values;
}
}; // namespace util
}; // namespace nam
118 changes: 118 additions & 0 deletions NAM/json_util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#pragma once

// Helpers for safely reading required fields out of an untrusted .nam model-file JSON
// document.
//
// nlohmann::json's `operator[]` on a `const` object asserts (via `JSON_ASSERT`, which is
// plain `assert()`) that the requested key exists before dereferencing it. Under
// `-DNDEBUG` (a typical plugin host Release build) a missing key is therefore undefined
// behavior rather than a thrown exception. `.nam` files are downloaded from the internet,
// so any field the loader treats as required must be looked up through the helpers below
// instead of `operator[]`.
//
// Deliberately kept free of Eigen so that consumers of this header don't need to pull it
// in (see NAM/util.h, which does include Eigen).

#include <stdexcept>
#include <string>
#include <vector>

#include "json.hpp"

namespace nam
{
namespace util
{
/// \brief Memory-safety bound for integer dimensions read from a model file (e.g. channel
/// counts, layer counts). This is not a claim about the size of legitimate models--it only
/// exists to stop a hostile value from driving an unbounded allocation.
constexpr int kMaxModelDimension = 1 << 16;

/// \brief Memory-safety bound for the LENGTH of arrays read from a model file (e.g. the
/// "layers" array, or a per-layer "dilations"/"kernel_sizes" array). Each element of such an
/// array typically drives construction of a heap-allocated object (a `Layer`, a `LayerArray`),
/// so an unbounded array length lets a tiny, highly-compressible file request an unbounded
/// number of allocations. This is not a claim about the size of legitimate models--real
/// WaveNets have tens of layers, not thousands--it only exists to bound the cost of parsing a
/// hostile file.
constexpr int kMaxModelArrayLength = 4096;

/// \brief Look up a required key in a JSON object, throwing if it is absent.
/// \param j The JSON object to search
/// \param key The required key
/// \param context Human-readable description of the enclosing object, used in the error
/// message (e.g. "WaveNet layer array 2")
/// \return Reference to the value at `key`
/// \throws std::runtime_error If `j` is not an object or `key` is not present
const nlohmann::json& RequireField(const nlohmann::json& j, const char* key, const char* context);

/// \brief Look up a required key and convert its value to `T`, throwing if the key is
/// absent or the value can't be converted.
/// \param j The JSON object to search
/// \param key The required key
/// \param context Human-readable description of the enclosing object, used in the error
/// message
/// \return The value at `key`, converted to `T`
/// \throws std::runtime_error If `j` is not an object, `key` is not present, or the value
/// can't be converted to `T`
template <typename T>
T RequireValue(const nlohmann::json& j, const char* key, const char* context)
{
const nlohmann::json& value = RequireField(j, key, context);
try
{
return value.get<T>();
}
catch (const nlohmann::json::exception& e)
{
throw std::runtime_error(std::string(context) + ": field '" + key + "' has the wrong type (" + e.what() + ")");
}
}

/// \brief Look up a required integer dimension (e.g. a channel count), enforcing that it
/// falls within `[1, maxValue]`.
/// \param j The JSON object to search
/// \param key The required key
/// \param context Human-readable description of the enclosing object, used in the error
/// message
/// \param maxValue Inclusive upper bound on the returned value
/// \return The validated dimension
/// \throws std::runtime_error If the key is absent, isn't an integer (a JSON float such as
/// `4.9` is rejected rather than silently truncated), or is outside `[1, maxValue]`
int RequireDimension(const nlohmann::json& j, const char* key, const char* context, int maxValue = kMaxModelDimension);

/// \brief Look up an OPTIONAL integer dimension, enforcing that it falls within
/// `[1, maxValue]` when present. Use this for fields that default to a fixed value when
/// absent (e.g. `in_channels` defaulting to 1)--absence is fine, but a present-and-hostile
/// value (e.g. 0, negative, non-integral, or absurdly large) is not.
/// \param j The JSON object to search
/// \param key The optional key
/// \param context Human-readable description of the enclosing object, used in the error
/// message
/// \param defaultValue Value to return if `key` is absent
/// \param maxValue Inclusive upper bound on the returned value
/// \return `defaultValue` if `key` is absent, otherwise the validated value
/// \throws std::runtime_error If `j` is not an object, or `key` is present but isn't an
/// integer or is outside `[1, maxValue]`
int OptionalDimension(const nlohmann::json& j, const char* key, const char* context, int defaultValue,
int maxValue = kMaxModelDimension);

/// \brief Look up a required array of integers, enforcing that every element falls within
/// `[minValue, maxValue]` and that the array itself isn't longer than `maxLength`.
/// \param j The JSON object to search
/// \param key The required key
/// \param context Human-readable description of the enclosing object, used in the error
/// message
/// \param minValue Inclusive lower bound on every element
/// \param maxValue Inclusive upper bound on every element
/// \param allowEmpty Whether an empty array is acceptable
/// \param maxLength Inclusive upper bound on the array's length (see `kMaxModelArrayLength`)
/// \return The validated array
/// \throws std::runtime_error If the key is absent, isn't an array of integers (a JSON float
/// element such as `4.9` is rejected rather than silently truncated), is empty when
/// `allowEmpty` is false, is longer than `maxLength`, or contains an out-of-range
/// element
std::vector<int> RequireIntArray(const nlohmann::json& j, const char* key, const char* context, int minValue,
int maxValue, bool allowEmpty = false, int maxLength = kMaxModelArrayLength);
}; // namespace util
}; // namespace nam
14 changes: 9 additions & 5 deletions NAM/linear.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <complex>
#include <stdexcept>

#include "json_util.h"
#include "registry.h"

#include <unsupported/Eigen/FFT>
Expand Down Expand Up @@ -305,12 +306,15 @@ std::string nam::linear::implementation_to_string(const LinearImplementation imp

nam::linear::LinearConfig nam::linear::parse_config_json(const nlohmann::json& config)
{
static constexpr const char* kContext = "Linear config";

LinearConfig c;
c.receptive_field = config["receptive_field"];
c.bias = config["bias"];
// Default to 1 channel in/out for backward compatibility
c.in_channels = config.value("in_channels", 1);
c.out_channels = config.value("out_channels", 1);
c.receptive_field = nam::util::RequireDimension(config, "receptive_field", kContext);
c.bias = nam::util::RequireValue<bool>(config, "bias", kContext);
// Default to 1 channel in/out for backward compatibility, but a present-and-hostile value
// feeds a buffer resize()--validate it when present.
c.in_channels = nam::util::OptionalDimension(config, "in_channels", kContext, 1);
c.out_channels = nam::util::OptionalDimension(config, "out_channels", kContext, 1);
c.implementation = parse_implementation(config.value("implementation", "auto"));
return c;
}
Expand Down
Loading