Skip to content

x86: bound ParseLeaf2 cache-descriptor loop by CPU_FEATURES_MAX_CACHE_LEVEL#462

Open
EylonKrause wants to merge 1 commit into
google:mainfrom
EylonKrause:fix/x86-parseleaf2-cache-overflow
Open

x86: bound ParseLeaf2 cache-descriptor loop by CPU_FEATURES_MAX_CACHE_LEVEL#462
EylonKrause wants to merge 1 commit into
google:mainfrom
EylonKrause:fix/x86-parseleaf2-cache-overflow

Conversation

@EylonKrause

Copy link
Copy Markdown

Description

ParseLeaf2 (legacy CPUID leaf-2 cache parsing in src/impl_x86__base_implementation.inl) appends a CacheLevelInfo to CacheInfo::levels — a fixed CPU_FEATURES_MAX_CACHE_LEVEL (10) array — for every non-zero descriptor byte, but the loop is bounded only by sizeof(data) == 16 and never checks info->size against the array capacity:

for (size_t i = 0; i < sizeof(data); ++i) {
    const uint8_t descriptor = data[i];
    if (descriptor == 0) continue;
    info->levels[info->size] = GetCacheLevelInfo(descriptor);  // OOB once size >= 10
    info->size++;
}

A CPUID leaf 2 advertising more than 10 non-zero descriptors — up to 15 are representable (16 bytes minus the AL count byte) — writes past levels[10], an out-of-bounds write into adjacent memory, reachable from public GetX86CacheInfo() on hardware/hypervisor-controlled CPUID input.

This is the same class of memory corruption fixed back in #190 for the leaf-4 / 0x8000001D path: the sibling ParseCacheInfo already guards its loop with info.size < CPU_FEATURES_MAX_CACHE_LEVEL. The legacy leaf-2 path (ParseLeaf2, added in #80) was left unbounded — this applies the same guard there.

Changes & Impact

  • src/impl_x86__base_implementation.inl: add && info->size < CPU_FEATURES_MAX_CACHE_LEVEL to the ParseLeaf2 loop condition, mirroring ParseCacheInfo. Behavior is byte-identical on real CPUs (which advertise far fewer than 10 leaf-2 descriptors); it only stops the overflow on pathological/hostile input.
  • test/cpuinfo_x86_test.cc: add Leaf2_TooManyDescriptors_DoesNotOverflow, a regression test feeding a leaf-2 with 15 non-zero descriptors.

Testing

Built cpuinfo_x86_test with -fsanitize=address,undefined (CMake + Ninja):

  • Before the fix: the new test triggers AddressSanitizer: stack-buffer-overflow WRITE ... in ParseLeaf2 src/impl_x86__base_implementation.inl:1876.
  • After the fix: the new test passes, and the full cpuinfo_x86_test suite is green (69/69) under ASan/UBSan.

…_LEVEL

ParseLeaf2 appends a CacheLevelInfo to CacheInfo::levels (a fixed
CPU_FEATURES_MAX_CACHE_LEVEL=10 array) for every non-zero leaf-2
descriptor byte, but the loop is bounded only by sizeof(data)==16 and
never checks info->size against the array capacity. A CPUID leaf 2
advertising more than 10 non-zero descriptors (up to 15 are possible)
writes past levels[10] -- an out-of-bounds write reachable from public
GetX86CacheInfo() on hardware/hypervisor-controlled CPUID input. The
sibling ParseCacheInfo already guards its loop with
'info.size < CPU_FEATURES_MAX_CACHE_LEVEL' (added for the same class of
corruption in google#190); apply the same bound to ParseLeaf2.

Adds a regression test (Leaf2_TooManyDescriptors_DoesNotOverflow) that
feeds a leaf 2 with 15 descriptors: it triggers an ASan
stack-buffer-overflow before the fix and passes after.

@gchatelet gchatelet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for the PR.

It might be simpler to just increase CPU_FEATURES_MAX_CACHE_LEVEL to 16 and call it a day.

Comment thread test/cpuinfo_x86_test.cc
@@ -1399,6 +1399,21 @@ flags : fpu mmx sse sse2 pni ssse3 sse4_1 sse4_2
}

// https://www.felixcloutier.com/x86/cpuid#example-3-1--example-of-cache-and-tlb-interpretation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should stay next to its original test.

Comment thread test/cpuinfo_x86_test.cc
});

const auto info = GetX86CacheInfo();
EXPECT_LE(info.size, CPU_FEATURES_MAX_CACHE_LEVEL);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test for equality since the purpose of this test is to saturate the array.

Comment thread test/cpuinfo_x86_test.cc
// Regression test: a CPUID leaf 2 advertising more cache/TLB descriptors
// than CPU_FEATURES_MAX_CACHE_LEVEL must not overflow CacheInfo::levels. AL
// (the low byte of EAX) is the ignored count byte; the other 15 bytes here
// are non-zero descriptors, exceeding the 10-entry levels array.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AL (the low byte of EAX) is the ignored count byte; the other 15 bytes here are non-zero descriptors, exceeding the 10-entry levels array.

This part of the comment is not very clear. It would be best to explain what the crafted value does.

  • Each byte is a descriptor so this is 16 descriptors total which is more than CPU_FEATURES_MAX_CACHE_LEVEL
  • The bit 31 must be set to zero for the leaf to be taken into account
  • The lower byte of EAX is the zero descriptor and so is not taken into account.

Comment thread test/cpuinfo_x86_test.cc
cpu().SetLeaves({
{{0x00000000, 0}, Leaf{0x00000002, 0x756E6547, 0x6C65746E, 0x49656E69}},
{{0x00000001, 0}, Leaf{0x00000F0A, 0x00010808, 0x00000000, 0x3FEBFBFF}},
{{0x00000002, 0}, Leaf{0x01010101, 0x01010101, 0x01010101, 0x01010101}},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment below to indicate that we're forging 16 descriptors (one per byte). AL being discarded. so 15 descriptors.

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.

2 participants