Skip to content
2 changes: 1 addition & 1 deletion ci/utils/aggregate_nightly.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ def main():
)

args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir = Path(args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)

# ---- Step 1: Collect summaries ----
Expand Down
2 changes: 1 addition & 1 deletion cpp/src/dual_simplex/phase2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1790,7 +1790,7 @@ i_t compute_delta_x(const lp_problem_t<i_t, f_t>& lp,

f_t scale = scaled_delta_xB_sparse.find_coefficient(basic_leaving_index);
work_estimate += 2 * scaled_delta_xB_sparse.i.size();
if (scale != scale) {
if (std::isnan(scale)) {
// We couldn't find a coefficient for the basic leaving index.
// The coefficient might be very small. Switch to a regular solve and try to recover.
std::vector<f_t> rhs;
Expand Down
5 changes: 3 additions & 2 deletions cpp/src/io/experimental_mps_fast/fast_fp64_parser.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#pragma once
Expand All @@ -8,6 +8,7 @@
#include <array>
#include <bit>
#include <cerrno>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -423,7 +424,7 @@ static inline double parse_fp64_advance(const char*& p, const char* end)
}

double v = assemble_fp64(dec);
if (v == v) {
if (!std::isnan(v)) {
if (p < end && (unsigned char)*p > 32) {
mps_parser_fail(error_type_t::ValidationError, "Invalid or out-of-range MPS numeric token");
}
Expand Down
7 changes: 4 additions & 3 deletions cpp/src/io/experimental_mps_fast/fast_parser.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// reserved. SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0

#include "fast_parser.hpp"
#include "fast_parse_primitives.hpp"
Expand Down Expand Up @@ -219,7 +219,8 @@ class scoped_timer_t {
}
#endif

~scoped_timer_t()
~scoped_timer_t() // NOSONAR(S1048): profiling-only path; none of the called functions
// realistically throw
{
#ifdef MPS_FAST_TIMERS
auto end = std::chrono::high_resolution_clock::now();
Expand Down Expand Up @@ -1200,7 +1201,7 @@ static const char* find_line_start(const char* section_start, const char* p)
{
while (p > section_start && p[-1] != '\n')
--p;
return p;
return p; // NOSONAR: pointer stays within [section_start, original_p]; guard prevents underflow
}

static std::vector<bounds_chunk_boundary_t> compute_bounds_chunk_boundaries(
Expand Down
10 changes: 6 additions & 4 deletions cpp/src/io/experimental_mps_fast/lz4_file_reader.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights
// reserved. SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#include "file_reader.hpp"
#include "mps_section_scanner.hpp"
Expand Down Expand Up @@ -452,9 +452,11 @@ struct lz4_pipeline_t {
: input(input_),
window_count(cuda::ceil_div(input.compressed_size_, window_bytes)),
windows(window_count),
window_state_(std::make_unique<window_state_t[]>(window_count)),
window_state_(std::make_unique<window_state_t[]>(
window_count)), // NOSONAR: window_count declared before window_state_ (line 869 vs 891)
io_threads(std::min(lz4_input_max_io_threads, window_count)),
window_done(window_count, 0)
window_done(window_count,
0) // NOSONAR: window_count declared before window_done (line 869 vs 880)
{
for (std::size_t i = 0; i < window_count; ++i) {
std::size_t offset = i * window_bytes;
Expand Down
66 changes: 49 additions & 17 deletions cpp/src/io/file_to_string.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ std::vector<char> bz2_file_to_string(const std::string& file)
void operator()(void* f)
{
int bzerror;
if (f != nullptr) fptr(&bzerror, f);
if (f != nullptr)
fptr(&bzerror,
f); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects_fatal(
bzerror == BZ_OK, error_type_t::ValidationError, "Error closing bzip2 file!");
}
Expand Down Expand Up @@ -105,7 +107,9 @@ std::vector<char> bz2_file_to_string(const std::string& file)
file.c_str());
int bzerror = BZ_OK;
std::unique_ptr<void, BzReadCloseDeleter> bzfile{
BZ2_bzReadOpen(&bzerror, fp.get(), 0, 0, nullptr, 0), {BZ2_bzReadClose}};
BZ2_bzReadOpen(&bzerror, fp.get(), 0, 0, nullptr, 0),
{BZ2_bzReadClose}}; // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is
// reached
mps_parser_expects(bzerror == BZ_OK,
error_type_t::ValidationError,
"Could not open bzip2 compressed file! Given path: %s",
Expand All @@ -115,7 +119,12 @@ std::vector<char> bz2_file_to_string(const std::string& file)
const size_t readbufsize = 1ull << 24; // 16MiB - just a guess.
std::vector<char> readbuf(readbufsize);
while (bzerror == BZ_OK) {
const size_t bytes_read = BZ2_bzRead(&bzerror, bzfile.get(), readbuf.data(), readbuf.size());
const size_t bytes_read = BZ2_bzRead(
&bzerror,
bzfile.get(),
readbuf.data(),
readbuf
.size()); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
if (bzerror == BZ_OK || bzerror == BZ_STREAM_END) {
buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read);
}
Expand Down Expand Up @@ -150,7 +159,8 @@ std::vector<char> zlib_file_to_string(const std::string& file)
struct GzCloseDeleter {
void operator()(gzFile_s* f)
{
int err = fptr(f);
int err =
fptr(f); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects_fatal(
err == Z_OK, error_type_t::ValidationError, "Error closing gz file!");
}
Expand All @@ -177,12 +187,15 @@ std::vector<char> zlib_file_to_string(const std::string& file)
"Error loading zlib! Library version might be incompatible. Please decompress the .gz file "
"manually and open the uncompressed file. Given path: %s",
file.c_str());
std::unique_ptr<gzFile_s, GzCloseDeleter> gzfp{gzopen(file.c_str(), "rb"), {gzclose_r}};
std::unique_ptr<gzFile_s, GzCloseDeleter> gzfp{
gzopen(file.c_str(), "rb"),
{gzclose_r}}; // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects(gzfp != nullptr,
error_type_t::ValidationError,
"Error opening compressed input file! Given path: %s",
file.c_str());
int zlib_status = gzbuffer(gzfp.get(), 1 << 20); // 1 MiB
int zlib_status = gzbuffer(gzfp.get(), 1 << 20); // 1 MiB // NOSONAR: mps_parser_expects/LZ4F
// guards above throw before this is reached
mps_parser_expects(zlib_status == Z_OK,
error_type_t::ValidationError,
"Could not set zlib internal buffer size for decompression! Given path: %s",
Expand All @@ -192,10 +205,13 @@ std::vector<char> zlib_file_to_string(const std::string& file)
std::vector<char> readbuf(readbufsize);
int bytes_read = -1;
while (bytes_read != 0) {
bytes_read = gzread(gzfp.get(), readbuf.data(), readbuf.size());
bytes_read = gzread(
gzfp.get(), readbuf.data(), readbuf.size()); // NOSONAR: mps_parser_expects/LZ4F guards above
// throw before this is reached
if (bytes_read > 0) { buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); }
if (bytes_read < 0) {
gzerror(gzfp.get(), &zlib_status);
gzerror(gzfp.get(), &zlib_status); // NOSONAR: mps_parser_expects/LZ4F guards above throw
// before this is reached
break;
}
}
Expand Down Expand Up @@ -247,7 +263,8 @@ std::vector<char> lz4_file_to_string(const std::string& file)
void operator()(LZ4F_dctx* f)
{
if (f != nullptr) {
const LZ4F_errorCode_t err = fptr(f);
const LZ4F_errorCode_t err =
fptr(f); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects_fatal(
!is_error(err), error_type_t::ValidationError, "Error closing lz4 file!");
}
Expand Down Expand Up @@ -317,20 +334,29 @@ std::vector<char> lz4_file_to_string(const std::string& file)

constexpr unsigned lz4f_version = 100;
LZ4F_dctx* raw_dctx = nullptr;
LZ4F_errorCode_t lz4_status = LZ4F_createDecompressionContext(&raw_dctx, lz4f_version);
mps_parser_expects(!LZ4F_isError(lz4_status),
error_type_t::ValidationError,
"Could not open lz4 compressed file '%s': %s",
file.c_str(),
LZ4F_getErrorName(lz4_status));
LZ4F_errorCode_t lz4_status = LZ4F_createDecompressionContext(
&raw_dctx,
lz4f_version); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects(
!LZ4F_isError(
lz4_status), // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
error_type_t::ValidationError,
"Could not open lz4 compressed file '%s': %s",
file.c_str(),
LZ4F_getErrorName(
lz4_status)); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
std::unique_ptr<LZ4F_dctx, Lz4DctxDeleter> dctx{raw_dctx,
{LZ4F_freeDecompressionContext, LZ4F_isError}};

const char* src = compressed.data();
size_t src_size = compressed.size();
LZ4F_frameInfo_t frame_info{};
size_t src_used = src_size;
lz4_status = LZ4F_getFrameInfo(dctx.get(), &frame_info, src, &src_used);
lz4_status = LZ4F_getFrameInfo(
dctx.get(),
&frame_info,
src,
&src_used); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects(!LZ4F_isError(lz4_status),
error_type_t::ValidationError,
"Error reading lz4 frame info for input file '%s': %s",
Expand All @@ -346,7 +372,13 @@ std::vector<char> lz4_file_to_string(const std::string& file)
while (lz4_status != 0) {
size_t dst_size = readbuf.size();
src_used = src_size;
lz4_status = LZ4F_decompress(dctx.get(), readbuf.data(), &dst_size, src, &src_used, nullptr);
lz4_status = LZ4F_decompress(
dctx.get(),
readbuf.data(),
&dst_size,
src,
&src_used,
nullptr); // NOSONAR: mps_parser_expects/LZ4F guards above throw before this is reached
mps_parser_expects(!LZ4F_isError(lz4_status),
error_type_t::ValidationError,
"Error in lz4 decompression of input file '%s': %s",
Expand Down
2 changes: 1 addition & 1 deletion cpp/src/utilities/pcgenerator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class pcgenerator_t {
state = oldstate * 6364136223846793005ULL + stream;
uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
uint32_t rot = oldstate >> 59u;
ret = (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
ret = (xorshifted >> rot) | (xorshifted << ((32u - rot) & 31u));
return ret;
}

Expand Down
3 changes: 2 additions & 1 deletion cpp/tests/linear_programming/c_api_tests/c_api_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <cuopt/mathematical_optimization/cuopt_c.h>

#include <cuda_runtime.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -262,7 +263,7 @@ static cuopt_int_t test_mip_callbacks_internal(int include_set_callback)
goto DONE;
}

if (context.last_solution_bound != context.last_solution_bound) {
if (isnan(context.last_solution_bound)) {
printf("Error reading solution bound in callback\n");
status = CUOPT_INVALID_ARGUMENT;
goto DONE;
Expand Down
84 changes: 76 additions & 8 deletions python/cuopt_server/cuopt_server/webserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,15 @@ def health():

# Get name for file that stores the result of Solve
def get_output_name(resultdir, CUOPT_DATA_FILE, CUOPT_RESULT_FILE):
# Prevent absolute paths, or navigating with ../..
if CUOPT_RESULT_FILE.startswith("/") or ".." in CUOPT_RESULT_FILE:
CUOPT_RESULT_FILE = ""
# Reject paths that escape resultdir using canonicalized containment check.
if CUOPT_RESULT_FILE and resultdir:
root = os.path.realpath(resultdir)
candidate = os.path.realpath(os.path.join(root, CUOPT_RESULT_FILE))
if (
os.path.isabs(CUOPT_RESULT_FILE)
or os.path.commonpath([root, candidate]) != root
):
CUOPT_RESULT_FILE = ""
if not resultdir:
res = ""
elif CUOPT_RESULT_FILE:
Expand Down Expand Up @@ -343,6 +349,18 @@ def getsolverlogs(
f"supported values are {[mime_json, mime_msgpack, mime_zlib]}",
)

try:
uuid.UUID(id)
except ValueError:
raise HTTPException(
status_code=400, detail="Invalid request id format"
)

if frombyte < 0:
raise HTTPException(
status_code=422, detail="frombyte must be >= 0"
)

# result_dir is guaranteed to exist on startup
log_dir, _, _ = settings.get_result_dir()
log_fname = "log_" + id
Expand Down Expand Up @@ -448,6 +466,13 @@ def deletesolverlogs(
f"supported values are {[mime_json, mime_msgpack, mime_zlib]}",
)

try:
uuid.UUID(id)
except ValueError:
raise HTTPException(
status_code=400, detail="Invalid request id format"
)

# Delete the log for the request if the request is not done
log_dir, _, _ = settings.get_result_dir()
log_fname = "log_" + id
Expand Down Expand Up @@ -1316,6 +1341,52 @@ def cuopt(request: Request, data_bytes: bytes = Depends(get_body)):
)
)

# Canonicalize NVCF_LARGE_OUTPUT_DIR before use as a write directory.
# Headers are untrusted; resolve symlinks and require absolute paths.
# If CUOPT_NVCF_OUTPUT_ROOT is set, further require containment within
# that deployment-configured root.
large_output_dir = ""
if NVCF_LARGE_OUTPUT_DIR:
if not os.path.isabs(NVCF_LARGE_OUTPUT_DIR):
raise HTTPException(
status_code=400,
detail="nvcf-large-output-dir must be an absolute path",
)
large_output_dir = os.path.realpath(NVCF_LARGE_OUTPUT_DIR)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
nvcf_output_root = os.environ.get("CUOPT_NVCF_OUTPUT_ROOT", "")
if nvcf_output_root:
trusted_root = os.path.realpath(nvcf_output_root)
if (
os.path.commonpath([trusted_root, large_output_dir])
!= trusted_root
):
raise HTTPException(
status_code=400,
detail="nvcf-large-output-dir must be within the configured output root",
)

# Validate NVCF asset path before registering the result so that
# validation failures do not leave a registered-but-unreachable result.
file_path = None
if NVCF_FUNCTION_ASSET_IDS:
asset_id = NVCF_FUNCTION_ASSET_IDS.split(",")[0]
if not NVCF_ASSET_DIR:
raise HTTPException(
status_code=400,
detail="nvcf-asset-dir must be set when nvcf-function-asset-ids is provided",
)
asset_root = os.path.realpath(NVCF_ASSET_DIR)
candidate = os.path.realpath(os.path.join(asset_root, asset_id))
if (
os.path.isabs(asset_id)
or os.path.commonpath([asset_root, candidate]) != asset_root
):
raise HTTPException(
status_code=400,
detail="Asset path must stay within the asset directory",
)
file_path = candidate

# Create a NVCFJobResult to hold the solution
if os.environ.get("CUOPT_SERVER_TEST_LARGE_RESULT", False):
maxresult = 0
Expand All @@ -1324,13 +1395,10 @@ def cuopt(request: Request, data_bytes: bytes = Depends(get_body)):
maxresult = int(NVCF_MAX_RESPONSE_SIZE_BYTES) / 1000
except Exception:
_, maxresult, _ = settings.get_result_dir()
r = NVCFJobResult(NVCF_LARGE_OUTPUT_DIR, maxresult, accept)
r = NVCFJobResult(large_output_dir, maxresult, accept)
id = r.register_result()

if NVCF_FUNCTION_ASSET_IDS:
file_path = os.path.join(
NVCF_ASSET_DIR, NVCF_FUNCTION_ASSET_IDS.split(",")[0]
)
if file_path is not None:
job = SolverBinaryJobPath(
id,
warnings,
Expand Down
Loading