diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 28310b992d..0aa1e1b670 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -752,6 +752,7 @@ struct TryBlock { }; #define CN1_MAX_STACK_CALL_DEPTH 1024 +#define CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT CN1_MAX_STACK_CALL_DEPTH #define CN1_MAX_OBJECT_STACK_DEPTH 16536 #define PER_THREAD_ALLOCATION_COUNT 4096 diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index 9c2f91c62c..342b921949 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -230,6 +230,10 @@ public static void markDependencies(List lst, String[] nativeSour bc.markDependent(lst); continue; } + if(bc.clsName.equals("java_lang_StackOverflowError")) { + bc.markDependent(lst); + continue; + } if(bc.clsName.equals("java_text_DateFormat")) { bc.markDependent(lst); continue; diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 5a25dc122b..5f39d3ee5a 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -27,6 +27,7 @@ #include "java_lang_NullPointerException.h" #include "java_lang_Class.h" #include "java_lang_System.h" +#include "java_lang_StackOverflowError.h" #if defined(__APPLE__) && defined(__OBJC__) #import @@ -1550,9 +1551,12 @@ void initMethodStack(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, int THROW_NULL_POINTER_EXCEPTION(); } #endif + if (threadStateData->callStackOffset >= CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT - 1) { + throwException(threadStateData, __NEW_INSTANCE_java_lang_StackOverflowError(threadStateData)); + return; + } memset(&threadStateData->threadObjectStack[threadStateData->threadObjectStackOffset], 0, sizeof(struct elementStruct) * (localsStackSize + stackSize)); threadStateData->threadObjectStackOffset += localsStackSize + stackSize; - CODENAME_ONE_ASSERT(threadStateData->callStackOffset < CN1_MAX_STACK_CALL_DEPTH - 1); threadStateData->callStackClass[threadStateData->callStackOffset] = classNameId; threadStateData->callStackMethod[threadStateData->callStackOffset] = methodNameId; threadStateData->callStackOffset++; diff --git a/vm/JavaAPI/src/java/lang/StackOverflowError.java b/vm/JavaAPI/src/java/lang/StackOverflowError.java new file mode 100644 index 0000000000..afa53a0372 --- /dev/null +++ b/vm/JavaAPI/src/java/lang/StackOverflowError.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.lang; +/** + * Thrown when a stack overflow occurs because an application recurses too deeply. + * Since: JDK1.0, CLDC 1.0 + */ +public class StackOverflowError extends java.lang.VirtualMachineError{ + /** + * Constructs a StackOverflowError with no detail message. + */ + public StackOverflowError(){ + } + + /** + * Constructs a StackOverflowError with the specified detail message. + * s - the detail message. + */ + public StackOverflowError(java.lang.String s){ + super(s); + } + +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowIntegrationTest.java new file mode 100644 index 0000000000..5ea1ad5295 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowIntegrationTest.java @@ -0,0 +1,306 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.params.ParameterizedTest; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StackOverflowIntegrationTest { + + @ParameterizedTest + @org.junit.jupiter.params.provider.MethodSource("com.codename1.tools.translator.BytecodeInstructionIntegrationTest#provideCompilerConfigs") + void throwsAndRecoversFromStackOverflow(CompilerHelper.CompilerConfig config) throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("stack-overflow-source"); + Path classesDir = Files.createTempDirectory("stack-overflow-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-classes"); + + Files.write(sourceDir.resolve("StackOverflowApp.java"), appSource().getBytes(StandardCharsets.UTF_8)); + Files.write(sourceDir.resolve("native_report.c"), nativeReportSource().getBytes(StandardCharsets.UTF_8)); + + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + CompilerHelper.compileJavaAPI(javaApiDir, config); + + List compileArgs = new ArrayList<>(); + + int jdkMajor = CompilerHelper.getJdkMajor(config); + if (jdkMajor >= 9) { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.add(sourceDir.resolve("StackOverflowApp.java").toString()); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "StackOverflowApp should compile with " + config); + + CompilerHelper.copyDirectory(javaApiDir, classesDir); + Files.copy(sourceDir.resolve("native_report.c"), classesDir.resolve("native_report.c")); + + Path outputDir = Files.createTempDirectory("stack-overflow-output"); + CleanTargetIntegrationTest.runTranslator(classesDir, outputDir, "StackOverflowApp"); + + Path distDir = outputDir.resolve("dist"); + Path cmakeLists = distDir.resolve("CMakeLists.txt"); + assertTrue(Files.exists(cmakeLists), "Translator should emit a CMake project for StackOverflowApp"); + + Path srcRoot = distDir.resolve("StackOverflowApp-src"); + CleanTargetIntegrationTest.patchCn1Globals(srcRoot); + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(cmakeLists, srcRoot.getFileName().toString()); + assertGeneratedSources(srcRoot); + + Path buildDir = distDir.resolve("build"); + Files.createDirectories(buildDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList( + "cmake", + "-S", distDir.toString(), + "-B", buildDir.toString(), + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang" + ), distDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + + Path executable = buildDir.resolve("StackOverflowApp"); + ProcessResult probeResult = runProcess(Arrays.asList(executable.toString()), buildDir); + String probeDiagnostics = buildDiagnostics(srcRoot, executable, probeResult); + assertEquals(0, probeResult.exitCode, + "StackOverflowApp probe run exited with code " + probeResult.exitCode + + ". Output:\n" + probeResult.output + + probeDiagnostics); + assertTrue(probeResult.output.contains("PROBE_CONSTANT"), + "StackOverflowApp probe run should succeed. Output was:\n" + probeResult.output + probeDiagnostics); + ProcessResult smokeResult = runProcess(Arrays.asList(executable.toString(), "smoke"), buildDir); + String smokeDiagnostics = buildDiagnostics(srcRoot, executable, smokeResult); + assertEquals(0, smokeResult.exitCode, + "StackOverflowApp smoke run exited with code " + smokeResult.exitCode + + ". Output:\n" + smokeResult.output + + smokeDiagnostics); + assertTrue(smokeResult.output.contains("PROBE_CONSTANT"), + "StackOverflowApp smoke run should emit probe marker before recursion. Output was:\n" + + smokeResult.output + smokeDiagnostics); + assertTrue(smokeResult.output.contains("SMOKE_OK"), + "StackOverflowApp smoke run should succeed. Output was:\n" + + smokeResult.output + + "\nMissing SMOKE_OK suggests a crash during boundedRecursion or report(String)." + + smokeDiagnostics); + + ProcessResult result = runProcess(Arrays.asList(executable.toString(), "overflow", "run"), buildDir); + String diagnostics = buildDiagnostics(srcRoot, executable, result); + assertEquals(0, result.exitCode, + "StackOverflowApp exited with code " + result.exitCode + + ". Output:\n" + result.output + + diagnostics); + assertTrue(result.output.contains("STACK_OVERFLOW_OK"), + "StackOverflowError should be thrown and caught. Output was:\n" + result.output + diagnostics); + } + + private String appSource() { + return "public class StackOverflowApp {\n" + + " private static native void report(String msg);\n" + + " private static native void reportConstant();\n" + + " private static void triggerOverflow() {\n" + + " triggerOverflow();\n" + + " }\n" + + " private static int boundedRecursion(int depth) {\n" + + " if (depth <= 0) {\n" + + " return 1;\n" + + " }\n" + + " return depth + boundedRecursion(depth - 1);\n" + + " }\n" + + " public static void main(String[] args) {\n" + + " if (args == null || args.length == 0) {\n" + + " reportConstant();\n" + + " return;\n" + + " }\n" + + " if (args.length == 1) {\n" + + " reportConstant();\n" + + " int value = boundedRecursion(5);\n" + + " StringBuilder sb = new StringBuilder();\n" + + " sb.append(\"SMOKE_OK:\");\n" + + " sb.append(value);\n" + + " report(sb.toString());\n" + + " reportConstant();\n" + + " return;\n" + + " }\n" + + " try {\n" + + " triggerOverflow();\n" + + " } catch (StackOverflowError err) {\n" + + " report(\"STACK_OVERFLOW_OK\");\n" + + " }\n" + + " }\n" + + "}\n"; + } + + private String nativeReportSource() { + return "#include \"cn1_globals.h\"\n" + + "#include \n" + + "void StackOverflowApp_reportConstant__(CODENAME_ONE_THREAD_STATE) {\n" + + " printf(\"PROBE_CONSTANT\\n\");\n" + + " fflush(stdout);\n" + + "}\n" + + "void StackOverflowApp_report___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT msg) {\n" + + " struct String_Struct {\n" + + " JAVA_OBJECT header;\n" + + " JAVA_OBJECT value;\n" + + " JAVA_INT offset;\n" + + " JAVA_INT count;\n" + + " };\n" + + " struct String_Struct* str = (struct String_Struct*)msg;\n" + + " struct JavaArrayPrototype* arr = (struct JavaArrayPrototype*)str->value;\n" + + " if (arr) {\n" + + " JAVA_CHAR* chars = (JAVA_CHAR*)arr->data;\n" + + " int len = str->count;\n" + + " int off = str->offset;\n" + + " for (int i = 0; i < len; i++) {\n" + + " printf(\"%c\", (char)chars[off + i]);\n" + + " }\n" + + " printf(\"\\n\");\n" + + " fflush(stdout);\n" + + " }\n" + + "}\n"; + } + + private void assertGeneratedSources(Path srcRoot) throws Exception { + Path nativeMethods = srcRoot.resolve("nativeMethods.c"); + assertTrue(Files.exists(nativeMethods), + "Expected nativeMethods.c at " + nativeMethods); + String nativeMethodsSource = new String(Files.readAllBytes(nativeMethods), StandardCharsets.UTF_8); + assertTrue(nativeMethodsSource.contains("java_lang_StackOverflowError.h"), + "nativeMethods.c should include java_lang_StackOverflowError.h to support stack overflow handling."); + assertTrue(nativeMethodsSource.contains("CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT"), + "nativeMethods.c should reference CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT for stack overflow detection."); + + Path cn1Globals = srcRoot.resolve("cn1_globals.h"); + assertTrue(Files.exists(cn1Globals), + "Expected cn1_globals.h at " + cn1Globals); + String globalsSource = new String(Files.readAllBytes(cn1Globals), StandardCharsets.UTF_8); + assertTrue(globalsSource.contains("CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT"), + "cn1_globals.h should define CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT."); + + Path appSource = srcRoot.resolve("StackOverflowApp.c"); + assertTrue(Files.exists(appSource), + "Expected StackOverflowApp.c at " + appSource); + String appSourceText = new String(Files.readAllBytes(appSource), StandardCharsets.UTF_8); + assertTrue(appSourceText.contains("StackOverflowApp_triggerOverflow__"), + "StackOverflowApp.c should include triggerOverflow method for recursion."); + } + + private String buildDiagnostics(Path srcRoot, Path executable, ProcessResult result) throws Exception { + StringBuilder diagnostics = new StringBuilder(); + diagnostics.append("\nExecutable: ").append(executable); + if (Files.exists(executable)) { + diagnostics.append("\nExecutable size: ").append(Files.size(executable)).append(" bytes"); + } + + Path cn1Globals = srcRoot.resolve("cn1_globals.h"); + if (Files.exists(cn1Globals)) { + String globalsSource = new String(Files.readAllBytes(cn1Globals), StandardCharsets.UTF_8); + diagnostics.append("\n").append(extractLine(globalsSource, "CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT")); + } + + Path nativeMethods = srcRoot.resolve("nativeMethods.c"); + if (Files.exists(nativeMethods)) { + String nativeMethodsSource = new String(Files.readAllBytes(nativeMethods), StandardCharsets.UTF_8); + diagnostics.append("\nContains java_lang_StackOverflowError.h: ") + .append(nativeMethodsSource.contains("java_lang_StackOverflowError.h")); + diagnostics.append("\nContains CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT: ") + .append(nativeMethodsSource.contains("CN1_STACK_OVERFLOW_CALL_DEPTH_LIMIT")); + diagnostics.append("\ninitMethodStack snippet: ") + .append(extractSnippet(nativeMethodsSource, "initMethodStack", 120)); + } + + Path appSource = srcRoot.resolve("StackOverflowApp.c"); + if (Files.exists(appSource)) { + String appSourceText = new String(Files.readAllBytes(appSource), StandardCharsets.UTF_8); + diagnostics.append("\ntriggerOverflow snippet: ") + .append(extractSnippet(appSourceText, "StackOverflowApp_triggerOverflow__", 120)); + diagnostics.append("\nboundedRecursion snippet: ") + .append(extractSnippet(appSourceText, "StackOverflowApp_boundedRecursion___int_R_int", 120)); + diagnostics.append("\nmain snippet: ") + .append(extractSnippet(appSourceText, "StackOverflowApp_main___java_lang_String_1ARRAY", 160)); + diagnostics.append("\nreport snippet: ") + .append(extractSnippet(appSourceText, "StackOverflowApp_report___java_lang_String", 120)); + } + if (!result.output.isEmpty()) { + diagnostics.append("\nOutput length: ").append(result.output.length()); + } + return diagnostics.toString(); + } + + private String extractLine(String source, String token) { + int idx = source.indexOf(token); + if (idx < 0) { + return "Missing " + token; + } + int lineStart = source.lastIndexOf('\n', idx); + int lineEnd = source.indexOf('\n', idx); + if (lineStart < 0) { + lineStart = 0; + } else { + lineStart += 1; + } + if (lineEnd < 0) { + lineEnd = source.length(); + } + return source.substring(lineStart, lineEnd).trim(); + } + + private String extractSnippet(String source, String token, int radius) { + int idx = source.indexOf(token); + if (idx < 0) { + return "Missing " + token; + } + int start = Math.max(0, idx - radius); + int end = Math.min(source.length(), idx + radius); + return source.substring(start, end).replace("\n", "\\n"); + } + + private ProcessResult runProcess(List command, Path workingDir) throws Exception { + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(workingDir.toFile()); + builder.redirectErrorStream(true); + Process process = builder.start(); + String output; + try (java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) { + output = reader.lines().collect(java.util.stream.Collectors.joining("\n")); + } + int exit = process.waitFor(); + return new ProcessResult(exit, output); + } + + private static final class ProcessResult { + private final int exitCode; + private final String output; + + private ProcessResult(int exitCode, String output) { + this.exitCode = exitCode; + this.output = output; + } + } +}