Skip to content

fix(MeshIO): make malformed PLY input an error instead of a wrong mesh - #29

Merged
csparker247 merged 2 commits into
developfrom
fix/ply-read-robustness_20260904
Sep 5, 2026
Merged

fix(MeshIO): make malformed PLY input an error instead of a wrong mesh#29
csparker247 merged 2 commits into
developfrom
fix/ply-read-robustness_20260904

Conversation

@csparker247

Copy link
Copy Markdown
Member

What this is

Three cases where read_ply or write_ply produced a wrong result instead
of an error
. All three predate #28 and are independent of binary write and
endianness — found by code review of that PR, fixed here so #28's scope stays
settled.

Branched off develop.

The bugs

1. Unbounded list count on an unknown element → wrong geometry, no error

A PLY may contain elements read_ply doesn't understand (edge, tristrips,
application-defined). It skips them by reading each list's count and jumping
that many bytes. That count had no bound.

PLY permits a signed count type — property list char double junk is legal.
A count byte of 0xFF reads as -1; as an unsigned byte total that becomes
~1.8e19, multiplying wraps it back to just below zero, and the cast to
std::streamsize gives -8. A seek of -8 skips nothing, so the reader stays
inside the element it meant to step over and parses that payload as vertex data.
With enough bytes remaining, nothing fails:

truth:                    v0=(1,2,3)          v1=(4,5,6)
before   char count 0xFF: v0=(0,-2.54688,0)   v1=(-2.51562,0,-2.46875)   NO ERROR
after    char count 0xFF: throws "list property count 18446744073709551615 exceeds maximum of 1024"

uchar 0xFF (255) was already safe — it runs off the end of the file and
throws. Only a signed count type reaches the silent path.

Fix: bound the count before multiplying. kMaxFaceVertices and
kMaxFaceListLength move from duplicated function-local constants in the two
face helpers to namespace scope, so all three skip sites share one bound.

2. List property on the vertex element → first vertex right, rest garbage

The binary vertex reader sizes each record once by summing its properties'
scalar widths, then does one read per vertex. A list property occupies a count
plus N values, so the record size comes out short and every read after the first
is misaligned. The ASCII path has the same flaw by a different route — it
indexes tokens by property position.

truth:   v0=(1,2,3)  v1=(4,5,6)
before:  v0=(1,2,3)  v1=(-0,-1.08421e-19,-2.00002)   NO ERROR
after:   throws "list property 'extra' on the vertex element is not supported"

Fix: refuse the file. Lists on a vertex element are legal but rare — lists
are conventionally a face thing — and refusing cannot break a working case,
because no such file was ever read correctly.

3. write_ply reported success on a write that failed at flush

Each tier ended with if (!file) throw; — but that ran while the tail of the
data was still in the stream buffer. The final flush happens when the
ofstream is destroyed, and a failure there is swallowed, so a write that
failed only at flush time returned normally on an incomplete file.

Fix: file.close() before the check, in all three tiers. close() performs
that flush and records its failure.

Testing

Four failing tests first, then the fixes; two additional guard tests so the
fixes can't be over-strict (a well-formed unknown element still skips
correctly, and a genuinely huge count still fails).

TestMeshIO 71 → 77 tests. Green in Debug and Release.

Bug 3 needed care to test honestly. My first attempt passed both before and
after the fix — worse than no test, since it looks like coverage. The real test
isolates the failure to the flush and nowhere earlier: RLIMIT_FSIZE at 0 makes
every write to the file fail, and a mesh smaller than the stream buffer means no
write is attempted until close() — so the stream is still good when the old
check ran, and only the flush fails. It's #if defined(__unix__) || defined(__APPLE__)
with the limit and SIGXFSZ disposition restored by an RAII guard, and skips
via GTEST_SKIP where the limit can't be lowered. There is no portable way to
provoke this.

Interaction with #28

#28 also hoists kMaxFaceVertices / kMaxFaceListLength to namespace scope
(so its validate_ply_face_lists can static_assert the writer's limits
against the reader's caps), and it adds a needs_swap argument to the
skip_binary_prop lambda touched here. Expect a conflict in
MeshIO_PLY.hpp on whichever merges second
; the resolution is mechanical —
keep the hoisted constants once, and keep both the bound and the needs_swap
argument.

If this merges first, #28 rebases and drops its duplicate hoist.

csparker247 and others added 2 commits September 4, 2026 18:54
Red phase. Each of these makes a malformed or unusual file produce a wrong
result instead of an error:

- UnknownElementSignedListCount_Throws: PLY allows a signed list-count type.
  A 0xFF count byte read as char is -1, which as an unsigned byte count wraps
  to a small negative seek that skips nothing, so the element's payload is
  parsed as vertex data. Returns two junk vertices, no error.
- VertexElementWithListProperty_Throws (+ _ASCII_): the binary vertex reader
  sizes each record by summing scalar widths and the ASCII reader indexes
  tokens by position, so neither handles a list property on the vertex
  element. The first vertex reads correctly and the rest are garbage.
- WriteFailureInFinalFlush_Throws: write_ply checks the stream while the tail
  is still buffered. Isolated with RLIMIT_FSIZE at 0 and a mesh smaller than
  the stream buffer, so nothing is written until close and only the flush
  fails - the stream is still good when the existing check runs. POSIX-only;
  there is no portable way to provoke it.

Two guard tests pass already, and exist so the fixes cannot be over-strict: a
well-formed unknown element still skips correctly, and a genuinely huge count
still fails.

Found by code review of #28. All three predate that branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three cases where read_ply or write_ply produced a wrong result rather than
failing. All predate #28 and are independent of binary write and endianness.

1. Unknown-element list skip had no bound on the element count. PLY permits a
   signed count type, and a 0xFF count byte read as char is -1: as an unsigned
   byte total that wraps to a negative seek, which skips nothing, so the
   element's payload was parsed as vertex data. The count is now bounded
   before it is multiplied. kMaxFaceVertices and kMaxFaceListLength move from
   duplicated function-local constants in the two face helpers to namespace
   scope so all three skip sites share one bound.

2. A list property on the vertex element was silently misread. The binary
   reader sizes each record by summing its properties' scalar widths and the
   ASCII reader indexes tokens by property position, so neither accounts for a
   count plus N values: the first vertex read correctly and every later one
   came from the wrong offset. Such a file is now refused.

3. write_ply checked the stream while the tail of the data was still buffered.
   The final flush happens when the ofstream is destroyed and its failure is
   swallowed, so a write that failed only at flush time returned normally on
   an incomplete file. All three tiers now close before checking.

Found by code review of #28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
csparker247 added a commit that referenced this pull request Sep 5, 2026
…ary-io_20260904)

The manual MeshLab check was completed; the note still said it was
outstanding. Records what it found and points at #29 for the robustness
fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@csparker247
csparker247 merged commit d192280 into develop Sep 5, 2026
5 checks passed
@csparker247
csparker247 deleted the fix/ply-read-robustness_20260904 branch September 5, 2026 10:28
csparker247 added a commit that referenced this pull request Sep 5, 2026
…ary-io_20260904)

The manual MeshLab check was completed; the note still said it was
outstanding. Records what it found and points at #29 for the robustness
fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant