diff --git a/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/interop/GR46212.java b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/interop/GR46212.java new file mode 100644 index 00000000000..75cf643a327 --- /dev/null +++ b/graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/interop/GR46212.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023, 2023, 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.truffle.js.test.interop; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import com.oracle.truffle.js.runtime.JSContextOptions; +import com.oracle.truffle.js.test.JSTest; + +/** + * Tests of the modification of length property of foreign arrays. + */ +@RunWith(Parameterized.class) +public class GR46212 { + private final String arrayLengthExpr; + + @Parameterized.Parameters + public static List data() { + return List.of("array.length", "array['length']"); + } + + public GR46212(String arrayLengthExpr) { + this.arrayLengthExpr = arrayLengthExpr; + } + + @Test + public void testDecrease() { + try (Context context = JSTest.newContextBuilder().build()) { + ProxyArray array = new MapBasedProxyArray(); + array.set(0, Value.asValue(42)); + array.set(1, Value.asValue(211)); + array.set(2, Value.asValue(3.14)); + context.getBindings(ID).putMember("array", array); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 3); + context.eval(ID, arrayLengthExpr + " = 1"); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 1); + assertEquals(context.eval(ID, "array[0]").asLong(), 42); + } + } + + @Test + public void testIncrease() { + try (Context context = JSTest.newContextBuilder().build()) { + ProxyArray array = new MapBasedProxyArray(); + array.set(0, Value.asValue(42)); + context.getBindings(ID).putMember("array", array); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 1); + context.eval(ID, arrayLengthExpr + " = 3"); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 3); + assertEquals(context.eval(ID, "array[0]").asLong(), 42); + } + } + + @Test + public void testNotInteger() { + try (Context context = JSTest.newContextBuilder().build()) { + ProxyArray array = new MapBasedProxyArray(); + context.getBindings(ID).putMember("array", array); + assertTrue(context.eval(ID, "try { " + arrayLengthExpr + " = 3.14; false; } catch (e) { e instanceof RangeError }").asBoolean()); + } + } + + @Test + public void testNegative() { + try (Context context = JSTest.newContextBuilder().build()) { + ProxyArray array = new MapBasedProxyArray(); + context.getBindings(ID).putMember("array", array); + assertTrue(context.eval(ID, "try { " + arrayLengthExpr + " = -1; false; } catch (e) { e instanceof RangeError }").asBoolean()); + } + } + + @Test + public void testUnsupportedSloppy() { + try (Context context = JSTest.newContextBuilder().build()) { + ProxyArray array = ProxyArray.fromArray(42, 211); + context.getBindings(ID).putMember("array", array); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + context.eval(ID, arrayLengthExpr + " = 5"); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + context.eval(ID, arrayLengthExpr + " = 1"); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + } + } + + @Test + public void testUnsupportedStrict() { + try (Context context = JSTest.newContextBuilder().option(JSContextOptions.STRICT_NAME, "true").build()) { + ProxyArray array = ProxyArray.fromArray(42, 211); + context.getBindings(ID).putMember("array", array); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + assertTrue(context.eval(ID, "try { " + arrayLengthExpr + " = 5; false; } catch (e) { e instanceof TypeError }").asBoolean()); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + assertTrue(context.eval(ID, "try { " + arrayLengthExpr + " = 1; false; } catch (e) { e instanceof TypeError }").asBoolean()); + assertEquals(context.eval(ID, arrayLengthExpr).asLong(), 2); + } + } + + private class MapBasedProxyArray implements ProxyArray { + private Map map = new HashMap<>(); + + @Override + public Object get(long index) { + return map.get(index); + } + + @Override + public void set(long index, Value value) { + map.put(index, value); + } + + @Override + public boolean remove(long index) { + return (map.remove(index) != null); + } + + @Override + public long getSize() { + long maxIndex = -1; + for (long index : map.keySet()) { + maxIndex = Math.max(maxIndex, index); + } + return maxIndex + 1; + } + + } + +} diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/PropertySetNode.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/PropertySetNode.java index 0366d6a9d0d..62ed8c938c0 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/PropertySetNode.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/PropertySetNode.java @@ -75,11 +75,13 @@ import com.oracle.truffle.js.runtime.JSRuntime; import com.oracle.truffle.js.runtime.Strings; import com.oracle.truffle.js.runtime.Symbol; +import com.oracle.truffle.js.runtime.builtins.JSAbstractArray; import com.oracle.truffle.js.runtime.builtins.JSAdapter; import com.oracle.truffle.js.runtime.builtins.JSArray; import com.oracle.truffle.js.runtime.builtins.JSArrayBufferView; import com.oracle.truffle.js.runtime.builtins.JSGlobal; import com.oracle.truffle.js.runtime.builtins.JSProxy; +import com.oracle.truffle.js.runtime.interop.JSInteropUtil; import com.oracle.truffle.js.runtime.objects.Accessor; import com.oracle.truffle.js.runtime.objects.Dead; import com.oracle.truffle.js.runtime.objects.JSAttributes; @@ -805,7 +807,7 @@ protected boolean setValue(Object thisObj, Object value, Object receiver, Proper } else if (isForeignObject.profile(JSGuards.isForeignObject(thisObj))) { if (foreignSetNode == null) { CompilerDirectives.transferToInterpreterAndInvalidate(); - foreignSetNode = insert(new ForeignPropertySetNode(root.getContext())); + foreignSetNode = insert(new ForeignPropertySetNode(root.getContext(), root.getKey())); } foreignSetNode.setValue(thisObj, value, receiver, root, guard); } else { @@ -858,12 +860,14 @@ public static final class ForeignPropertySetNode extends LinkedPropertySetNode { @Child private InteropLibrary interop; @Child private InteropLibrary setterInterop; private final BranchProfile errorBranch = BranchProfile.create(); + private final boolean isLength; - public ForeignPropertySetNode(JSContext context) { + public ForeignPropertySetNode(JSContext context, Object key) { super(new ForeignLanguageCheckNode()); this.context = context; this.export = ExportValueNode.create(); this.interop = InteropLibrary.getFactory().createDispatched(JSConfig.InteropLibraryLimit); + this.isLength = key.equals(JSAbstractArray.LENGTH); } private Object nullCheck(Object truffleObject, Object key) { @@ -873,36 +877,32 @@ private Object nullCheck(Object truffleObject, Object key) { return truffleObject; } - @Override - protected boolean setValueInt(Object thisObj, int value, Object receiver, PropertySetNode root, boolean guard) { + private boolean setValueImpl(Object thisObj, Object value, PropertySetNode root) { Object key = root.getKey(); Object truffleObject = nullCheck(thisObj, key); if (!Strings.isTString(key)) { return false; } + if (isLength && interop.hasArrayElements(thisObj)) { + return JSInteropUtil.setArraySize(thisObj, value, root.isStrict, interop, this, errorBranch); + } return performWriteMember(truffleObject, value, root); } + @Override + protected boolean setValueInt(Object thisObj, int value, Object receiver, PropertySetNode root, boolean guard) { + return setValueImpl(thisObj, value, root); + } + @Override protected boolean setValueDouble(Object thisObj, double value, Object receiver, PropertySetNode root, boolean guard) { - Object key = root.getKey(); - Object truffleObject = nullCheck(thisObj, key); - if (!Strings.isTString(key)) { - return false; - } - return performWriteMember(truffleObject, value, root); + return setValueImpl(thisObj, value, root); } @InliningCutoff @Override protected boolean setValue(Object thisObj, Object value, Object receiver, PropertySetNode root, boolean guard) { - Object key = root.getKey(); - Object truffleObject = nullCheck(thisObj, key); - if (!Strings.isTString(key)) { - return false; - } - Object exportedValue = export.execute(value); - return performWriteMember(truffleObject, exportedValue, root); + return setValueImpl(thisObj, export.execute(value), root); } private boolean performWriteMember(Object truffleObject, Object value, PropertySetNode root) { @@ -1193,7 +1193,7 @@ protected final boolean isDeclaration() { @Override protected SetCacheNode createTruffleObjectPropertyNode() { - return new ForeignPropertySetNode(context); + return new ForeignPropertySetNode(context, getKey()); } @Override diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/WriteElementNode.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/WriteElementNode.java index cbaca085f48..754a0e8ba79 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/WriteElementNode.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/access/WriteElementNode.java @@ -154,6 +154,7 @@ import com.oracle.truffle.js.runtime.builtins.JSSlowArray; import com.oracle.truffle.js.runtime.builtins.JSString; import com.oracle.truffle.js.runtime.builtins.JSSymbol; +import com.oracle.truffle.js.runtime.interop.JSInteropUtil; import com.oracle.truffle.js.runtime.objects.JSDynamicObject; import com.oracle.truffle.js.runtime.objects.JSObject; import com.oracle.truffle.js.runtime.objects.JSObjectUtil; @@ -1760,7 +1761,8 @@ protected void doForeignObject(Object target, Object index, Object value, @Suppr } Object propertyKey; Object exportedValue = exportValue.execute(value); - if (interop.hasArrayElements(truffleObject)) { + boolean hasArrayElements = interop.hasArrayElements(truffleObject); + if (hasArrayElements) { Object indexOrPropertyKey = toArrayIndex(index); if (indexOrPropertyKey instanceof Long) { try { @@ -1798,6 +1800,9 @@ protected void doForeignObject(Object target, Object index, Object value, @Suppr return; } TruffleString stringKey = (TruffleString) propertyKey; + if (hasArrayElements && Strings.equals(JSAbstractArray.LENGTH, stringKey)) { + JSInteropUtil.setArraySize(truffleObject, value, root.isStrict, interop, this, null); + } if (root.context.isOptionNashornCompatibilityMode()) { if (tryInvokeSetter(truffleObject, stringKey, exportedValue, root.context)) { return; diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSAbstractArray.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSAbstractArray.java index e659b850fa8..b1cbef3380a 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSAbstractArray.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/JSAbstractArray.java @@ -508,7 +508,7 @@ protected static List ownPropertyKeysSlowArray(JSDynamicObject thisObj, return list; } - protected static long toArrayLengthOrRangeError(Object obj, Node originatingNode) { + public static long toArrayLengthOrRangeError(Object obj, Node originatingNode) { Number len = JSRuntime.toNumber(obj); Number len32 = JSRuntime.toUInt32(len); /* diff --git a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/interop/JSInteropUtil.java b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/interop/JSInteropUtil.java index cdf2da280f3..bf2daa7b5af 100644 --- a/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/interop/JSInteropUtil.java +++ b/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/interop/JSInteropUtil.java @@ -54,6 +54,7 @@ import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.interop.UnsupportedTypeException; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.profiles.BranchProfile; import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.strings.TruffleString; import com.oracle.truffle.js.nodes.interop.ExportValueNode; @@ -62,9 +63,11 @@ import com.oracle.truffle.js.runtime.Errors; import com.oracle.truffle.js.runtime.JSRuntime; import com.oracle.truffle.js.runtime.Strings; +import com.oracle.truffle.js.runtime.builtins.JSAbstractArray; import com.oracle.truffle.js.runtime.builtins.JSError; import com.oracle.truffle.js.runtime.objects.Null; import com.oracle.truffle.js.runtime.objects.PropertyDescriptor; +import com.oracle.truffle.js.runtime.objects.Undefined; /** * Utility class for interop operations. Provides methods that can be used in Cached annotations of @@ -84,6 +87,43 @@ public static long getArraySize(Object foreignObj, InteropLibrary interop, Node } } + public static boolean setArraySize(Object obj, Object value, boolean isStrict, InteropLibrary interop, Node originatingNode, BranchProfile errorBranch) { + long newLen = JSAbstractArray.toArrayLengthOrRangeError(value, originatingNode); + long oldLen; + try { + oldLen = interop.getArraySize(obj); + } catch (UnsupportedMessageException e) { + if (errorBranch != null) { + errorBranch.enter(); + } + throw Errors.createTypeErrorInteropException(obj, e, "getArraySize", originatingNode); + } + String message = null; + try { + if (newLen < oldLen) { + message = "removeArrayElement"; + for (long idx = oldLen - 1; idx >= newLen; idx--) { + interop.removeArrayElement(obj, idx); + } + } else { + message = "writeArrayElement"; + for (long idx = oldLen; idx < newLen; idx++) { + interop.writeArrayElement(obj, idx, Undefined.instance); + } + } + } catch (InteropException e) { + if (isStrict) { + if (errorBranch != null) { + errorBranch.enter(); + } + throw Errors.createTypeErrorInteropException(obj, e, message, originatingNode); + } else { + return false; + } + } + return true; + } + public static Object readMemberOrDefault(Object obj, Object member, Object defaultValue) { return readMemberOrDefault(obj, member, defaultValue, InteropLibrary.getUncached(), ImportValueNode.getUncached(), null); }