diff --git a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/ResourcesTest.java b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/ResourcesTest.java index c2ba25acac..241f674cc5 100644 --- a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/ResourcesTest.java +++ b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/ResourcesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -43,6 +43,9 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Engine; @@ -79,4 +82,33 @@ public void testResourcesAlwaysAllowReading() { assertTrue(foundHome, foundHome.contains("python" + File.separator + "python-home")); } } + + @Test + public void testTracebackSourceLineRespectsDeniedIO() throws IOException { + Path sourceFile = Files.createTempFile("graalpy-traceback-source", ".py"); + String leakedLine = "this line must not appear in the traceback"; + Files.writeString(sourceFile, leakedLine + "\n"); + try (Context context = Context.newBuilder("python").allowIO(IOAccess.NONE).build()) { + String traceback = context.eval("python", """ + import io + import traceback + + code = compile("1/0\\n", '%s', "exec") + out = io.StringIO() + try: + exec(code) + except ZeroDivisionError: + traceback.print_exc(file=out) + out.getvalue() + """.formatted(escapePythonString(sourceFile.toString()))).asString(); + assertTrue(traceback, traceback.contains(sourceFile.toString())); + assertTrue(traceback, !traceback.contains(leakedLine)); + } finally { + Files.deleteIfExists(sourceFile); + } + } + + private static String escapePythonString(String value) { + return value.replace("\\", "\\\\").replace("'", "\\'"); + } } diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/runtime/PythonContextPathTests.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/runtime/PythonContextPathTests.java new file mode 100644 index 0000000000..39ac84456f --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/runtime/PythonContextPathTests.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.test.runtime; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import org.junit.After; +import org.junit.Test; + +import com.oracle.graal.python.runtime.PythonContext; +import com.oracle.graal.python.test.PythonTests; +import com.oracle.truffle.api.TruffleFile; + +public class PythonContextPathTests { + + @After + public void tearDown() { + PythonTests.closeContext(); + } + + @Test + public void isPyFileInLanguageHomeNormalizesPathBeforeContainmentCheck() { + PythonTests.enterContext(); + PythonContext context = PythonContext.get(null); + String languageHomePath = context.getLanguageHome().toJavaStringUncached(); + TruffleFile languageHome = context.getEnv().getInternalTruffleFile(languageHomePath).getAbsoluteFile().normalize(); + assumeFalse("resource-backed language home is rooted at /", "/".equals(languageHome.getPath())); + TruffleFile insideLanguageHome = context.getEnv().getInternalTruffleFile(languageHomePath + "/lib-python"); + TruffleFile escapedLanguageHome = context.getEnv().getInternalTruffleFile(languageHomePath + "/../outside.py"); + + assertTrue(context.isPyFileInLanguageHome(insideLanguageHome)); + assertFalse("escaped path " + escapedLanguageHome.getAbsoluteFile().normalize() + + " should not be contained in language home " + languageHome, + context.isPyFileInLanguageHome(escapedLanguageHome)); + } +} diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java index aeb8c1d999..6088c97134 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java @@ -56,6 +56,7 @@ import java.nio.charset.StandardCharsets; import com.oracle.graal.python.PythonFileDetector; +import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.code.PCode; @@ -255,8 +256,8 @@ protected static CharSequence getSourceLine(TruffleString fileName, int lineNo) final PythonContext context = PythonContext.get(null); TruffleFile file = null; try { - file = context.getEnv().getInternalTruffleFile(fileName.toJavaStringUncached()); - } catch (Exception e) { + file = context.getPublicTruffleFileRelaxed(fileName, PythonLanguage.T_DEFAULT_PYTHON_EXTENSIONS); + } catch (IllegalArgumentException | SecurityException | UnsupportedOperationException e) { return null; } String line = null; @@ -278,7 +279,7 @@ protected static CharSequence getSourceLine(TruffleString fileName, int lineNo) i++; } } - } catch (IOException ioe) { + } catch (IllegalArgumentException | IOException | SecurityException | UnsupportedOperationException e) { line = null; } return line; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java index 5ac015a9a9..f285b99d13 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java @@ -2641,8 +2641,8 @@ public boolean isPyFileInLanguageHome(TruffleFile path) { // This deliberately uses 'getAbsoluteFile' and not 'getCanonicalFile' because if, e.g., // 'path' is a symlink outside of the language home, the user should not be able to read // the symlink if 'allowIO' is false. - TruffleFile coreHomePath = getEnv().getInternalTruffleFile(langHome.toJavaStringUncached()).getAbsoluteFile(); - TruffleFile absolutePath = path.getAbsoluteFile(); + TruffleFile coreHomePath = getEnv().getInternalTruffleFile(langHome.toJavaStringUncached()).getAbsoluteFile().normalize(); + TruffleFile absolutePath = path.getAbsoluteFile().normalize(); return absolutePath.startsWith(coreHomePath); } LOGGER.log(Level.FINE, () -> "Cannot access file " + path + " because there is no language home.");