|
6 | 6 | #include <cstddef> |
7 | 7 | #include <cstring> |
8 | 8 | #include "../../main/cpp/objectSampler.h" |
| 9 | +#include "vmEntry.h" |
| 10 | + |
| 11 | +// --------------------------------------------------------------------------- |
| 12 | +// ObjectSamplerTestAccessor — friend of ObjectSampler, exposes internals |
| 13 | +// needed by the regression tests. |
| 14 | +// --------------------------------------------------------------------------- |
| 15 | +class ObjectSamplerTestAccessor { |
| 16 | +public: |
| 17 | + static void setActive(ObjectSampler *s, bool v) { |
| 18 | + __atomic_store_n(&s->_active, v, __ATOMIC_RELEASE); |
| 19 | + } |
| 20 | + |
| 21 | + static void callRecordAllocation(ObjectSampler *s, jvmtiEnv *jvmti, |
| 22 | + JNIEnv *jni, jthread thread, |
| 23 | + int event_type, jobject object, |
| 24 | + jclass klass, jlong size) { |
| 25 | + s->recordAllocation(jvmti, jni, thread, event_type, object, klass, size); |
| 26 | + } |
| 27 | +}; |
| 28 | + |
| 29 | +// --------------------------------------------------------------------------- |
| 30 | +// Mock-JVMTI infrastructure for Deallocate regression tests |
| 31 | +// --------------------------------------------------------------------------- |
| 32 | + |
| 33 | +// Read-only buffer the success mocks hand back as a class signature; never |
| 34 | +// modified, so file-scope storage is safe. |
| 35 | +static char g_mock_class_name[] = "Ljava/lang/String;"; |
| 36 | + |
| 37 | +// Test fixture owning the per-test JVMTI function table and Deallocate |
| 38 | +// counter. Each TEST_F gets a fresh instance, which (a) prevents shared |
| 39 | +// static state from leaking between tests, (b) lets the fixture restore |
| 40 | +// the process-global ObjectSampler _active flag in TearDown, and (c) |
| 41 | +// removes the use-after-return hazard of returning a struct whose |
| 42 | +// _jvmtiEnv::functions points into a per-call static. |
| 43 | +class ObjectSamplerDeallocateTest : public ::testing::Test { |
| 44 | +protected: |
| 45 | + jvmtiInterface_1_ tbl{}; |
| 46 | + _jvmtiEnv mock_env{}; |
| 47 | + int deallocate_calls = 0; |
| 48 | + |
| 49 | + // Active fixture pointer used by the C-style mock callbacks to reach |
| 50 | + // per-instance state. Reset in SetUp/TearDown so the mocks never see a |
| 51 | + // stale fixture even if a previous test crashed mid-run. |
| 52 | + static thread_local ObjectSamplerDeallocateTest *active_fixture; |
| 53 | + |
| 54 | + void SetUp() override { |
| 55 | + deallocate_calls = 0; |
| 56 | + tbl = jvmtiInterface_1_{}; |
| 57 | + tbl.Deallocate = &mock_Deallocate; |
| 58 | + mock_env.functions = &tbl; |
| 59 | + active_fixture = this; |
| 60 | + } |
| 61 | + |
| 62 | + void TearDown() override { |
| 63 | + // Restore the process-global singleton flag so subsequent tests start |
| 64 | + // from a known state. |
| 65 | + ObjectSamplerTestAccessor::setActive(ObjectSampler::instance(), false); |
| 66 | + active_fixture = nullptr; |
| 67 | + } |
| 68 | + |
| 69 | + void setMockGetClassSignature( |
| 70 | + jvmtiError(JNICALL *fn)(jvmtiEnv *, jclass, char **, char **)) { |
| 71 | + tbl.GetClassSignature = fn; |
| 72 | + } |
| 73 | + |
| 74 | + // Mock Deallocate increments the per-fixture counter; it never frees the |
| 75 | + // pointer because the mock signature buffer is statically allocated. |
| 76 | + static jvmtiError JNICALL mock_Deallocate(jvmtiEnv * /*env*/, |
| 77 | + unsigned char * /*mem*/) { |
| 78 | + if (active_fixture) { |
| 79 | + ++active_fixture->deallocate_calls; |
| 80 | + } |
| 81 | + return JVMTI_ERROR_NONE; |
| 82 | + } |
| 83 | +}; |
| 84 | + |
| 85 | +thread_local ObjectSamplerDeallocateTest * |
| 86 | + ObjectSamplerDeallocateTest::active_fixture = nullptr; |
| 87 | + |
| 88 | +// GetClassSignature mock: returns JVMTI_ERROR_NONE and writes |
| 89 | +// g_mock_class_name into *signature_ptr. |
| 90 | +static jvmtiError JNICALL mock_GetClassSignature_success( |
| 91 | + jvmtiEnv * /*env*/, jclass /*klass*/, |
| 92 | + char **signature_ptr, char ** /*generic_ptr*/) { |
| 93 | + if (signature_ptr) { |
| 94 | + *signature_ptr = g_mock_class_name; |
| 95 | + } |
| 96 | + return JVMTI_ERROR_NONE; |
| 97 | +} |
| 98 | + |
| 99 | +// GetClassSignature mock: returns an error AND writes a non-NULL sentinel |
| 100 | +// into *signature_ptr (the UAF scenario we are guarding against). |
| 101 | +static jvmtiError JNICALL mock_GetClassSignature_error_with_sentinel( |
| 102 | + jvmtiEnv * /*env*/, jclass /*klass*/, |
| 103 | + char **signature_ptr, char ** /*generic_ptr*/) { |
| 104 | + if (signature_ptr) { |
| 105 | + *signature_ptr = g_mock_class_name; // sentinel: non-NULL despite error |
| 106 | + } |
| 107 | + return JVMTI_ERROR_INVALID_CLASS; |
| 108 | +} |
| 109 | + |
| 110 | +// GetClassSignature mock: returns an error and leaves *signature_ptr at NULL. |
| 111 | +static jvmtiError JNICALL mock_GetClassSignature_error_null( |
| 112 | + jvmtiEnv * /*env*/, jclass /*klass*/, |
| 113 | + char **signature_ptr, char ** /*generic_ptr*/) { |
| 114 | + // Leave *signature_ptr unchanged (NULL as initialised by recordAllocation). |
| 115 | + (void)signature_ptr; |
| 116 | + return JVMTI_ERROR_INVALID_CLASS; |
| 117 | +} |
| 118 | + |
| 119 | +// GetClassSignature mock: returns JVMTI_ERROR_NONE but writes NULL into |
| 120 | +// *signature_ptr — a misbehaving JVMTI impl. |
| 121 | +static jvmtiError JNICALL mock_GetClassSignature_success_null_name( |
| 122 | + jvmtiEnv * /*env*/, jclass /*klass*/, |
| 123 | + char **signature_ptr, char ** /*generic_ptr*/) { |
| 124 | + if (signature_ptr) { |
| 125 | + *signature_ptr = NULL; |
| 126 | + } |
| 127 | + return JVMTI_ERROR_NONE; |
| 128 | +} |
9 | 129 |
|
10 | 130 | // Regression tests for ObjectSampler::normalizeClassSignature, the |
11 | 131 | // guard that recordAllocation uses against null, empty, or malformed |
@@ -87,3 +207,75 @@ TEST(ObjectSamplerTest, NormalizePassesThroughObjectArray) { |
87 | 207 | EXPECT_EQ(out_name, signature); |
88 | 208 | EXPECT_EQ(out_len, strlen("[Ljava/lang/String;")); |
89 | 209 | } |
| 210 | + |
| 211 | +// --------------------------------------------------------------------------- |
| 212 | +// T-01: GetClassSignature returns error with non-NULL sentinel in *signature_ptr. |
| 213 | +// Deallocate MUST NOT be called. |
| 214 | +// --------------------------------------------------------------------------- |
| 215 | +TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledOnErrorWithNonNullSentinel) { |
| 216 | + setMockGetClassSignature(mock_GetClassSignature_error_with_sentinel); |
| 217 | + ObjectSampler *s = ObjectSampler::instance(); |
| 218 | + ObjectSamplerTestAccessor::setActive(s, true); |
| 219 | + ObjectSamplerTestAccessor::callRecordAllocation( |
| 220 | + s, &mock_env, nullptr, nullptr, BCI_ALLOC, |
| 221 | + nullptr, nullptr, 1024); |
| 222 | + EXPECT_EQ(deallocate_calls, 0); |
| 223 | +} |
| 224 | + |
| 225 | +// --------------------------------------------------------------------------- |
| 226 | +// T-02: GetClassSignature succeeds with a valid class name. |
| 227 | +// Deallocate IS called exactly once (on the success path). |
| 228 | +// Note: lookupClass returns -1 because the class map is empty, so the |
| 229 | +// method returns without recording — that is the expected behaviour. |
| 230 | +// --------------------------------------------------------------------------- |
| 231 | +TEST_F(ObjectSamplerDeallocateTest, DeallocateCalledOnceOnGetClassSignatureSuccess) { |
| 232 | + setMockGetClassSignature(mock_GetClassSignature_success); |
| 233 | + ObjectSampler *s = ObjectSampler::instance(); |
| 234 | + ObjectSamplerTestAccessor::setActive(s, true); |
| 235 | + ObjectSamplerTestAccessor::callRecordAllocation( |
| 236 | + s, &mock_env, nullptr, nullptr, BCI_ALLOC, |
| 237 | + nullptr, nullptr, 1024); |
| 238 | + EXPECT_EQ(deallocate_calls, 1); |
| 239 | +} |
| 240 | + |
| 241 | +// --------------------------------------------------------------------------- |
| 242 | +// T-03: GetClassSignature fails and leaves class_name at NULL. |
| 243 | +// Deallocate MUST NOT be called. |
| 244 | +// --------------------------------------------------------------------------- |
| 245 | +TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledOnErrorWithNullName) { |
| 246 | + setMockGetClassSignature(mock_GetClassSignature_error_null); |
| 247 | + ObjectSampler *s = ObjectSampler::instance(); |
| 248 | + ObjectSamplerTestAccessor::setActive(s, true); |
| 249 | + ObjectSamplerTestAccessor::callRecordAllocation( |
| 250 | + s, &mock_env, nullptr, nullptr, BCI_ALLOC, |
| 251 | + nullptr, nullptr, 1024); |
| 252 | + EXPECT_EQ(deallocate_calls, 0); |
| 253 | +} |
| 254 | + |
| 255 | +// --------------------------------------------------------------------------- |
| 256 | +// T-04: GetClassSignature succeeds but writes NULL into *signature_ptr. |
| 257 | +// Deallocate MUST NOT be called (the NULL guard in the condition fires). |
| 258 | +// --------------------------------------------------------------------------- |
| 259 | +TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledWhenSuccessButNullName) { |
| 260 | + setMockGetClassSignature(mock_GetClassSignature_success_null_name); |
| 261 | + ObjectSampler *s = ObjectSampler::instance(); |
| 262 | + ObjectSamplerTestAccessor::setActive(s, true); |
| 263 | + ObjectSamplerTestAccessor::callRecordAllocation( |
| 264 | + s, &mock_env, nullptr, nullptr, BCI_ALLOC, |
| 265 | + nullptr, nullptr, 1024); |
| 266 | + EXPECT_EQ(deallocate_calls, 0); |
| 267 | +} |
| 268 | + |
| 269 | +// --------------------------------------------------------------------------- |
| 270 | +// T-05: _active is false — recordAllocation returns immediately. |
| 271 | +// Deallocate MUST NOT be called. |
| 272 | +// --------------------------------------------------------------------------- |
| 273 | +TEST_F(ObjectSamplerDeallocateTest, DeallocateNotCalledWhenNotActive) { |
| 274 | + setMockGetClassSignature(mock_GetClassSignature_success); |
| 275 | + ObjectSampler *s = ObjectSampler::instance(); |
| 276 | + ObjectSamplerTestAccessor::setActive(s, false); |
| 277 | + ObjectSamplerTestAccessor::callRecordAllocation( |
| 278 | + s, &mock_env, nullptr, nullptr, BCI_ALLOC, |
| 279 | + nullptr, nullptr, 1024); |
| 280 | + EXPECT_EQ(deallocate_calls, 0); |
| 281 | +} |
0 commit comments