Merge pull request #3120 from Ashutosh0x:fix/safe-overflow-msvc

PiperOrigin-RevId: 923296001
Change-Id: I037dcc55c91d6eb68b3e63b34a0e059c796b5116
This commit is contained in:
Copybara-Service
2026-05-29 03:12:40 -07:00
2 changed files with 88 additions and 3 deletions
+62
View File
@@ -921,5 +921,67 @@ TEST_F(EngineIoTest, RedZoneAlignmentTest) {
}
#endif
// Regression test: crafted binary model with overflow-inducing sizes must be
// safely rejected (not cause heap overflow). This exercises the overflow checks
// in safeAddToBufferSize on all compilers including MSVC.
TEST_F(EngineIoTest, LoadModelBufferRejectsOverflowingSizes) {
// construct a minimal valid-looking .mjb header
int header[5];
header[0] = 20; // ID
header[1] = sizeof(mjtNum); // floating point size
// We need the correct nsize and nptr from the current build.
// Rather than hardcoding, we create and save a trivial model, then mutate
// the sizes to trigger overflow.
constexpr char xml[] = "<mujoco />";
std::array<char, 1024> error;
mjModel* model = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data();
// save model to a buffer
int bufsize = mj_sizeModel(model);
ASSERT_GT(bufsize, 0);
std::vector<char> buffer(bufsize);
mj_saveModel(model, nullptr, buffer.data(), bufsize);
mj_deleteModel(model);
// locate the size fields in the buffer (after 5-int header)
const int header_bytes = 5 * sizeof(int);
ASSERT_GT(bufsize, header_bytes + 77 * (int)sizeof(mjtSize));
// mutate a size field to an extremely large value that would overflow
// when multiplied by sizeof(type). ntexdata is a good candidate since it
// is a byte count field and gets multiplied by sizeof(mjtByte)==1, but other
// fields multiply by sizeof(int) or sizeof(mjtNum), making overflow easier.
// Use memcpy to avoid undefined behavior from unaligned access.
const int size_offset = header_bytes + 49 * sizeof(mjtSize);
// set to overflow-inducing value
mjtSize overflow_val = static_cast<mjtSize>(SIZE_MAX / 2);
std::memcpy(buffer.data() + size_offset, &overflow_val, sizeof(mjtSize));
// also need to update the nbuffer field (last size) to avoid the early
// nbuffer mismatch check — but the overflow should be caught earlier
// in safeAddToBufferSize/mj_makeModel before we reach that check.
// intercept mju_warning because the test framework translates it to ADD_FAILURE
static bool warning_triggered = false;
warning_triggered = false;
mju_user_warning = [](const char* msg) {
warning_triggered = true;
};
// attempt to load — should return NULL, not crash
mjModel* bad_model = mj_loadModelBuffer(buffer.data(), bufsize);
EXPECT_THAT(bad_model, IsNull())
<< "Expected mj_loadModelBuffer to reject overflow-inducing sizes";
EXPECT_TRUE(warning_triggered) << "Expected a warning about invalid sizes";
// clean up if somehow it succeeded
if (bad_model) {
mj_deleteModel(bad_model);
}
}
} // namespace
} // namespace mujoco