self::MAX_BYTES ) { throw new \RuntimeException( 'Unreadable file or file exceeds the 16 MiB integrity-stream limit; not inspected.' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Bounded native reads bind fstat/lstat identity and streamed hashes to one handle; whole-file filesystem APIs break the scan memory and race bounds. $stream = @fopen( $path, 'rb' ); if ( ! $stream ) { throw new \RuntimeException( 'Could not open file for bounded integrity hashing.' ); } try { $opened = fstat( $stream ); if ( ! self::same( $before, $opened ) ) { throw new \RuntimeException( 'File changed before integrity hashing.' ); } $sha = hash_init( 'sha256' ); $md5 = hash_init( 'md5' ); $bytes = 0; $deadline = microtime( true ) + 2; while ( ! feof( $stream ) ) { if ( microtime( true ) > $deadline ) { throw new \RuntimeException( 'File integrity hashing exceeded its time limit; assessment remains incomplete.' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Bounded native reads bind fstat/lstat identity and streamed hashes to one handle; whole-file filesystem APIs break the scan memory and race bounds. $part = fread( $stream, self::CHUNK_BYTES ); if ( false === $part || ( '' === $part && ! feof( $stream ) ) ) { throw new \RuntimeException( 'File integrity stream could not be read completely.' ); } $bytes += strlen( $part ); if ( $bytes > self::MAX_BYTES || $bytes > $opened['size'] ) { throw new \RuntimeException( 'File changed or exceeded the integrity-stream limit.' ); } hash_update( $sha, $part ); hash_update( $md5, $part ); } clearstatcache( true, $path ); $after = @lstat( $path ); $ended = fstat( $stream ); if ( ! $after || ! self::same( $opened, $after ) || ! self::same( $opened, $ended ) || $bytes !== $opened['size'] ) { throw new \RuntimeException( 'File changed during integrity hashing; rescan required.' ); } PathGuard::resolve( $root, $relative ); $hash = hash_final( $sha ); return array( 'hash' => $hash, 'digests' => array( 'sha256' => $hash, 'md5' => hash_final( $md5 ) ), 'stat' => $after, 'path' => $path, 'structural_limit' => true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Bounded native reads bind fstat/lstat identity and streamed hashes to one handle; whole-file filesystem APIs break the scan memory and race bounds. } finally { fclose( $stream ); } } private static function same( array $left, array $right ): bool { if ( 0100000 !== ( $right['mode'] & 0170000 ) || 1 !== $right['nlink'] ) { return false; } foreach ( array( 'ino', 'dev', 'size', 'mtime', 'ctime', 'mode' ) as $key ) { if ( $left[ $key ] !== $right[ $key ] ) { return false; } } return true; } }