fix: correct operator precedence in ForAllFields reverse iteration (#8991)

* fix: correct operator precedence in ForAllFields reverse iteration

The expression `size() - i + 1` evaluates as `(size() - i) + 1` due to
left-to-right associativity, producing an out-of-bounds index when
reverse=true. For a vector of size N, the first iteration (i=0) accesses
index N+1, which is 2 past the last valid index.

Changed to `size() - (i + 1)` to match the correct implementation
already present in bfbs_gen.h:192.

Bug: CWE-125 (Out-of-bounds Read), CWE-783 (Operator Precedence Error)

* test: add ForAllFieldsReverseTest for reverse iteration correctness

Verify that ForAllFields with reverse=true iterates fields in
descending ID order. Tests both Stat (3 fields) and Monster
(many fields with non-sequential definition order) tables.

---------

Co-authored-by: Tulgaa <tulgaa.kek@gmail.com>
diff --git a/src/reflection.cpp b/src/reflection.cpp
index e77d21b..268d7d8 100644
--- a/src/reflection.cpp
+++ b/src/reflection.cpp
@@ -389,7 +389,7 @@
 
   for (size_t i = 0; i < field_to_id_map.size(); ++i) {
     func(object->fields()->Get(
-        field_to_id_map[reverse ? field_to_id_map.size() - i + 1 : i]));
+        field_to_id_map[reverse ? field_to_id_map.size() - (i + 1) : i]));
   }
 }
 
diff --git a/tests/reflection_test.cpp b/tests/reflection_test.cpp
index 1c48918..9f80b93 100644
--- a/tests/reflection_test.cpp
+++ b/tests/reflection_test.cpp
@@ -248,6 +248,93 @@
           true);
 }
 
+// Test that ForAllFields with reverse=true iterates fields in descending
+// ID order. This exercises a fix for an operator precedence bug where the
+// expression `size() - i + 1` was evaluated as `(size() - i) + 1` instead
+// of the correct `size() - (i + 1)`, causing an out-of-bounds read.
+void ForAllFieldsReverseTest(const std::string& tests_data_path) {
+  // Load the binary schema.
+  std::string bfbsfile;
+  TEST_EQ(flatbuffers::LoadFile((tests_data_path + "monster_test.bfbs").c_str(),
+                                true, &bfbsfile),
+          true);
+
+  // Verify the schema.
+  flatbuffers::Verifier verifier(
+      reinterpret_cast<const uint8_t*>(bfbsfile.c_str()), bfbsfile.length());
+  TEST_EQ(reflection::VerifySchemaBuffer(verifier), true);
+
+  auto& schema = *reflection::GetSchema(bfbsfile.c_str());
+
+  // Use the Stat table which has 3 fields with sequential IDs:
+  //   id:string   (id: 0)
+  //   val:long     (id: 1)
+  //   count:ushort (id: 2)
+  auto stat_object = schema.objects()->LookupByKey("MyGame.Example.Stat");
+  TEST_NOTNULL(stat_object);
+  TEST_EQ(stat_object->fields()->size(), 3u);
+
+  // Test forward iteration: fields should come in ascending ID order (0,1,2).
+  {
+    std::vector<uint16_t> forward_ids;
+    flatbuffers::ForAllFields(
+        stat_object, /*reverse=*/false,
+        [&forward_ids](const reflection::Field* field) {
+          forward_ids.push_back(field->id());
+        });
+    TEST_EQ(forward_ids.size(), 3u);
+    TEST_EQ(forward_ids[0], 0);  // id
+    TEST_EQ(forward_ids[1], 1);  // val
+    TEST_EQ(forward_ids[2], 2);  // count
+  }
+
+  // Test reverse iteration: fields should come in descending ID order (2,1,0).
+  // With the old buggy code `size() - i + 1`, at i=0 this would compute
+  // index 3 - 0 + 1 = 4, which is out of bounds for a size-3 vector.
+  {
+    std::vector<uint16_t> reverse_ids;
+    flatbuffers::ForAllFields(
+        stat_object, /*reverse=*/true,
+        [&reverse_ids](const reflection::Field* field) {
+          reverse_ids.push_back(field->id());
+        });
+    TEST_EQ(reverse_ids.size(), 3u);
+    TEST_EQ(reverse_ids[0], 2);  // count (highest ID first)
+    TEST_EQ(reverse_ids[1], 1);  // val
+    TEST_EQ(reverse_ids[2], 0);  // id (lowest ID last)
+  }
+
+  // Also test with the root Monster table which has many fields and
+  // non-sequential definition order vs. ID order (e.g., pos=id:0, hp=id:2,
+  // mana=id:1). This ensures the ID-to-index mapping works correctly.
+  auto root_table = schema.root_table();
+  TEST_NOTNULL(root_table);
+  {
+    std::vector<uint16_t> forward_ids;
+    flatbuffers::ForAllFields(
+        root_table, /*reverse=*/false,
+        [&forward_ids](const reflection::Field* field) {
+          forward_ids.push_back(field->id());
+        });
+    // Verify ascending order.
+    for (size_t i = 1; i < forward_ids.size(); ++i) {
+      TEST_ASSERT(forward_ids[i - 1] < forward_ids[i]);
+    }
+  }
+  {
+    std::vector<uint16_t> reverse_ids;
+    flatbuffers::ForAllFields(
+        root_table, /*reverse=*/true,
+        [&reverse_ids](const reflection::Field* field) {
+          reverse_ids.push_back(field->id());
+        });
+    // Verify descending order.
+    for (size_t i = 1; i < reverse_ids.size(); ++i) {
+      TEST_ASSERT(reverse_ids[i - 1] > reverse_ids[i]);
+    }
+  }
+}
+
 void MiniReflectFlatBuffersTest(uint8_t* flatbuf) {
   auto s =
       flatbuffers::FlatBufferToString(flatbuf, Monster::MiniReflectTypeTable());
diff --git a/tests/reflection_test.h b/tests/reflection_test.h
index fa6f470..da6fb1f 100644
--- a/tests/reflection_test.h
+++ b/tests/reflection_test.h
@@ -10,6 +10,7 @@
 
 void ReflectionTest(const std::string& tests_data_path, uint8_t* flatbuf,
                     size_t length);
+void ForAllFieldsReverseTest(const std::string& tests_data_path);
 void MiniReflectFixedLengthArrayTest();
 void MiniReflectFlatBuffersTest(uint8_t* flatbuf);
 
diff --git a/tests/test.cpp b/tests/test.cpp
index 68e8e68..5a43546 100644
--- a/tests/test.cpp
+++ b/tests/test.cpp
@@ -1774,6 +1774,7 @@
   FixedLengthArrayJsonTest(tests_data_path, false);
   FixedLengthArrayJsonTest(tests_data_path, true);
   ReflectionTest(tests_data_path, flatbuf.data(), flatbuf.size());
+  ForAllFieldsReverseTest(tests_data_path);
   ParseProtoTest(tests_data_path);
   EvolutionTest(tests_data_path);
   UnionDeprecationTest(tests_data_path);