@@ -704,6 +704,236 @@ TEST_F(XNNWeightsCacheTest, MultiplePTEsInSameInstance_NoFileGrowth) {
704704 ::unlink (cache_path.c_str());
705705}
706706
707+ namespace {
708+
709+ // Little-endian decode helpers matching XNNWeightsCache's on-disk format.
710+ uint32_t read_le_u32 (const uint8_t * p) {
711+ uint32_t v = 0 ;
712+ for (int i = 0 ; i < 4 ; ++i) {
713+ v |= static_cast <uint32_t >(p[i]) << (8 * i);
714+ }
715+ return v;
716+ }
717+ uint64_t read_le_u64 (const uint8_t * p) {
718+ uint64_t v = 0 ;
719+ for (int i = 0 ; i < 8 ; ++i) {
720+ v |= static_cast <uint64_t >(p[i]) << (8 * i);
721+ }
722+ return v;
723+ }
724+ void write_le_u32 (std::ostream& f, uint32_t v) {
725+ for (int i = 0 ; i < 4 ; ++i) {
726+ char b = static_cast <char >((v >> (8 * i)) & 0xff );
727+ f.write (&b, 1 );
728+ }
729+ }
730+ void write_le_u64 (std::ostream& f, uint64_t v) {
731+ for (int i = 0 ; i < 8 ; ++i) {
732+ char b = static_cast <char >((v >> (8 * i)) & 0xff );
733+ f.write (&b, 1 );
734+ }
735+ }
736+
737+ } // namespace
738+
739+ // A cache file written by older code (kCacheVersion=1) carries no per-entry
740+ // seed field. Loading such a file with the current schema would yield
741+ // entries with seed=0 and mismatch every fresh look_up. The version bump
742+ // must reject it outright so the next init re-packs from scratch.
743+ TEST_F (XNNWeightsCacheTest, LoadPackedCache_RejectsV1Format) {
744+ std::string cache_path = std::string (" /tmp/xnn_weights_cache_v1_" ) +
745+ std::to_string (::getpid ()) + " .packed_cache" ;
746+ ::unlink (cache_path.c_str());
747+
748+ // v1 layout: 64 bytes of dummy data, then 20-byte footer with version=1.
749+ {
750+ std::ofstream f (cache_path, std::ios::binary);
751+ std::vector<char > data (64 , 0 );
752+ f.write (data.data (), data.size ());
753+ write_le_u64 (f, 64 ); // index_start
754+ write_le_u32 (f, 0 ); // entry_count
755+ write_le_u32 (f, 0x58505743 ); // kCacheMagic "XPWC"
756+ write_le_u32 (f, 1 ); // OLD kCacheVersion = 1
757+ }
758+
759+ XNNWeightsCache cache;
760+ cache.set_packed_cache_path (cache_path);
761+ Error err =
762+ cache.initialize_for_runtime (memory_allocator_.get (), data_map_.get ());
763+ ASSERT_EQ (err, Error::Ok);
764+ // Version mismatch → load_packed_cache returned false → no entries.
765+ EXPECT_EQ (cache.get_packed_data_names ().size (), 0u );
766+
767+ ::unlink (cache_path.c_str());
768+ }
769+
770+ // Verify save_packed_index writes the schema version 2 footer and embeds a
771+ // 4-byte seed field in each entry record. Guards against future refactors
772+ // silently dropping the seed write.
773+ TEST_F (XNNWeightsCacheTest, SavePackedIndex_EntryFormatIncludesSeed) {
774+ std::string cache_path = std::string (" /tmp/xnn_weights_cache_format_" ) +
775+ std::to_string (::getpid ()) + " .packed_cache" ;
776+ ::unlink (cache_path.c_str());
777+
778+ std::vector<size_t > batches{1 , 2 , 3 };
779+ size_t input_channels = 3 ;
780+ size_t output_channels = 4 ;
781+ size_t num_batches = 1 * 2 * 3 ;
782+ size_t padding = 32 ;
783+ std::vector<float > input (num_batches * input_channels + padding, 1 .0f );
784+ std::vector<float > output (num_batches * output_channels, 0 .0f );
785+
786+ {
787+ XNNWeightsCache cache;
788+ cache.set_packed_cache_path (cache_path);
789+ cache.initialize_for_runtime (memory_allocator_.get (), data_map_.get ());
790+ BuildAndRunGraphWithWeightsCache (
791+ cache,
792+ batches,
793+ input_channels,
794+ output_channels,
795+ input.data (),
796+ output.data ());
797+ ASSERT_EQ (cache.save_packed_index (), Error::Ok);
798+ }
799+
800+ // Parse footer at file_size - 20.
801+ std::ifstream f (cache_path, std::ios::binary);
802+ ASSERT_TRUE (f.is_open ());
803+ f.seekg (0 , std::ios::end);
804+ size_t file_size = f.tellg ();
805+ ASSERT_GE (file_size, 24u );
806+
807+ uint8_t footer[20 ];
808+ f.seekg (file_size - 20 );
809+ f.read (reinterpret_cast <char *>(footer), 20 );
810+ uint32_t magic = read_le_u32 (footer + 12 );
811+ uint32_t version = read_le_u32 (footer + 16 );
812+ EXPECT_EQ (magic, 0x58505743u );
813+ EXPECT_EQ (version, 2u );
814+
815+ // Walk first entry:
816+ // [name_len:u32][name][file_offset:u64][data_size:u64][seed:u32]
817+ uint64_t index_start = read_le_u64 (footer);
818+ uint32_t entry_count = read_le_u32 (footer + 8 );
819+ ASSERT_GT (entry_count, 0u );
820+
821+ f.seekg (index_start);
822+ uint8_t name_len_buf[4 ];
823+ f.read (reinterpret_cast <char *>(name_len_buf), 4 );
824+ uint32_t name_len = read_le_u32 (name_len_buf);
825+
826+ // The seed field sits at index_start + 4 + name_len + 8 + 8.
827+ f.seekg (index_start + 4 + name_len + 8 + 8 );
828+ uint8_t seed_buf[4 ];
829+ f.read (reinterpret_cast <char *>(seed_buf), 4 );
830+ // XNNPACK ukernel seeds are non-zero in practice. The signal here is
831+ // simply that 4 well-formed bytes follow the size field — confirming
832+ // the new entry layout was written, not the legacy 16-byte tail.
833+ uint32_t stored_seed = read_le_u32 (seed_buf);
834+ EXPECT_NE (stored_seed, 0u );
835+
836+ ::unlink (cache_path.c_str());
837+ }
838+
839+ // After loading a cache file whose entry seed has been tampered with
840+ // (simulating an XNNPACK upgrade where the same ukernel now emits a
841+ // different seed), the next inference must produce correct output. Either
842+ // look_up's seed check or look_up_or_insert's memcmp fallback drives the
843+ // re-pack; this test exercises the end-to-end safety net.
844+ TEST_F (
845+ XNNWeightsCacheTest,
846+ LoadPackedCache_CorruptedSeed_ProducesCorrectOutput) {
847+ std::string cache_path = std::string (" /tmp/xnn_weights_cache_badseed_" ) +
848+ std::to_string (::getpid ()) + " .packed_cache" ;
849+ ::unlink (cache_path.c_str());
850+
851+ std::vector<size_t > batches{1 , 2 , 3 };
852+ size_t input_channels = 3 ;
853+ size_t output_channels = 4 ;
854+ size_t num_batches = 1 * 2 * 3 ;
855+ size_t padding = 32 ;
856+ std::vector<float > input (num_batches * input_channels + padding, 1 .0f );
857+
858+ // Baseline: fresh pack, heap-only, no cache file.
859+ std::vector<float > baseline (num_batches * output_channels, 0 .0f );
860+ {
861+ XNNWeightsCache cache;
862+ cache.initialize_for_runtime (memory_allocator_.get (), data_map_.get ());
863+ BuildAndRunGraphWithWeightsCache (
864+ cache,
865+ batches,
866+ input_channels,
867+ output_channels,
868+ input.data (),
869+ baseline.data ());
870+ }
871+
872+ // Write a valid cache file.
873+ {
874+ XNNWeightsCache cache;
875+ cache.set_packed_cache_path (cache_path);
876+ cache.initialize_for_runtime (memory_allocator_.get (), data_map_.get ());
877+ std::vector<float > out (num_batches * output_channels, 0 .0f );
878+ BuildAndRunGraphWithWeightsCache (
879+ cache,
880+ batches,
881+ input_channels,
882+ output_channels,
883+ input.data (),
884+ out.data ());
885+ ASSERT_EQ (cache.save_packed_index (), Error::Ok);
886+ }
887+
888+ // Corrupt the seed field of the first entry to a value no real ukernel
889+ // would emit (0xDEADBEEF).
890+ {
891+ std::fstream f (cache_path, std::ios::binary | std::ios::in | std::ios::out);
892+ ASSERT_TRUE (f.is_open ());
893+ f.seekg (0 , std::ios::end);
894+ size_t file_size = f.tellg ();
895+ ASSERT_GE (file_size, 24u );
896+
897+ uint8_t footer_buf[20 ];
898+ f.seekg (file_size - 20 );
899+ f.read (reinterpret_cast <char *>(footer_buf), 20 );
900+ uint64_t index_start = read_le_u64 (footer_buf);
901+ uint32_t entry_count = read_le_u32 (footer_buf + 8 );
902+ ASSERT_GT (entry_count, 0u );
903+
904+ f.seekg (index_start);
905+ uint8_t name_len_buf[4 ];
906+ f.read (reinterpret_cast <char *>(name_len_buf), 4 );
907+ uint32_t name_len = read_le_u32 (name_len_buf);
908+
909+ size_t seed_offset = index_start + 4 + name_len + 8 + 8 ;
910+ f.seekp (seed_offset);
911+ uint32_t corrupted = 0xDEADBEEFu ;
912+ f.write (reinterpret_cast <const char *>(&corrupted), 4 );
913+ f.close ();
914+ }
915+
916+ // Reload and run. Output must still match baseline.
917+ std::vector<float > after_corruption (num_batches * output_channels, 0 .0f );
918+ {
919+ XNNWeightsCache cache;
920+ cache.set_packed_cache_path (cache_path);
921+ cache.initialize_for_runtime (memory_allocator_.get (), data_map_.get ());
922+ ASSERT_GT (cache.get_packed_data_names ().size (), 0u );
923+ BuildAndRunGraphWithWeightsCache (
924+ cache,
925+ batches,
926+ input_channels,
927+ output_channels,
928+ input.data (),
929+ after_corruption.data ());
930+ }
931+
932+ EXPECT_EQ (after_corruption, baseline);
933+
934+ ::unlink (cache_path.c_str());
935+ }
936+
707937// save_packed_index must be a true no-op when no new reserve_space happened
708938// since the last save — same content but writing would still bump mtime,
709939// making the cache file look modified on every model load.
0 commit comments