Skip to content

Commit 65ea54c

Browse files
yuvaltassacopybara-github
authored andcommitted
Fix heap-buffer-overflow and edge cases when parsing GMSH files.
1. Resource buffers are length-delimited, not null-terminated: FileToMemory sizes its vector to the file length, and mj_addBufferVFS copies exactly the bytes it is given. LoadGMSH handed such a buffer straight to sscanf, whose internal scan ran off the end for any GMSH file with no NUL byte after "$MeshFormat" -- that is, every ASCII .msh. Parse a bounded copy of the version line instead. Under -fsanitize=address this aborted ~15 UserFlexTest MSH cases, RecompileCompareTest and WriteReadCompareTest. 2. The "missing $Nodes" and "missing $Elements" checks tested nodebegin/elembegin for -1 after the section name length had already been added to them, so they were never true. Check for sections before offsetting past their names. 3. findstring stopped scanning at buffer_sz - len, one position short of the last possible match, so a marker flush against the end of the buffer was reported as absent. Scan up to buffer_sz - len. 4. Add regression tests for truncated headers, missing sections, and EOF markers. PiperOrigin-RevId: 985075471 Change-Id: Ib62509d1d509d54014b49ce898345c43fe7ab6e7
1 parent dc8bb13 commit 65ea54c

3 files changed

Lines changed: 132 additions & 11 deletions

File tree

‎doc/changelog.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Bug fixes
5757
register the forces that tendons apply to bodies (:issue:`832`).
5858
- :ref:`mjd_transitionFD` and :ref:`mjd_inverseFD` now raise an error when :ref:`sleeping<Sleeping>` is enabled.
5959
Previously, their repeated evaluations changed the sleep state, leading to internal errors or wrong derivatives.
60+
- Fixed an out-of-bounds read when parsing the header of a :ref:`GMSH file<gmsh-file-docs>` loaded by
61+
:ref:`flexcomp<body-flexcomp>`. Truncated headers are now reported as an error.
6062

6163
Version 3.13.0 (September 8, 2026)
6264
----------------------------------

‎src/user/user_flexcomp.cc‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,8 +1367,8 @@ bool mjCFlexcomp::MakeMesh(
13671367
static int findstring(const char* buffer, int buffer_sz, const char* str) {
13681368
int len = (int)strlen(str);
13691369

1370-
// scan buffer
1371-
for (int i = 0; i < buffer_sz - len; i++) {
1370+
// scan buffer, including a match ending at the last byte
1371+
for (int i = 0; i <= buffer_sz - len; i++) {
13721372
// check for string at position i
13731373
bool found = true;
13741374
for (int k = 0; k < len; k++) {
@@ -1965,12 +1965,13 @@ void mjCFlexcomp::LoadGMSH(mjCModel* model, mjResource* resource) {
19651965
throw mjCError(NULL, "GMSH file must begin with $MeshFormat");
19661966
}
19671967

1968-
// check version, determine ascii or binary
1969-
double version;
1970-
int binary;
1971-
if (sscanf(buffer + 11, "%lf %d", &version, &binary) != 2) {
1972-
throw mjCError(NULL, "Could not read GMSH file header");
1973-
}
1968+
// check version, determine ascii or binary; the resource buffer is not
1969+
// null-terminated, so parse a bounded copy of the version line
1970+
constexpr int kGmshVersionLineMax = 64;
1971+
stringstream header(std::string(buffer + 11, std::min(buffer_sz - 11, kGmshVersionLineMax)));
1972+
double version;
1973+
int binary;
1974+
if (!(header >> version >> binary)) { throw mjCError(NULL, "Could not read GMSH file header"); }
19741975
if (mju_round(100 * version) != 220 && mju_round(100 * version) != 410) {
19751976
throw mjCError(NULL, "Only GMSH file format versions 4.1 and 2.2 are supported");
19761977
}
@@ -1982,14 +1983,16 @@ void mjCFlexcomp::LoadGMSH(mjCModel* model, mjResource* resource) {
19821983
int elemend = findstring(buffer, buffer_sz, "$EndElements");
19831984

19841985

1986+
// check sections are present, before begin is offset past the section name
1987+
if (nodebegin < 0) { throw mjCError(NULL, "GMSH file missing $Nodes"); }
1988+
if (elembegin < 0) { throw mjCError(NULL, "GMSH file missing $Elements"); }
1989+
19851990
// correct begin for string size, +1 for LF in binary (CRLF in Win ascii works)
19861991
nodebegin += (int)strlen("$Nodes") + 1;
19871992
elembegin += (int)strlen("$Elements") + 1;
19881993

1989-
// check sections
1990-
if (nodebegin < 0) { throw mjCError(NULL, "GMSH file missing $Nodes"); }
1994+
// check sections are closed
19911995
if (nodeend < nodebegin) { throw mjCError(NULL, "GMSH file missing $EndNodes after $Nodes"); }
1992-
if (elembegin < 0) { throw mjCError(NULL, "GMSH file missing $Elements"); }
19931996
if (elemend < elembegin) {
19941997
throw mjCError(NULL, "GMSH file missing $EndElements after $Elements");
19951998
}

‎test/user/user_flex_test.cc‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// Tests for user/user_model.cc.
1616

1717
#include <array>
18+
#include <cstdio>
1819
#include <memory>
1920
#include <string>
2021

@@ -826,6 +827,121 @@ TEST_F(UserFlexTest, LoadMSHASCII_22_MissingElement_Fail) {
826827
mj_deleteModel(m);
827828
}
828829

830+
// Resource buffers are not null-terminated, so the GMSH header parser must stay
831+
// within the reported size. A file that is exactly "$MeshFormat" leaves nothing
832+
// after the tag; reading on is a heap overflow.
833+
TEST_F(UserFlexTest, LoadMSHTruncatedHeader_Fail) {
834+
static constexpr char msh[] = "$MeshFormat";
835+
836+
// sizeof - 1: the buffer must be added without its terminating null
837+
mjVFS vfs;
838+
mj_defaultVFS(&vfs);
839+
mj_addBufferVFS(&vfs, "truncated.msh", msh, sizeof(msh) - 1);
840+
841+
static constexpr char xml[] = R"(
842+
<mujoco>
843+
<worldbody>
844+
<flexcomp name="test" type="gmsh" dim="3" radius=".001"
845+
file="truncated.msh"/>
846+
</worldbody>
847+
</mujoco>
848+
)";
849+
std::array<char, 1024> error;
850+
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size(), &vfs);
851+
EXPECT_THAT(m.get(), IsNull());
852+
EXPECT_THAT(error.data(), HasSubstr("Could not read GMSH file header"));
853+
mj_deleteVFS(&vfs);
854+
}
855+
856+
TEST_F(UserFlexTest, LoadMSHMissingNodesSection_Fail) {
857+
static constexpr char msh[] =
858+
"$MeshFormat\n4.1 0 8\n$EndMeshFormat\n"
859+
"$Elements\n1 1 1 1\n3 1 4 1\n1 1 1 1 1\n$EndElements\n";
860+
861+
mjVFS vfs;
862+
mj_defaultVFS(&vfs);
863+
mj_addBufferVFS(&vfs, "nonodes.msh", msh, sizeof(msh) - 1);
864+
865+
static constexpr char xml[] = R"(
866+
<mujoco>
867+
<worldbody>
868+
<flexcomp name="test" type="gmsh" dim="3" radius=".001"
869+
file="nonodes.msh"/>
870+
</worldbody>
871+
</mujoco>
872+
)";
873+
std::array<char, 1024> error;
874+
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size(), &vfs);
875+
EXPECT_THAT(m.get(), IsNull());
876+
EXPECT_THAT(error.data(), HasSubstr("GMSH file missing $Nodes"));
877+
mj_deleteVFS(&vfs);
878+
}
879+
880+
TEST_F(UserFlexTest, LoadMSHMissingElementsSection_Fail) {
881+
static constexpr char msh[] =
882+
"$MeshFormat\n4.1 0 8\n$EndMeshFormat\n"
883+
"$Nodes\n1 1 1 1\n3 1 0 1\n1\n0 0 0\n$EndNodes\n";
884+
885+
mjVFS vfs;
886+
mj_defaultVFS(&vfs);
887+
mj_addBufferVFS(&vfs, "noelements.msh", msh, sizeof(msh) - 1);
888+
889+
static constexpr char xml[] = R"(
890+
<mujoco>
891+
<worldbody>
892+
<flexcomp name="test" type="gmsh" dim="3" radius=".001"
893+
file="noelements.msh"/>
894+
</worldbody>
895+
</mujoco>
896+
)";
897+
std::array<char, 1024> error;
898+
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size(), &vfs);
899+
EXPECT_THAT(m.get(), IsNull());
900+
EXPECT_THAT(error.data(), HasSubstr("GMSH file missing $Elements"));
901+
mj_deleteVFS(&vfs);
902+
}
903+
904+
// The last section marker of a GMSH file need not be followed by a newline.
905+
TEST_F(UserFlexTest, LoadMSHEndMarkerAtEOF_Success) {
906+
// read a well-formed GMSH file
907+
const std::string msh_path =
908+
GetTestDataFilePath("user/testdata/cube_41_ascii_vol_gmshApp.msh");
909+
FILE* f = fopen(msh_path.c_str(), "rb");
910+
ASSERT_THAT(f, NotNull()) << "Could not open " << msh_path;
911+
fseek(f, 0, SEEK_END);
912+
long msh_size = ftell(f);
913+
fseek(f, 0, SEEK_SET);
914+
std::string msh(msh_size, '\0');
915+
fread(msh.data(), 1, msh_size, f);
916+
fclose(f);
917+
918+
// drop the newline after the final $EndElements
919+
static constexpr char kEndElements[] = "$EndElements";
920+
size_t end = msh.rfind(kEndElements);
921+
ASSERT_NE(end, std::string::npos);
922+
msh.resize(end + sizeof(kEndElements) - 1);
923+
924+
mjVFS vfs;
925+
mj_defaultVFS(&vfs);
926+
mj_addBufferVFS(&vfs, "cube.msh", msh.data(), msh.size());
927+
928+
static constexpr char xml[] = R"(
929+
<mujoco>
930+
<worldbody>
931+
<flexcomp name="test" type="gmsh" dim="3" radius=".001" file="cube.msh">
932+
<edge equality="true"/>
933+
</flexcomp>
934+
</worldbody>
935+
</mujoco>
936+
)";
937+
std::array<char, 1024> error;
938+
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size(), &vfs);
939+
ASSERT_THAT(m.get(), NotNull()) << error.data();
940+
EXPECT_EQ(m->nflexvert, 14);
941+
EXPECT_EQ(m->nflexelem, 24);
942+
mj_deleteVFS(&vfs);
943+
}
944+
829945
TEST_F(UserFlexTest, LoadMSHASCII_dim_missing_in_xml) {
830946
const std::string xml_path = GetTestDataFilePath(
831947
"user/testdata/cube_22_ascii_vol_gmshApp_missing_dim.xml");

0 commit comments

Comments
 (0)
⌂ 换网址