diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5311f4d50..ff6ae65d3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ android { androidResources.generateLocaleConfig = true buildFeatures { + aidl = true compose = true prefab = true buildConfig = true diff --git a/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java b/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java new file mode 100644 index 000000000..984093e01 --- /dev/null +++ b/app/src/androidTest/java/ru/playsoftware/j2meloader/memory/MemoryTargetProbeTest.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +import android.os.Process; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MemoryTargetProbeTest { + private static final int MANAGED_PROBE_A = 0x5A17C0DE; + private static final int MANAGED_PROBE_B = 0x31C0FFEE; + private static volatile int managedProbe; + + @Test + public void thoroughProbeReturnsAlignedCompleteResidentRuns() { + int pageSize = NativeMemoryTarget.pageSize(); + assertTrue(pageSize > 0); + + long[] runs = NativeMemoryTarget.collectResidentRuns( + MemoryEngineContract.SCOPE_JAVA_THOROUGH, 4096); + assertNotNull(runs); + assertTrue(MemoryEngineContract.isCompleteRunList(runs)); + for (int index = 2; index < runs.length; index += 2) { + assertTrue(runs[index] > 0L); + assertTrue(runs[index + 1] > runs[index]); + assertEquals(0L, runs[index] % pageSize); + assertEquals(0L, runs[index + 1] % pageSize); + if (index > 2) { + assertTrue(runs[index] >= runs[index - 1]); + } + } + } + + @Test + public void readCapabilityProbeVerifiesExpectedBits() { + long[] probe = NativeMemoryTarget.readProbe(); + assertNotNull(probe); + assertEquals(2, probe.length); + assertTrue(NativeMemoryEngine.canReadTarget(Process.myPid(), probe[0], probe[1])); + assertTrue(!NativeMemoryEngine.canReadTarget(Process.myPid(), probe[0], probe[1] ^ 1L)); + } + + @Test + public void nativeEngineFindsAndRefinesManagedArtValue() { + int pageSize = NativeMemoryTarget.pageSize(); + long[] runs = NativeMemoryTarget.collectResidentRuns( + MemoryEngineContract.SCOPE_JAVA_FAST, 4096); + assertNotNull(runs); + assertTrue(MemoryEngineContract.isCompleteRunList(runs)); + + long token = 0x4A4C4D454D544553L; + managedProbe = MANAGED_PROBE_A; + try { + assertEquals(MemoryEngineContract.RESULT_OK, + NativeMemoryEngine.configureTarget( + Process.myPid(), pageSize, token, runs)); + assertEquals(MemoryEngineContract.RESULT_OK, + NativeMemoryEngine.startKnown( + MemoryEngineContract.TYPE_INT, + MemoryEngineContract.PREDICATE_EQUAL, + Integer.toString(MANAGED_PROBE_A), "")); + assertTrue("managed ART probe was not inside the selected Java ranges", + NativeMemoryEngine.resultCount() > 0L); + + managedProbe = MANAGED_PROBE_B; + assertEquals(MemoryEngineContract.RESULT_OK, + NativeMemoryEngine.refineKnown( + MemoryEngineContract.PREDICATE_EQUAL, + Integer.toString(MANAGED_PROBE_B), "")); + assertTrue("managed ART probe did not survive direct refine", + NativeMemoryEngine.resultCount() > 0L); + } finally { + managedProbe = 0; + NativeMemoryEngine.clear(); + } + } + + @Test + public void runtimeTokenCannotBeClosedByAnOlderOwner() { + long[] endedToken = {0L}; + MemoryRuntimeSession.Listener listener = token -> endedToken[0] = token; + MemoryRuntimeSession.addListener(listener); + try { + long token = MemoryRuntimeSession.start(); + assertTrue(token != 0L); + MemoryRuntimeSession.close(token ^ 1L); + assertTrue(MemoryRuntimeSession.isActive(token)); + assertEquals(0L, endedToken[0]); + MemoryRuntimeSession.close(token); + assertEquals(0L, MemoryRuntimeSession.currentToken()); + assertEquals(token, endedToken[0]); + } finally { + MemoryRuntimeSession.close(MemoryRuntimeSession.currentToken()); + MemoryRuntimeSession.removeListener(listener); + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 777246476..333414e72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -220,6 +220,14 @@ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="j2me_midlet_runtime" /> + + diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl new file mode 100644 index 000000000..fc9d2b3e5 --- /dev/null +++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineCallback.aidl @@ -0,0 +1,14 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. + */ + +package ru.playsoftware.j2meloader.memory; + +/** Publishes completion of an asynchronous Memory Editor engine operation. */ +oneway interface IMemoryEngineCallback { + void onOperationFinished(long operationId, int resultCode, long resultCount, String message); +} diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl new file mode 100644 index 000000000..58ac931e3 --- /dev/null +++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryEngineService.aidl @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. + */ + +package ru.playsoftware.j2meloader.memory; + +import android.os.Bundle; +import ru.playsoftware.j2meloader.memory.IMemoryEngineCallback; + +/** Logical API; result addresses are informational and no operation accepts a raw destination. */ +interface IMemoryEngineService { + Bundle getCapabilities(); + void registerCallback(IMemoryEngineCallback callback); + void unregisterCallback(IMemoryEngineCallback callback); + + long startKnownSearch(long runtimeToken, int scope, int valueType, int predicate, + String firstValue, String secondValue); + long startUnknownSearch(long runtimeToken, int scope, int valueType); + long refineKnown(long runtimeToken, int predicate, String firstValue, String secondValue); + long refineRelative(long runtimeToken, int predicate, int compareTarget, + String firstValue, String secondValue); + long undoSearch(long runtimeToken); + + long getResultCount(long runtimeToken); + long[] getResultPage(long runtimeToken, int offset, int limit); + void clearSearch(long runtimeToken); + void cancelOperation(long runtimeToken); +} diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl new file mode 100644 index 000000000..9a3956636 --- /dev/null +++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetBridge.aidl @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. + */ + +package ru.playsoftware.j2meloader.memory; + +import ru.playsoftware.j2meloader.memory.IMemoryTargetCallback; + +/** Thin target-process bridge. Scanning and candidate ownership remain in :memory_engine. */ +interface IMemoryTargetBridge { + void registerTargetCallback(IMemoryTargetCallback callback); + void unregisterTargetCallback(IMemoryTargetCallback callback); + long getRuntimeToken(); + int getTargetPid(); + int getPageSize(); + /** Returns [address, expectedBits] for a target-owned read-only capability probe. */ + long[] getReadProbe(long runtimeToken); + + /** + * Returns [runCount, truncated, start0, end0, ...]. Only resident readable/writable runs + * selected by the requested scope are returned. + */ + long[] getResidentRuns(long runtimeToken, int scope, int maxRuns); +} diff --git a/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl new file mode 100644 index 000000000..ba77d346c --- /dev/null +++ b/app/src/main/aidl/ru/playsoftware/j2meloader/memory/IMemoryTargetCallback.aidl @@ -0,0 +1,14 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. + */ + +package ru.playsoftware.j2meloader.memory; + +/** Notifies the engine that an address namespace is no longer valid. */ +oneway interface IMemoryTargetCallback { + void onRuntimeEnded(long runtimeToken); +} diff --git a/app/src/main/cpp/Android.mk b/app/src/main/cpp/Android.mk index 1d4ec7f86..5053e7d64 100644 --- a/app/src/main/cpp/Android.mk +++ b/app/src/main/cpp/Android.mk @@ -1 +1 @@ -include $(call all-subdir-makefiles) \ No newline at end of file +include $(call all-subdir-makefiles) diff --git a/app/src/main/cpp/memory/Android.mk b/app/src/main/cpp/memory/Android.mk new file mode 100644 index 000000000..ec856d064 --- /dev/null +++ b/app/src/main/cpp/memory/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := jlmem_target +LOCAL_SRC_FILES := target_probe.cpp +LOCAL_CPPFLAGS := -std=c++17 -Wall -Wextra -Werror +LOCAL_CPP_FEATURES := exceptions +LOCAL_LDLIBS := -llog +include $(BUILD_SHARED_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := jlmem +LOCAL_SRC_FILES := memory_engine.cpp +LOCAL_CPPFLAGS := -std=c++17 -Wall -Wextra -Werror +LOCAL_CPP_FEATURES := exceptions +LOCAL_LDLIBS := -llog +include $(BUILD_SHARED_LIBRARY) diff --git a/app/src/main/cpp/memory/memory_engine.cpp b/app/src/main/cpp/memory/memory_engine.cpp new file mode 100644 index 000000000..605c0b2a8 --- /dev/null +++ b/app/src/main/cpp/memory/memory_engine.cpp @@ -0,0 +1,1204 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr jint kOk = 0; +constexpr jint kCancelled = 1; +constexpr jint kInvalidRequest = 2; +constexpr jint kResourceLimit = 3; +constexpr jint kTargetLost = 5; +constexpr jint kNoSession = 6; + +constexpr jint kTypeAuto = 0; +constexpr jint kTypeByte = 1; +constexpr jint kTypeShort = 2; +constexpr jint kTypeChar = 3; +constexpr jint kTypeInt = 4; +constexpr jint kTypeLong = 5; +constexpr jint kTypeFloat = 6; +constexpr jint kTypeDouble = 7; + +constexpr jint kEqual = 0; +constexpr jint kNotEqual = 1; +constexpr jint kGreater = 2; +constexpr jint kLess = 3; +constexpr jint kGreaterOrEqual = 4; +constexpr jint kLessOrEqual = 5; +constexpr jint kBetween = 6; +constexpr jint kChanged = 7; +constexpr jint kUnchanged = 8; +constexpr jint kIncreased = 9; +constexpr jint kDecreased = 10; +constexpr jint kIncreasedBy = 11; +constexpr jint kDecreasedBy = 12; +constexpr jint kChangedBy = 13; +constexpr jint kIncreasedByRange = 14; +constexpr jint kDecreasedByRange = 15; + +constexpr jint kComparePrevious = 0; +constexpr jint kCompareInitial = 1; +constexpr jint kStable = 0; +// Candidate records are compact native data and never cross Binder in bulk. Two million typed +// aliases covers the dense searches seen in the prototype while retaining a deterministic bound +// on old API 23 devices. An incomplete set is never committed. +constexpr size_t kCandidateLimit = 2'000'000; +constexpr size_t kSnapshotByteLimit = 96U * 1024U * 1024U; +constexpr size_t kReadChunkSize = 256U * 1024U; +constexpr size_t kHistoryLimit = 8; +constexpr size_t kHistoryByteLimit = 192U * 1024U * 1024U; +constexpr size_t kResultStride = 7; + +struct Range { + uintptr_t start; + uintptr_t end; +}; + +struct Target { + pid_t pid = 0; + size_t pageSize = 0; + jlong token = 0; + uint64_t generation = 0; + std::vector ranges; +}; + +struct Candidate { + uint64_t id; + uintptr_t address; + jint type; + jint state; + uint64_t initialBits; + uint64_t previousBits; + uint64_t currentBits; +}; + +struct SnapshotRun { + uintptr_t start; + std::vector bytes; +}; + +enum class StateMode { + Empty, + Unknown, + Candidates, +}; + +struct SearchState { + StateMode mode = StateMode::Empty; + jint requestedType = kTypeAuto; + uint64_t logicalCount = 0; + std::vector snapshots; + std::vector candidates; + + size_t retainedBytes() const { + size_t result = + sizeof(SearchState) + candidates.size() * sizeof(Candidate); + for (const SnapshotRun &snapshot : snapshots) { + if (result > + std::numeric_limits::max() - snapshot.bytes.size()) { + return std::numeric_limits::max(); + } + result += snapshot.bytes.size(); + } + return result; + } +}; + +struct Query { + jint type = 0; + bool floating = false; + int64_t integerFirst = 0; + int64_t integerSecond = 0; + uint64_t deltaFirst = 0; + uint64_t deltaSecond = 0; + double floatingFirst = 0; + double floatingSecond = 0; +}; + +std::mutex gMutex; +Target gTarget; +const std::shared_ptr gEmptyState = + std::make_shared(); +std::shared_ptr gState = gEmptyState; +std::deque> gHistory; +uint64_t gNextCandidateId = 1; +std::atomic gCancelled{false}; +std::string gLastMessage; + +void setMessage(const char *message) { + std::lock_guard lock(gMutex); + gLastMessage = message; +} + +size_t widthOf(jint type) { + switch (type) { + case kTypeByte: + return 1; + case kTypeShort: + case kTypeChar: + return 2; + case kTypeInt: + case kTypeFloat: + return 4; + case kTypeLong: + case kTypeDouble: + return 8; + default: + return 0; + } +} + +std::vector expandedTypes(jint requestedType) { + if (requestedType == kTypeAuto) { + return {kTypeByte, kTypeShort, kTypeChar, kTypeInt, + kTypeLong, kTypeFloat, kTypeDouble}; + } + if (widthOf(requestedType) == 0) { + return {}; + } + return {requestedType}; +} + +bool parseInteger(const std::string &text, jint type, int64_t &value) { + if (text.empty()) { + return false; + } + errno = 0; + char *end = nullptr; + const char *number = text.c_str(); + while (*number == ' ' || *number == '\t' || *number == '\r' || + *number == '\n') { + ++number; + } + const char *prefix = *number == '+' || *number == '-' ? number + 1 : number; + const int base = prefix[0] == '0' && (prefix[1] == 'x' || prefix[1] == 'X') + ? 16 + : 10; + const long long parsed = std::strtoll(text.c_str(), &end, base); + while (end != nullptr && + (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { + ++end; + } + if (errno == ERANGE || end == text.c_str() || end == nullptr || + *end != '\0') { + return false; + } + int64_t minimum = std::numeric_limits::min(); + int64_t maximum = std::numeric_limits::max(); + switch (type) { + case kTypeByte: + minimum = std::numeric_limits::min(); + maximum = std::numeric_limits::max(); + break; + case kTypeShort: + minimum = std::numeric_limits::min(); + maximum = std::numeric_limits::max(); + break; + case kTypeChar: + minimum = 0; + maximum = std::numeric_limits::max(); + break; + case kTypeInt: + minimum = std::numeric_limits::min(); + maximum = std::numeric_limits::max(); + break; + case kTypeLong: + break; + default: + return false; + } + value = static_cast(parsed); + return value >= minimum && value <= maximum; +} + +bool parseFloating(const std::string &text, double &value) { + if (text.empty()) { + return false; + } + errno = 0; + char *end = nullptr; + value = std::strtod(text.c_str(), &end); + while (end != nullptr && + (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { + ++end; + } + return errno != ERANGE && end != text.c_str() && end != nullptr && + *end == '\0' && std::isfinite(value); +} + +bool parseDelta(const std::string &text, jint type, uint64_t &value) { + if (text.empty()) { + return false; + } + const char *number = text.c_str(); + while (*number == ' ' || *number == '\t' || *number == '\r' || + *number == '\n') { + ++number; + } + if (*number == '-') { + return false; + } + if (*number == '+') { + ++number; + } + const int base = number[0] == '0' && (number[1] == 'x' || number[1] == 'X') + ? 16 + : 10; + errno = 0; + char *end = nullptr; + const unsigned long long parsed = std::strtoull(text.c_str(), &end, base); + while (end != nullptr && + (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { + ++end; + } + if (errno == ERANGE || end == text.c_str() || end == nullptr || + *end != '\0') { + return false; + } + uint64_t maximum = 0; + switch (type) { + case kTypeByte: + maximum = std::numeric_limits::max(); + break; + case kTypeShort: + case kTypeChar: + maximum = std::numeric_limits::max(); + break; + case kTypeInt: + maximum = std::numeric_limits::max(); + break; + case kTypeLong: + maximum = std::numeric_limits::max(); + break; + default: + return false; + } + value = static_cast(parsed); + return value <= maximum; +} + +bool predicateNeedsFirst(jint predicate) { + return predicate <= kBetween || predicate >= kIncreasedBy; +} + +bool predicateNeedsSecond(jint predicate) { + return predicate == kBetween || predicate == kIncreasedByRange || + predicate == kDecreasedByRange; +} + +bool parseQuery(jint type, jint predicate, const std::string &first, + const std::string &second, Query &query) { + query.type = type; + query.floating = type == kTypeFloat || type == kTypeDouble; + const bool deltaPredicate = predicate >= kIncreasedBy; + if (predicateNeedsFirst(predicate)) { + if (query.floating) { + if (!parseFloating(first, query.floatingFirst)) { + return false; + } + if (deltaPredicate && query.floatingFirst < 0) { + return false; + } + if (type == kTypeFloat) { + const float rounded = static_cast(query.floatingFirst); + if (!std::isfinite(rounded)) { + return false; + } + query.floatingFirst = rounded; + } + } else if (deltaPredicate) { + if (!parseDelta(first, type, query.deltaFirst)) { + return false; + } + } else if (!parseInteger(first, type, query.integerFirst)) { + return false; + } + } + if (predicateNeedsSecond(predicate)) { + if (query.floating) { + if (!parseFloating(second, query.floatingSecond) || + query.floatingFirst > query.floatingSecond) { + return false; + } + if (deltaPredicate && query.floatingSecond < 0) { + return false; + } + if (type == kTypeFloat) { + const float rounded = static_cast(query.floatingSecond); + if (!std::isfinite(rounded)) { + return false; + } + query.floatingSecond = rounded; + } + } else if (deltaPredicate) { + if (!parseDelta(second, type, query.deltaSecond) || + query.deltaFirst > query.deltaSecond) { + return false; + } + } else if (!parseInteger(second, type, query.integerSecond) || + query.integerFirst > query.integerSecond) { + return false; + } + } + return true; +} + +uint64_t loadBits(const uint8_t *data, size_t width) { + uint64_t result = 0; + std::memcpy(&result, data, width); + return result; +} + +int64_t integerValue(jint type, uint64_t bits) { + switch (type) { + case kTypeByte: + return static_cast(bits); + case kTypeShort: + return static_cast(bits); + case kTypeChar: + return static_cast(bits); + case kTypeInt: + return static_cast(bits); + case kTypeLong: + return static_cast(bits); + default: + return 0; + } +} + +double floatingValue(jint type, uint64_t bits) { + if (type == kTypeFloat) { + uint32_t raw = static_cast(bits); + float value = 0; + std::memcpy(&value, &raw, sizeof(value)); + return value; + } + double value = 0; + std::memcpy(&value, &bits, sizeof(value)); + return value; +} + +template +bool matchesOrdered(T value, jint predicate, T first, T second) { + switch (predicate) { + case kEqual: + return value == first; + case kNotEqual: + return value != first; + case kGreater: + return value > first; + case kLess: + return value < first; + case kGreaterOrEqual: + return value >= first; + case kLessOrEqual: + return value <= first; + case kBetween: + return value >= first && value <= second; + default: + return false; + } +} + +bool matchesKnown(uint64_t bits, const Query &query, jint predicate) { + if (query.floating) { + const double value = floatingValue(query.type, bits); + return !std::isnan(value) && + matchesOrdered(value, predicate, query.floatingFirst, + query.floatingSecond); + } + return matchesOrdered(integerValue(query.type, bits), predicate, + query.integerFirst, query.integerSecond); +} + +bool matchesRelative(uint64_t currentBits, uint64_t referenceBits, + const Query &query, jint predicate) { + if (query.floating) { + const double current = floatingValue(query.type, currentBits); + const double reference = floatingValue(query.type, referenceBits); + if (std::isnan(current) || std::isnan(reference)) { + return false; + } + const double delta = current - reference; + switch (predicate) { + case kChanged: + return current != reference; + case kUnchanged: + return current == reference; + case kIncreased: + return current > reference; + case kDecreased: + return current < reference; + case kIncreasedBy: + return delta == query.floatingFirst; + case kDecreasedBy: + return -delta == query.floatingFirst; + case kChangedBy: + return std::fabs(delta) == std::fabs(query.floatingFirst); + case kIncreasedByRange: + return delta >= query.floatingFirst && + delta <= query.floatingSecond; + case kDecreasedByRange: + return -delta >= query.floatingFirst && + -delta <= query.floatingSecond; + default: + return false; + } + } + + const int64_t current = integerValue(query.type, currentBits); + const int64_t reference = integerValue(query.type, referenceBits); + const bool increased = current >= reference; + const uint64_t magnitude = + increased ? static_cast(current) - + static_cast(reference) + : static_cast(reference) - + static_cast(current); + switch (predicate) { + case kChanged: + return current != reference; + case kUnchanged: + return current == reference; + case kIncreased: + return current > reference; + case kDecreased: + return current < reference; + case kIncreasedBy: + return increased && magnitude == query.deltaFirst; + case kDecreasedBy: + return !increased && magnitude == query.deltaFirst; + case kChangedBy: + return magnitude == query.deltaFirst; + case kIncreasedByRange: + return increased && magnitude >= query.deltaFirst && + magnitude <= query.deltaSecond; + case kDecreasedByRange: + return !increased && magnitude >= query.deltaFirst && + magnitude <= query.deltaSecond; + default: + return false; + } +} + +bool readExact(pid_t pid, uintptr_t address, void *destination, size_t size) { + auto *output = static_cast(destination); + size_t completed = 0; + while (completed < size) { + iovec local{output + completed, size - completed}; + iovec remote{reinterpret_cast(address + completed), + size - completed}; + const ssize_t read = process_vm_readv(pid, &local, 1, &remote, 1, 0); + if (read < 0 && errno == EINTR) { + continue; + } + if (read <= 0) { + return false; + } + completed += static_cast(read); + } + return true; +} + +bool safeAdd(uint64_t &value, uint64_t addition) { + if (value > std::numeric_limits::max() - addition) { + value = std::numeric_limits::max(); + return false; + } + value += addition; + return true; +} + +struct OperationContext { + Target target; + std::shared_ptr state; + uint64_t nextId; +}; + +bool beginOperation(OperationContext &context) { + gCancelled.store(false, std::memory_order_release); + std::lock_guard lock(gMutex); + if (gTarget.pid <= 0 || gTarget.token == 0 || gTarget.ranges.empty()) { + gLastMessage = "No configured MIDlet runtime"; + return false; + } + context.target = gTarget; + context.state = gState; + context.nextId = gNextCandidateId; + return true; +} + +void trimHistoryLocked() { + size_t retained = 0; + for (const auto &state : gHistory) { + const size_t bytes = state->retainedBytes(); + retained = retained > std::numeric_limits::max() - bytes + ? std::numeric_limits::max() + : retained + bytes; + } + while (!gHistory.empty() && + (gHistory.size() > kHistoryLimit || retained > kHistoryByteLimit)) { + const size_t removed = gHistory.front()->retainedBytes(); + gHistory.pop_front(); + retained = removed > retained ? 0 : retained - removed; + } +} + +jint commitOperation(const OperationContext &context, + std::shared_ptr next, bool preserveHistory) { + if (gCancelled.load(std::memory_order_acquire)) { + setMessage("Operation cancelled; previous results were preserved"); + return kCancelled; + } + std::lock_guard lock(gMutex); + if (gTarget.generation != context.target.generation || + gTarget.token != context.target.token) { + gLastMessage = "MIDlet runtime changed during the operation"; + return kTargetLost; + } + if (preserveHistory && gState->mode != StateMode::Empty) { + gHistory.push_back(gState); + trimHistoryLocked(); + } else if (!preserveHistory) { + gHistory.clear(); + } + gState = std::move(next); + gNextCandidateId = context.nextId; + gLastMessage = ""; + return kOk; +} + +bool buildQueries(jint requestedType, jint predicate, const std::string &first, + const std::string &second, bool relative, + std::vector &queries) { + if ((!relative && (predicate < kEqual || predicate > kBetween)) || + (relative && (predicate < kChanged || predicate > kDecreasedByRange))) { + return false; + } + for (jint type : expandedTypes(requestedType)) { + Query query; + if (parseQuery(type, predicate, first, second, query)) { + queries.push_back(query); + } else if (requestedType != kTypeAuto) { + return false; + } + } + return !queries.empty(); +} + +jint scanKnown(const OperationContext &context, jint requestedType, + jint predicate, const std::string &first, + const std::string &second) { + if (context.nextId > + std::numeric_limits::max() - kCandidateLimit) { + setMessage("Candidate identifier space is exhausted for this runtime"); + return kResourceLimit; + } + std::vector queries; + if (!buildQueries(requestedType, predicate, first, second, false, + queries)) { + setMessage("Invalid value, type, or predicate"); + return kInvalidRequest; + } + auto next = std::make_shared(); + next->mode = StateMode::Candidates; + next->requestedType = requestedType; + std::vector buffer; + + for (const Range &range : context.target.ranges) { + for (uintptr_t chunkStart = range.start; chunkStart < range.end;) { + if (gCancelled.load(std::memory_order_acquire)) { + setMessage( + "Operation cancelled; previous results were preserved"); + return kCancelled; + } + const size_t remaining = + static_cast(range.end - chunkStart); + const size_t chunkSize = std::min(remaining, kReadChunkSize); + buffer.resize(chunkSize); + if (!readExact(context.target.pid, chunkStart, buffer.data(), + chunkSize)) { + setMessage("A target range changed while it was being scanned"); + return kTargetLost; + } + for (const Query &query : queries) { + const size_t width = widthOf(query.type); + uintptr_t address = chunkStart; + const size_t misalignment = + static_cast(address % width); + if (misalignment != 0) { + address += width - misalignment; + } + while (address >= chunkStart && + address <= chunkStart + chunkSize - + std::min(width, chunkSize)) { + const size_t offset = + static_cast(address - chunkStart); + if (offset + width > chunkSize) { + break; + } + const uint64_t bits = + loadBits(buffer.data() + offset, width); + if (matchesKnown(bits, query, predicate)) { + if (next->candidates.size() >= kCandidateLimit) { + setMessage("Candidate limit reached; previous " + "results were preserved"); + return kResourceLimit; + } + next->candidates.push_back( + {context.nextId + next->candidates.size(), + address, query.type, kStable, bits, bits, + bits}); + } + if (address > + std::numeric_limits::max() - width) { + break; + } + address += width; + } + } + chunkStart += chunkSize; + } + } + next->logicalCount = next->candidates.size(); + OperationContext committed = context; + committed.nextId += next->candidates.size(); + return commitOperation(committed, std::move(next), false); +} + +jint snapshotUnknown(const OperationContext &context, jint requestedType) { + const std::vector types = expandedTypes(requestedType); + if (types.empty()) { + setMessage("Invalid value type"); + return kInvalidRequest; + } + auto next = std::make_shared(); + next->mode = StateMode::Unknown; + next->requestedType = requestedType; + size_t retained = 0; + for (const Range &range : context.target.ranges) { + if (gCancelled.load(std::memory_order_acquire)) { + setMessage("Operation cancelled; previous results were preserved"); + return kCancelled; + } + const size_t size = static_cast(range.end - range.start); + if (size > + kSnapshotByteLimit - std::min(retained, kSnapshotByteLimit)) { + setMessage("Unknown-value snapshot exceeds the memory budget"); + return kResourceLimit; + } + SnapshotRun snapshot; + snapshot.start = range.start; + snapshot.bytes.resize(size); + if (!readExact(context.target.pid, range.start, snapshot.bytes.data(), + size)) { + setMessage("A target range changed while it was being captured"); + return kTargetLost; + } + retained += size; + for (jint type : types) { + const size_t width = widthOf(type); + const size_t adjustment = + range.start % width == 0 ? 0 : width - range.start % width; + if (range.start > + std::numeric_limits::max() - adjustment) { + continue; + } + const uintptr_t aligned = range.start + adjustment; + if (aligned < range.end && + static_cast(range.end - aligned) >= width) { + safeAdd(next->logicalCount, + 1U + static_cast( + (range.end - aligned - width) / width)); + } + } + next->snapshots.push_back(std::move(snapshot)); + } + return commitOperation(context, std::move(next), false); +} + +jint captureCurrentImage(const Target &target, + std::vector &snapshots) { + size_t retained = 0; + snapshots.clear(); + snapshots.reserve(target.ranges.size()); + for (const Range &range : target.ranges) { + if (gCancelled.load(std::memory_order_acquire)) { + setMessage("Operation cancelled; previous results were preserved"); + return kCancelled; + } + const size_t size = static_cast(range.end - range.start); + if (size > kSnapshotByteLimit - std::min(retained, kSnapshotByteLimit)) { + setMessage("Refine snapshot exceeds the memory budget"); + return kResourceLimit; + } + SnapshotRun snapshot; + snapshot.start = range.start; + snapshot.bytes.resize(size); + if (!readExact(target.pid, range.start, snapshot.bytes.data(), size)) { + setMessage("A target range changed while it was being refined"); + return kTargetLost; + } + retained += size; + snapshots.push_back(std::move(snapshot)); + } + return kOk; +} + +bool readImageBits(const std::vector &snapshots, + const Candidate &candidate, uint64_t &bits) { + const size_t width = widthOf(candidate.type); + if (width == 0) { + return false; + } + const auto run = std::upper_bound( + snapshots.begin(), snapshots.end(), candidate.address, + [](uintptr_t address, const SnapshotRun &item) { + return address < item.start; + }); + if (run == snapshots.begin()) { + return false; + } + const SnapshotRun &snapshot = *std::prev(run); + if (candidate.address < snapshot.start) { + return false; + } + const size_t offset = static_cast(candidate.address - snapshot.start); + if (offset > snapshot.bytes.size() || + width > snapshot.bytes.size() - offset) { + return false; + } + bits = loadBits(snapshot.bytes.data() + offset, width); + return true; +} + +jint refineCandidates(const OperationContext &context, jint predicate, + const std::string &first, const std::string &second, + bool relative, jint compareTarget) { + if (context.state->mode == StateMode::Empty) { + setMessage("No search session to refine"); + return kNoSession; + } + if (context.state->mode == StateMode::Unknown && + context.nextId > + std::numeric_limits::max() - kCandidateLimit) { + setMessage("Candidate identifier space is exhausted for this runtime"); + return kResourceLimit; + } + if (relative && compareTarget != kComparePrevious && + compareTarget != kCompareInitial) { + setMessage("Invalid comparison target"); + return kInvalidRequest; + } + std::vector queries; + if (!buildQueries(context.state->requestedType, predicate, first, second, + relative, queries)) { + setMessage("Invalid value, type, or predicate"); + return kInvalidRequest; + } + auto next = std::make_shared(); + next->mode = StateMode::Candidates; + next->requestedType = context.state->requestedType; + + if (context.state->mode == StateMode::Unknown) { + for (const SnapshotRun &snapshot : context.state->snapshots) { + std::vector current(snapshot.bytes.size()); + if (gCancelled.load(std::memory_order_acquire)) { + setMessage( + "Operation cancelled; previous results were preserved"); + return kCancelled; + } + if (!readExact(context.target.pid, snapshot.start, current.data(), + current.size())) { + setMessage("A target range changed while it was being refined"); + return kTargetLost; + } + for (const Query &query : queries) { + const size_t width = widthOf(query.type); + uintptr_t address = snapshot.start; + const size_t misalignment = + static_cast(address % width); + if (misalignment != 0) { + address += width - misalignment; + } + while (address >= snapshot.start) { + const size_t offset = + static_cast(address - snapshot.start); + if (offset + width > snapshot.bytes.size()) { + break; + } + const uint64_t initial = + loadBits(snapshot.bytes.data() + offset, width); + const uint64_t now = + loadBits(current.data() + offset, width); + const bool match = + relative ? matchesRelative(now, initial, query, + predicate) + : matchesKnown(now, query, predicate); + if (match) { + if (next->candidates.size() >= kCandidateLimit) { + setMessage("Candidate limit reached; previous " + "results were preserved"); + return kResourceLimit; + } + next->candidates.push_back( + {context.nextId + next->candidates.size(), + address, query.type, kStable, initial, initial, + now}); + } + if (address > + std::numeric_limits::max() - width) { + break; + } + address += width; + } + } + } + } else { + // Reading one complete resident image turns a million-candidate refine from a million + // process_vm_readv syscalls into sequential remote reads plus an in-process filter pass. + // It also makes a published zero trustworthy: partial coverage aborts transactionally. + std::vector currentImage; + const jint captureResult = + captureCurrentImage(context.target, currentImage); + if (captureResult != kOk) { + return captureResult; + } + next->candidates.reserve(context.state->candidates.size()); + for (const Candidate &candidate : context.state->candidates) { + if (gCancelled.load(std::memory_order_acquire)) { + setMessage( + "Operation cancelled; previous results were preserved"); + return kCancelled; + } + Candidate updated = candidate; + uint64_t current = 0; + if (!readImageBits(currentImage, candidate, current)) { + // The complete image was captured successfully, so an unresolved address is a + // stale binding. Never keep it as a result that looks editable. + continue; + } + updated.previousBits = candidate.currentBits; + updated.currentBits = current; + updated.state = kStable; + auto query = std::find_if(queries.begin(), queries.end(), + [&](const Query &item) { + return item.type == candidate.type; + }); + if (query == queries.end()) { + continue; + } + const uint64_t reference = compareTarget == kCompareInitial + ? candidate.initialBits + : candidate.currentBits; + const bool match = + relative ? matchesRelative(current, reference, *query, + predicate) + : matchesKnown(current, *query, predicate); + if (match) { + next->candidates.push_back(updated); + } + } + } + next->logicalCount = next->candidates.size(); + OperationContext committed = context; + if (context.state->mode == StateMode::Unknown) { + committed.nextId += next->candidates.size(); + } + return commitOperation(committed, std::move(next), true); +} + +std::string fromJString(JNIEnv *env, jstring value) { + if (value == nullptr) { + return {}; + } + const char *characters = env->GetStringUTFChars(value, nullptr); + if (characters == nullptr) { + return {}; + } + std::string result(characters); + env->ReleaseStringUTFChars(value, characters); + return result; +} + +template jint guardedOperation(Operation operation) { + try { + return operation(); + } catch (const std::bad_alloc &) { + setMessage("Memory budget could not be reserved; previous results were " + "preserved"); + return kResourceLimit; + } catch (...) { + setMessage("The native engine rejected the operation safely"); + return kInvalidRequest; + } +} + +} // namespace + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_configureTarget( + JNIEnv *env, jclass, jint pid, jint pageSize, jlong token, + jlongArray rawRuns) { + try { + if (pid <= 0 || pageSize <= 0 || (pageSize & (pageSize - 1)) != 0 || + token == 0 || rawRuns == nullptr) { + setMessage("Invalid target configuration"); + return kInvalidRequest; + } + const jsize length = env->GetArrayLength(rawRuns); + if (length < 4 || (length - 2) % 2 != 0) { + setMessage("Invalid target range list"); + return kInvalidRequest; + } + std::vector values(static_cast(length)); + env->GetLongArrayRegion(rawRuns, 0, length, values.data()); + if (env->ExceptionCheck() || values[1] != 0 || + values[0] != (length - 2) / 2) { + setMessage("Incomplete target range list"); + return kResourceLimit; + } + + Target target; + target.pid = pid; + target.pageSize = static_cast(pageSize); + target.token = token; + uintptr_t previousEnd = 0; + for (jsize index = 2; index < length; index += 2) { + if (values[index] <= 0 || values[index + 1] <= values[index] || + static_cast(values[index]) > + std::numeric_limits::max() || + static_cast(values[index + 1]) > + std::numeric_limits::max()) { + setMessage("Invalid target range bounds"); + return kInvalidRequest; + } + const uintptr_t start = static_cast(values[index]); + const uintptr_t end = static_cast(values[index + 1]); + if (start % target.pageSize != 0 || end % target.pageSize != 0 || + (!target.ranges.empty() && start < previousEnd)) { + setMessage("Target ranges are unaligned or overlap"); + return kInvalidRequest; + } + target.ranges.push_back({start, end}); + previousEnd = end; + } + + std::lock_guard lock(gMutex); + target.generation = gTarget.generation + 1; + const bool sameRuntime = + gTarget.pid == target.pid && gTarget.token == target.token; + gTarget = std::move(target); + if (!sameRuntime) { + gState = gEmptyState; + gHistory.clear(); + gNextCandidateId = 1; + } + gCancelled.store(false, std::memory_order_release); + gLastMessage = ""; + return kOk; + } catch (const std::bad_alloc &) { + setMessage("Target configuration exceeds the engine memory budget"); + return kResourceLimit; + } catch (...) { + setMessage("Invalid target configuration"); + return kInvalidRequest; + } +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_canReadTarget( + JNIEnv *, jclass, jint pid, jlong address, jlong expectedBits) { + if (pid <= 0 || address <= 0 || + static_cast(address) > + std::numeric_limits::max()) { + return JNI_FALSE; + } + uint64_t actual = 0; + return readExact(pid, static_cast(address), &actual, + sizeof(actual)) && + actual == static_cast(expectedBits) + ? JNI_TRUE + : JNI_FALSE; +} + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_startKnown( + JNIEnv *env, jclass, jint type, jint predicate, jstring first, + jstring second) { + return guardedOperation([&] { + OperationContext context; + if (!beginOperation(context)) { + return kNoSession; + } + return scanKnown(context, type, predicate, fromJString(env, first), + fromJString(env, second)); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_startUnknown( + JNIEnv *, jclass, jint type) { + return guardedOperation([&] { + OperationContext context; + if (!beginOperation(context)) { + return kNoSession; + } + return snapshotUnknown(context, type); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_refineKnown( + JNIEnv *env, jclass, jint predicate, jstring first, jstring second) { + return guardedOperation([&] { + OperationContext context; + if (!beginOperation(context)) { + return kNoSession; + } + return refineCandidates(context, predicate, fromJString(env, first), + fromJString(env, second), false, + kComparePrevious); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_refineRelative( + JNIEnv *env, jclass, jint predicate, jint compareTarget, jstring first, + jstring second) { + return guardedOperation([&] { + OperationContext context; + if (!beginOperation(context)) { + return kNoSession; + } + return refineCandidates(context, predicate, fromJString(env, first), + fromJString(env, second), true, compareTarget); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_undo(JNIEnv *, + jclass) { + std::lock_guard lock(gMutex); + if (gHistory.empty()) { + gLastMessage = "No earlier search state is available"; + return kNoSession; + } + gState = gHistory.back(); + gHistory.pop_back(); + gLastMessage = ""; + return kOk; +} + +extern "C" JNIEXPORT jlong JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_resultCount(JNIEnv *, + jclass) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + state = gState; + } + return state->logicalCount > static_cast( + std::numeric_limits::max()) + ? std::numeric_limits::max() + : static_cast(state->logicalCount); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_resultPage( + JNIEnv *env, jclass, jint offset, jint limit) { + if (offset < 0 || limit <= 0 || limit > 200) { + return nullptr; + } + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + state = gState; + } + const size_t start = + std::min(static_cast(offset), state->candidates.size()); + const size_t count = std::min(static_cast(limit), + state->candidates.size() - start); + const size_t outputSize = 1U + count * kResultStride; + std::vector output(outputSize); + output[0] = static_cast(count); + for (size_t index = 0; index < count; ++index) { + const Candidate &candidate = state->candidates[start + index]; + const size_t base = 1U + index * kResultStride; + output[base] = static_cast(candidate.id); + output[base + 1U] = static_cast(candidate.address); + output[base + 2U] = candidate.type; + output[base + 3U] = candidate.state; + output[base + 4U] = static_cast(candidate.initialBits); + output[base + 5U] = static_cast(candidate.previousBits); + output[base + 6U] = static_cast(candidate.currentBits); + } + jlongArray result = env->NewLongArray(static_cast(outputSize)); + if (result != nullptr) { + env->SetLongArrayRegion(result, 0, static_cast(outputSize), + output.data()); + } + return result; +} + +extern "C" JNIEXPORT void JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_clear(JNIEnv *, + jclass) { + std::lock_guard lock(gMutex); + ++gTarget.generation; + gTarget.pid = 0; + gTarget.pageSize = 0; + gTarget.token = 0; + gTarget.ranges.clear(); + gState = gEmptyState; + gHistory.clear(); + gNextCandidateId = 1; + gLastMessage = ""; +} + +extern "C" JNIEXPORT void JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_cancel(JNIEnv *, + jclass) { + gCancelled.store(true, std::memory_order_release); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryEngine_lastMessage( + JNIEnv *env, jclass) { + std::string message; + { + std::lock_guard lock(gMutex); + message = gLastMessage; + } + return env->NewStringUTF(message.c_str()); +} diff --git a/app/src/main/cpp/memory/target_probe.cpp b/app/src/main/cpp/memory/target_probe.cpp new file mode 100644 index 000000000..d9a5d853d --- /dev/null +++ b/app/src/main/cpp/memory/target_probe.cpp @@ -0,0 +1,223 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr jint kFastScope = 0; +constexpr jint kThoroughScope = 1; +alignas(uint64_t) volatile uint64_t gReadProbe = UINT64_C(0x4a4c4d454d50524f); + +bool isSelectedMap(const char *permissions, const std::string &name, + jint scope) { + if (std::strncmp(permissions, "rw-p", 4) != 0) { + return false; + } + + const bool dalvik = name.find("dalvik-") != std::string::npos; + const bool zygote = name.find("zygote") != std::string::npos; + if (scope == kFastScope) { + return dalvik && !zygote; + } + if (scope != kThoroughScope) { + return false; + } + + // ART names its managed-heap mappings on current releases. Also accept + // unnamed private anonymous mappings so the thorough scope remains useful + // on runtimes that omit those labels. File-backed and explicitly named + // non-ART mappings stay out. + return (dalvik && !zygote) || name.empty(); +} + +bool appendRun(std::vector> &runs, + uintptr_t start, uintptr_t end, size_t maxRuns, + bool &truncated) { + if (start >= end) { + return true; + } + if (!runs.empty() && runs.back().second == start) { + runs.back().second = end; + return true; + } + if (runs.size() >= maxRuns) { + truncated = true; + return false; + } + runs.emplace_back(start, end); + return true; +} + +} // namespace + +extern "C" JNIEXPORT jint JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_pageSize(JNIEnv *, + jclass) { + const long value = sysconf(_SC_PAGESIZE); + if (value <= 0 || value > std::numeric_limits::max()) { + return 0; + } + return static_cast(value); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_readProbe(JNIEnv *env, + jclass) { + const jlong values[] = { + static_cast(reinterpret_cast(&gReadProbe)), + static_cast(gReadProbe), + }; + jlongArray result = env->NewLongArray(2); + if (result != nullptr) { + env->SetLongArrayRegion(result, 0, 2, values); + } + return result; +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_ru_playsoftware_j2meloader_memory_NativeMemoryTarget_collectResidentRuns( + JNIEnv *env, jclass, jint scope, jint maxRuns) { + const long pageValue = sysconf(_SC_PAGESIZE); + if ((scope != kFastScope && scope != kThoroughScope) || maxRuns <= 0 || + pageValue <= 0) { + return nullptr; + } + const size_t pageSize = static_cast(pageValue); + + FILE *maps = nullptr; + char *line = nullptr; + try { + maps = std::fopen("/proc/self/maps", "re"); + if (maps == nullptr) { + return nullptr; + } + + std::vector> runs; + bool truncated = false; + size_t lineCapacity = 0; + while (!truncated && getline(&line, &lineCapacity, maps) >= 0) { + unsigned long long rawStart = 0; + unsigned long long rawEnd = 0; + char permissions[5] = {}; + int nameOffset = 0; + if (std::sscanf(line, "%llx-%llx %4s %*s %*s %*s %n", &rawStart, + &rawEnd, permissions, &nameOffset) < 3 || + rawStart >= rawEnd || + rawEnd > std::numeric_limits::max()) { + continue; + } + std::string name; + if (nameOffset > 0) { + const char *begin = line + nameOffset; + while (*begin == ' ' || *begin == '\t') { + ++begin; + } + name.assign(begin); + while (!name.empty() && + (name.back() == '\n' || name.back() == '\r' || + name.back() == ' ' || name.back() == '\t')) { + name.pop_back(); + } + } + if (!isSelectedMap(permissions, name, scope)) { + continue; + } + + const uintptr_t start = static_cast(rawStart); + const uintptr_t end = static_cast(rawEnd); + if (start % pageSize != 0 || end % pageSize != 0) { + continue; + } + const size_t pageCount = + static_cast((end - start) / pageSize); + if (pageCount == 0) { + continue; + } + std::vector residency; + try { + residency.resize(pageCount); + } catch (...) { + truncated = true; + break; + } + if (mincore(reinterpret_cast(start), end - start, + residency.data()) != 0) { + // A map may disappear between maps parsing and mincore. It is + // safer to report an incomplete snapshot than to publish + // silently partial ranges. + truncated = true; + break; + } + + size_t runStartPage = pageCount; + for (size_t page = 0; page <= pageCount; ++page) { + const bool resident = + page < pageCount && (residency[page] & 1U) != 0; + if (resident && runStartPage == pageCount) { + runStartPage = page; + } else if (!resident && runStartPage != pageCount) { + const uintptr_t runStart = start + runStartPage * pageSize; + const uintptr_t runEnd = start + page * pageSize; + if (!appendRun(runs, runStart, runEnd, + static_cast(maxRuns), truncated)) { + break; + } + runStartPage = pageCount; + } + } + } + std::free(line); + line = nullptr; + std::fclose(maps); + maps = nullptr; + + if (runs.size() > + (static_cast(std::numeric_limits::max()) - 2U) / + 2U) { + return nullptr; + } + const jsize outputSize = static_cast(2U + runs.size() * 2U); + std::vector output(static_cast(outputSize)); + output[0] = static_cast(runs.size()); + output[1] = truncated ? 1 : 0; + for (size_t index = 0; index < runs.size(); ++index) { + output[2U + index * 2U] = static_cast(runs[index].first); + output[3U + index * 2U] = static_cast(runs[index].second); + } + jlongArray result = env->NewLongArray(outputSize); + if (result != nullptr) { + env->SetLongArrayRegion(result, 0, outputSize, output.data()); + } + return result; + } catch (...) { + std::free(line); + if (maps != nullptr) { + std::fclose(maps); + } + return nullptr; + } +} diff --git a/app/src/main/java/javax/microedition/shell/MicroLoader.java b/app/src/main/java/javax/microedition/shell/MicroLoader.java index f4bbc67d7..fcd9ebd3e 100644 --- a/app/src/main/java/javax/microedition/shell/MicroLoader.java +++ b/app/src/main/java/javax/microedition/shell/MicroLoader.java @@ -75,6 +75,7 @@ import ru.playsoftware.j2meloader.crashes.CrashReporter; import ru.playsoftware.j2meloader.crashes.MidletSessionJournal; import ru.playsoftware.j2meloader.crashes.MidletSessionStore; +import ru.playsoftware.j2meloader.memory.MemoryRuntimeSession; import ru.playsoftware.j2meloader.util.AppUtils; import ru.playsoftware.j2meloader.util.FileUtils; import ru.playsoftware.j2meloader.util.IOUtils; @@ -103,6 +104,7 @@ public class MicroLoader { private String jarSha256; private TimingSession timingSession; private AutoSpeedController autoSpeedController; + private long memoryRuntimeToken; private boolean timingTransformCompatible; /** Set only after the MIDlet thread has successfully received the timing session. */ private boolean timingSessionTransferred; @@ -167,6 +169,7 @@ private void startTimingSession() { } timingSession = session; autoSpeedController = speedController; + memoryRuntimeToken = MemoryRuntimeSession.start(); } void closeTimingSession() { @@ -174,6 +177,8 @@ void closeTimingSession() { timingSession = null; autoSpeedController = null; GuestTimingBridge.clear(session); + MemoryRuntimeSession.close(memoryRuntimeToken); + memoryRuntimeToken = 0L; } /** diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java new file mode 100644 index 000000000..216b35378 --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineContract.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +/** Stable primitive constants shared by the UI-independent engine IPC and native core. */ +public final class MemoryEngineContract { + public static final int SCOPE_JAVA_FAST = 0; + public static final int SCOPE_JAVA_THOROUGH = 1; + + public static final int TYPE_AUTO = 0; + public static final int TYPE_BYTE = 1; + public static final int TYPE_SHORT = 2; + public static final int TYPE_CHAR = 3; + public static final int TYPE_INT = 4; + public static final int TYPE_LONG = 5; + public static final int TYPE_FLOAT = 6; + public static final int TYPE_DOUBLE = 7; + + public static final int PREDICATE_EQUAL = 0; + public static final int PREDICATE_NOT_EQUAL = 1; + public static final int PREDICATE_GREATER = 2; + public static final int PREDICATE_LESS = 3; + public static final int PREDICATE_GREATER_OR_EQUAL = 4; + public static final int PREDICATE_LESS_OR_EQUAL = 5; + public static final int PREDICATE_BETWEEN = 6; + public static final int PREDICATE_CHANGED = 7; + public static final int PREDICATE_UNCHANGED = 8; + public static final int PREDICATE_INCREASED = 9; + public static final int PREDICATE_DECREASED = 10; + public static final int PREDICATE_INCREASED_BY = 11; + public static final int PREDICATE_DECREASED_BY = 12; + public static final int PREDICATE_CHANGED_BY = 13; + public static final int PREDICATE_INCREASED_BY_RANGE = 14; + public static final int PREDICATE_DECREASED_BY_RANGE = 15; + + public static final int COMPARE_PREVIOUS = 0; + public static final int COMPARE_INITIAL = 1; + + public static final int RESULT_OK = 0; + public static final int RESULT_CANCELLED = 1; + public static final int RESULT_INVALID_REQUEST = 2; + public static final int RESULT_RESOURCE_LIMIT = 3; + public static final int RESULT_UNSUPPORTED = 4; + public static final int RESULT_TARGET_LOST = 5; + public static final int RESULT_NO_SESSION = 6; + + public static final int CANDIDATE_STABLE = 0; + public static final int CANDIDATE_RELOCATING = 1; + public static final int CANDIDATE_AMBIGUOUS = 2; + public static final int CANDIDATE_LOST = 3; + + /** [count, id, address, type, state, initialBits, previousBits, currentBits, ...]. */ + public static final int RESULT_PAGE_STRIDE = 7; + public static final int MAX_RESULT_PAGE_SIZE = 200; + + public static final String KEY_SUPPORTED = "supported"; + public static final String KEY_RUNTIME_TOKEN = "runtimeToken"; + public static final String KEY_TARGET_PID = "targetPid"; + public static final String KEY_PAGE_SIZE = "pageSize"; + public static final String KEY_MESSAGE = "message"; + + private MemoryEngineContract() { + } + + public static boolean isScope(int scope) { + return scope == SCOPE_JAVA_FAST || scope == SCOPE_JAVA_THOROUGH; + } + + public static boolean isValueType(int type) { + return type >= TYPE_AUTO && type <= TYPE_DOUBLE; + } + + public static boolean isCandidateType(int type) { + return type >= TYPE_BYTE && type <= TYPE_DOUBLE; + } + + static boolean isCompleteRunList(long[] runs) { + if (runs == null || runs.length < 2 || runs[0] <= 0L || runs[1] != 0L || + runs[0] > (runs.length - 2L) / 2L) { + return false; + } + return runs.length == 2 + (int) runs[0] * 2; + } +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java new file mode 100644 index 000000000..a9844faa7 --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryEngineService.java @@ -0,0 +1,363 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; + +import androidx.annotation.Nullable; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicLong; + +/** Owns all scan state in a dedicated app process and exposes only logical candidate IDs. */ +public final class MemoryEngineService extends Service { + private static final int MAX_RUNS = 4096; + + private final AtomicLong nextOperationId = new AtomicLong(1L); + private final AtomicLong cancelEpoch = new AtomicLong(); + private final RemoteCallbackList callbacks = new RemoteCallbackList<>(); + private final ExecutorService worker = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "MemoryEditorEngine"); + thread.setPriority(Thread.NORM_PRIORITY - 1); + return thread; + }); + private volatile IMemoryTargetBridge target; + private volatile boolean targetBound; + private volatile long configuredToken; + private final IMemoryTargetCallback targetCallback = new IMemoryTargetCallback.Stub() { + @Override + public void onRuntimeEnded(long runtimeToken) { + if (runtimeToken == configuredToken) { + invalidateTarget(); + } + } + }; + + private final ServiceConnection targetConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + IMemoryTargetBridge bridge = IMemoryTargetBridge.Stub.asInterface(service); + target = bridge; + try { + bridge.registerTargetCallback(targetCallback); + } catch (RemoteException exception) { + target = null; + invalidateTarget(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + target = null; + invalidateTarget(); + } + + @Override + public void onBindingDied(ComponentName name) { + target = null; + invalidateTarget(); + } + }; + + private final IMemoryEngineService.Stub binder = new IMemoryEngineService.Stub() { + @Override + public Bundle getCapabilities() { + Bundle result = new Bundle(); + IMemoryTargetBridge bridge = target; + if (bridge == null) { + result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, false); + result.putString(MemoryEngineContract.KEY_MESSAGE, "MIDlet runtime is not connected"); + return result; + } + try { + long token = bridge.getRuntimeToken(); + int pid = bridge.getTargetPid(); + int pageSize = bridge.getPageSize(); + long[] probe = token == 0L ? null : bridge.getReadProbe(token); + boolean supported = token != 0L && pid > 0 && pageSize > 0 && + canReadProbe(pid, probe); + result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, supported); + result.putLong(MemoryEngineContract.KEY_RUNTIME_TOKEN, token); + result.putInt(MemoryEngineContract.KEY_TARGET_PID, pid); + result.putInt(MemoryEngineContract.KEY_PAGE_SIZE, pageSize); + if (!supported) { + result.putString(MemoryEngineContract.KEY_MESSAGE, token == 0L + ? "No active MIDlet runtime" + : "Cross-process memory reads are not supported by this device/runtime"); + } + } catch (RemoteException exception) { + result.putBoolean(MemoryEngineContract.KEY_SUPPORTED, false); + result.putString(MemoryEngineContract.KEY_MESSAGE, "MIDlet runtime connection was lost"); + } + return result; + } + + @Override + public void registerCallback(IMemoryEngineCallback callback) { + if (callback != null) { + callbacks.register(callback); + } + } + + @Override + public void unregisterCallback(IMemoryEngineCallback callback) { + if (callback != null) { + callbacks.unregister(callback); + } + } + + @Override + public long startKnownSearch(long token, int scope, int type, int predicate, + String first, String second) { + return enqueue(token, true, scope, + () -> NativeMemoryEngine.startKnown(type, predicate, first, second)); + } + + @Override + public long startUnknownSearch(long token, int scope, int type) { + return enqueue(token, true, scope, () -> NativeMemoryEngine.startUnknown(type)); + } + + @Override + public long refineKnown(long token, int predicate, String first, String second) { + return enqueue(token, false, 0, + () -> NativeMemoryEngine.refineKnown(predicate, first, second)); + } + + @Override + public long refineRelative(long token, int predicate, int compareTarget, + String first, String second) { + return enqueue(token, false, 0, + () -> NativeMemoryEngine.refineRelative(predicate, compareTarget, first, second)); + } + + @Override + public long undoSearch(long token) { + return enqueue(token, false, 0, NativeMemoryEngine::undo); + } + + @Override + public long getResultCount(long token) { + return isCurrentToken(token) ? NativeMemoryEngine.resultCount() : 0L; + } + + @Override + public long[] getResultPage(long token, int offset, int limit) { + if (!isCurrentToken(token) || offset < 0 || limit <= 0 || + limit > MemoryEngineContract.MAX_RESULT_PAGE_SIZE) { + return new long[]{0L}; + } + long[] result = NativeMemoryEngine.resultPage(offset, limit); + return result == null ? new long[]{0L} : result; + } + + @Override + public void clearSearch(long token) { + if (isCurrentToken(token)) { + worker.execute(NativeMemoryEngine::clear); + } + } + + @Override + public void cancelOperation(long token) { + if (token != 0L && (token == configuredToken || isTargetToken(token))) { + cancelEpoch.incrementAndGet(); + NativeMemoryEngine.cancel(); + } + } + }; + + @Override + public void onCreate() { + super.onCreate(); + Intent intent = new Intent(this, MemoryTargetBridgeService.class); + targetBound = bindService(intent, targetConnection, Context.BIND_AUTO_CREATE); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onDestroy() { + IMemoryTargetBridge bridge = target; + if (bridge != null) { + try { + bridge.unregisterTargetCallback(targetCallback); + } catch (RemoteException ignored) { + // The target may already be gone. + } + } + NativeMemoryEngine.cancel(); + worker.shutdownNow(); + callbacks.kill(); + if (targetBound) { + unbindService(targetConnection); + targetBound = false; + } + NativeMemoryEngine.clear(); + super.onDestroy(); + } + + private long enqueue(long token, boolean configure, int scope, NativeOperation operation) { + long operationId = nextOperationId.getAndIncrement(); + long enqueueEpoch = cancelEpoch.get(); + worker.execute(() -> { + int result; + String serviceMessage = null; + if (enqueueEpoch != cancelEpoch.get()) { + result = MemoryEngineContract.RESULT_CANCELLED; + serviceMessage = "Operation cancelled before it started"; + } else if (token == 0L) { + result = MemoryEngineContract.RESULT_NO_SESSION; + serviceMessage = "No active MIDlet runtime"; + } else if (configure) { + result = configureTarget(token, scope); + if (result == MemoryEngineContract.RESULT_OK) { + result = operation.run(); + } else { + serviceMessage = configurationFailureMessage(result); + } + } else if (!isCurrentToken(token)) { + result = MemoryEngineContract.RESULT_TARGET_LOST; + serviceMessage = "MIDlet runtime changed or ended"; + } else { + result = operation.run(); + } + + if (result == MemoryEngineContract.RESULT_OK && !isCurrentToken(token)) { + NativeMemoryEngine.clear(); + configuredToken = 0L; + result = MemoryEngineContract.RESULT_TARGET_LOST; + serviceMessage = "MIDlet runtime changed during the operation"; + } + notifyFinished(operationId, result, serviceMessage); + }); + return operationId; + } + + private int configureTarget(long token, int scope) { + if (!MemoryEngineContract.isScope(scope)) { + return MemoryEngineContract.RESULT_INVALID_REQUEST; + } + IMemoryTargetBridge bridge = target; + if (bridge == null) { + return MemoryEngineContract.RESULT_TARGET_LOST; + } + try { + if (bridge.getRuntimeToken() != token) { + return MemoryEngineContract.RESULT_TARGET_LOST; + } + int pid = bridge.getTargetPid(); + int pageSize = bridge.getPageSize(); + if (!canReadProbe(pid, bridge.getReadProbe(token))) { + return MemoryEngineContract.RESULT_UNSUPPORTED; + } + long[] runs = bridge.getResidentRuns(token, scope, MAX_RUNS); + if (!MemoryEngineContract.isCompleteRunList(runs)) { + if (bridge.getRuntimeToken() != token) { + return MemoryEngineContract.RESULT_TARGET_LOST; + } + return MemoryEngineContract.RESULT_RESOURCE_LIMIT; + } + int result = NativeMemoryEngine.configureTarget(pid, pageSize, token, runs); + if (result == MemoryEngineContract.RESULT_OK) { + configuredToken = token; + } + return result; + } catch (RemoteException exception) { + return MemoryEngineContract.RESULT_TARGET_LOST; + } + } + + private boolean isCurrentToken(long token) { + if (token == 0L || token != configuredToken) { + return false; + } + return isTargetToken(token); + } + + private boolean isTargetToken(long token) { + IMemoryTargetBridge bridge = target; + if (token == 0L || bridge == null) { + return false; + } + try { + return bridge.getRuntimeToken() == token; + } catch (RemoteException exception) { + return false; + } + } + + private static boolean canReadProbe(int pid, long[] probe) { + return pid > 0 && probe != null && probe.length == 2 && probe[0] > 0L && + NativeMemoryEngine.canReadTarget(pid, probe[0], probe[1]); + } + + private void invalidateTarget() { + configuredToken = 0L; + NativeMemoryEngine.cancel(); + try { + worker.execute(NativeMemoryEngine::clear); + } catch (RejectedExecutionException ignored) { + // Service teardown already clears native state directly. + } + } + + private static String configurationFailureMessage(int result) { + return switch (result) { + case MemoryEngineContract.RESULT_UNSUPPORTED -> + "Cross-process memory reads are not supported by this device/runtime"; + case MemoryEngineContract.RESULT_TARGET_LOST -> "MIDlet runtime changed or ended"; + case MemoryEngineContract.RESULT_RESOURCE_LIMIT -> + "The complete resident range set exceeds the engine resource limit"; + default -> "Invalid memory engine target configuration"; + }; + } + + private void notifyFinished(long operationId, int result, @Nullable String serviceMessage) { + long count = result == MemoryEngineContract.RESULT_OK ? NativeMemoryEngine.resultCount() : 0L; + String message = serviceMessage == null ? NativeMemoryEngine.lastMessage() : serviceMessage; + int callbackCount = callbacks.beginBroadcast(); + try { + for (int index = 0; index < callbackCount; index++) { + try { + callbacks.getBroadcastItem(index) + .onOperationFinished(operationId, result, count, message); + } catch (RemoteException ignored) { + // RemoteCallbackList removes dead clients. + } + } + } finally { + callbacks.finishBroadcast(); + } + } + + private interface NativeOperation { + int run(); + } +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java new file mode 100644 index 000000000..2867ed1cc --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryRuntimeSession.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +import android.os.Process; +import android.os.SystemClock; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + +/** Process-local identity for one live MIDlet runtime, independent from Activity visibility. */ +public final class MemoryRuntimeSession { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final List LISTENERS = new ArrayList<>(); + private static long activeToken; + + interface Listener { + void onRuntimeEnded(long token); + } + + private MemoryRuntimeSession() { + } + + public static synchronized long start() { + if (activeToken != 0L) { + return activeToken; + } + long token; + do { + token = RANDOM.nextLong() + ^ SystemClock.elapsedRealtimeNanos() + ^ ((long) Process.myPid() << 32); + } while (token == 0L); + activeToken = token; + return token; + } + + public static synchronized long currentToken() { + return activeToken; + } + + public static synchronized boolean isActive(long token) { + return token != 0L && activeToken == token; + } + + public static void close(long token) { + Listener[] listeners; + synchronized (MemoryRuntimeSession.class) { + if (token == 0L || activeToken != token) { + return; + } + activeToken = 0L; + listeners = LISTENERS.toArray(new Listener[0]); + } + for (Listener listener : listeners) { + try { + listener.onRuntimeEnded(token); + } catch (RuntimeException ignored) { + // Runtime teardown must not be interrupted by an observer. + } + } + } + + static synchronized void addListener(Listener listener) { + if (!LISTENERS.contains(listener)) { + LISTENERS.add(listener); + } + } + + static synchronized void removeListener(Listener listener) { + LISTENERS.remove(listener); + } +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java new file mode 100644 index 000000000..3eb91e449 --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/MemoryTargetBridgeService.java @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; + +import androidx.annotation.Nullable; + +/** Minimal :midlet bridge for runtime identity and target-local mincore collection. */ +public final class MemoryTargetBridgeService extends Service { + private static final int MAX_RUNS = 4096; + private static final long[] EMPTY_RUNS = new long[]{0L, 0L}; + private final Object rangeLock = new Object(); + private final RemoteCallbackList callbacks = new RemoteCallbackList<>(); + private final MemoryRuntimeSession.Listener runtimeListener = this::notifyRuntimeEnded; + + private final IMemoryTargetBridge.Stub binder = new IMemoryTargetBridge.Stub() { + @Override + public void registerTargetCallback(IMemoryTargetCallback callback) { + if (callback != null) { + callbacks.register(callback); + } + } + + @Override + public void unregisterTargetCallback(IMemoryTargetCallback callback) { + if (callback != null) { + callbacks.unregister(callback); + } + } + + @Override + public long getRuntimeToken() { + return MemoryRuntimeSession.currentToken(); + } + + @Override + public int getTargetPid() { + return Process.myPid(); + } + + @Override + public int getPageSize() { + return NativeMemoryTarget.pageSize(); + } + + @Override + public long[] getReadProbe(long runtimeToken) { + if (!MemoryRuntimeSession.isActive(runtimeToken)) { + return new long[0]; + } + long[] probe = NativeMemoryTarget.readProbe(); + return probe == null ? new long[0] : probe; + } + + @Override + public long[] getResidentRuns(long runtimeToken, int scope, int maxRuns) { + if (!MemoryRuntimeSession.isActive(runtimeToken) + || !MemoryEngineContract.isScope(scope) + || maxRuns <= 0 || maxRuns > MAX_RUNS) { + return EMPTY_RUNS; + } + synchronized (rangeLock) { + long[] runs = NativeMemoryTarget.collectResidentRuns(scope, maxRuns); + return runs == null ? EMPTY_RUNS : runs; + } + } + }; + + @Override + public void onCreate() { + super.onCreate(); + MemoryRuntimeSession.addListener(runtimeListener); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onDestroy() { + MemoryRuntimeSession.removeListener(runtimeListener); + callbacks.kill(); + super.onDestroy(); + } + + private void notifyRuntimeEnded(long token) { + int count = callbacks.beginBroadcast(); + try { + for (int index = 0; index < count; index++) { + try { + callbacks.getBroadcastItem(index).onRuntimeEnded(token); + } catch (RemoteException ignored) { + // RemoteCallbackList removes dead clients. + } + } + } finally { + callbacks.finishBroadcast(); + } + } +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java new file mode 100644 index 000000000..727084339 --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryEngine.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +final class NativeMemoryEngine { + static { + System.loadLibrary("jlmem"); + } + + private NativeMemoryEngine() { + } + + static native int configureTarget(int pid, int pageSize, long runtimeToken, long[] runs); + + static native boolean canReadTarget(int pid, long address, long expectedBits); + + static native int startKnown(int valueType, int predicate, String first, String second); + + static native int startUnknown(int valueType); + + static native int refineKnown(int predicate, String first, String second); + + static native int refineRelative(int predicate, int compareTarget, String first, String second); + + static native int undo(); + + static native long resultCount(); + + static native long[] resultPage(int offset, int limit); + + static native void clear(); + + static native void cancel(); + + static native String lastMessage(); +} diff --git a/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java new file mode 100644 index 000000000..b0ab53b6c --- /dev/null +++ b/app/src/main/java/ru/playsoftware/j2meloader/memory/NativeMemoryTarget.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.playsoftware.j2meloader.memory; + +final class NativeMemoryTarget { + static { + System.loadLibrary("jlmem_target"); + } + + private NativeMemoryTarget() { + } + + static native int pageSize(); + + static native long[] readProbe(); + + static native long[] collectResidentRuns(int scope, int maxRuns); +} diff --git a/app/src/midlet/AndroidManifest.xml b/app/src/midlet/AndroidManifest.xml index 4eb64d5f6..c84bce99a 100644 --- a/app/src/midlet/AndroidManifest.xml +++ b/app/src/midlet/AndroidManifest.xml @@ -25,6 +25,11 @@ + +