Closed Bug 1609432 Opened 4 years ago Closed 2 years ago

Reconsider unsoundness when eagerly evaluating higher order native functions

Categories

(DevTools :: Console, task, P3)

task

Tracking

(firefox105 fixed)

RESOLVED FIXED
105 Branch
Tracking Status
firefox105 --- fixed

People

(Reporter: bhackett1024, Assigned: arai)

References

(Blocks 1 open bug)

Details

Attachments

(6 files)

If a higher order self hosted native function is passed a native which has side effects, that native can be called without invoking the debugger's onNativeCall hook. This can allow side effects to occur when eagerly evaluating something like "[1,2,3].map(Array.prototype.push)".

This unsoundness could be fixed by keeping track of which natives are higher order and vulnerable to this problem, and force-terminating the eager evaluation if any of them are called with an effectful native. This is pretty obscure though and might not be worth worrying about or incurring the complexity of a proper fix.

Priority: -- → P3

[1,2,3].map(Array.prototype.push) seems to just throw TypeError: "can't convert undefined to object".

I think you meant something like [1,2,3].map(Array.prototype.push, Array.prototype), then Array.prototype.length becomes 9.

I think this is what causes a bunch of prompt boxes to be opened when I type [1, 2, 3].map(prompt) (where prompt comes from autocompletion), which is very annoying.

arai, since we're looking at this area today, would you know how we could implement what is described in Comment 0?
I don't know how we can know if a native is higher order in the first place, and I'm not sure if the plan laid out is feasible only in JS.

Note that Bug 1625069 describe what looks like a more robust way of doing eager evaluation, but this is definitely not something that we (DevTools team) could do as we don't have much C++ knowledge at the moment.

Flags: needinfo?(arai.unmht)
See Also: → 1732543, 1754141, 1777241

We could expose the call's arguments to onNativeCall here.

https://searchfox.org/mozilla-central/rev/fa71140041c5401b80a11f099cc0cd0653295e2c/js/src/debugger/Debugger.cpp#2285-2287,2315

bool Debugger::fireNativeCall(JSContext* cx, const CallArgs& args,
                              CallReason reason, ResumeMode& resumeMode,
                              MutableHandleValue vp) {
...
  bool ok = js::Call(cx, fval, object, calleeval, reasonval, &rv);

So, as long as we could maintain the list of either "safe" or "danger" functions, it could work, but I'm not sure if that's sustainable.

Also, there's other possibility.
onNativeCall inside self-hosted is skipped here.

https://searchfox.org/mozilla-central/rev/fa71140041c5401b80a11f099cc0cd0653295e2c/js/src/debugger/Debugger.cpp#965-976

// The onNativeCall hook is fired when self hosted functions are called,
// and any other self hosted function or C++ native that is directly called
// by the self hosted function is considered to be part of the same
// native call.
//
// We check this only after checking that debuggerList has items in order
// to avoid unnecessary calls to cx->currentScript(), which can be expensive
// when the top frame is in jitcode.
JSScript* script = cx->currentScript();
if (script && script->selfHosted()) {
  return NativeResumeMode::Continue;
}

if it's really necessary, I think we could either:

  • (a) add a flag to debugger or something not to skip call inside self-hosted functions
  • (b) call some different hook (e.g. onNativeCallInSelfHosted) for such case
  • (c) (a) + (b)
Flags: needinfo?(arai.unmht)

(In reply to Tooru Fujisawa [:arai] from comment #5)

We could expose the call's arguments to onNativeCall here.

something like the following:

diff --git a/js/src/debugger/Debugger.cpp b/js/src/debugger/Debugger.cpp
index c5de834a41977..92e9e90efb95d 100644
--- a/js/src/debugger/Debugger.cpp
+++ b/js/src/debugger/Debugger.cpp
@@ -2311,8 +2311,28 @@ bool Debugger::fireNativeCall(JSContext* cx, const CallArgs& args,
 
   RootedValue reasonval(cx, StringValue(reasonAtom));
 
+  Rooted<ArrayObject*> argArray(cx, NewDenseEmptyArray(cx));
+  if (!argArray) {
+    return false;
+  }
+  for (unsigned i = 0; i < args.length(); i++) {
+    RootedValue arg(cx, args[i]);
+    if (!wrapDebuggeeValue(cx, &arg)) {
+      return false;
+    }
+    if (!NewbornArrayPush(cx, argArray, arg)) {
+      return false;
+    }
+  }
+
+  FixedInvokeArgs<3> invokeArgs(cx);
+  invokeArgs[0].set(calleeval);
+  invokeArgs[1].set(reasonval);
+  invokeArgs[2].set(ObjectValue(*argArray));
+
+  RootedValue objval(cx, ObjectValue(*object));
   RootedValue rv(cx);
-  bool ok = js::Call(cx, fval, object, calleeval, reasonval, &rv);
+  bool ok = js::Call(cx, fval, objval, invokeArgs, &rv);
 
   return processHandlerResult(cx, ok, rv, NullFramePtr(), nullptr, resumeMode,
                               vp);

I'll look into onNativeCall inside self-hosted case.

The issue with calling onNativeCall or variant inside self-hosted code is that,
the callee can be self-hosted JS's internal function, that's not exposed elsewhere.

https://searchfox.org/mozilla-central/rev/fa71140041c5401b80a11f099cc0cd0653295e2c/js/src/builtin/Array.js#156,158,161,166,173

function ArrayMap(callbackfn/*, thisArg*/) {
...
    var O = ToObject(this);
...
    var len = ToLength(O.length);
...
    if (!IsCallable(callbackfn))
...
    var A = ArraySpeciesCreate(O, len);

So, the nativeHasNoSideEffects check might not work, or needs special handling.

Possible workaround for the onNativeCall inside self-hosted JS is to hook into callContentFunction syntax, and call onNativeCall or variant only for the function call with it.
This needs slight change on the bytecode and how the call there works, but at least can avoid exposing self-hosted JS's internal functions.

Assignee: nobody → arai.unmht
Status: NEW → ASSIGNED

Calling onNativeCall for callContentFunction fixes the comment #2 and bug 1754141,
but not bug 1732543 and bug 1777241.

Bug 1777241 testcase uses std_Function_apply, that needs yet another special handling.

https://searchfox.org/mozilla-central/rev/c11f54459452dd2f9ab2f9bec4ae03127897d256/js/src/builtin/Reflect.js#31,43

function Reflect_apply(target, thisArgument, argumentsList) {
...
    return callFunction(std_Function_apply, target, thisArgument, argumentsList);

Bug 1732543 testcase uses Function.prototype.call that also needs yet another special handling.

RegExp.prototype methods called inside String.prototype.{match,search,replace}
should be allowed.

Use callContentFunction even if this value is undefined.
Use constructContentFunction even if newTarget value is constructot itself.

Depends on D152343

These instructions are for callContentFunction and constructContentFunction
in self-hosted JS, to notify possible native call to debugger in the later
patches.

Depends on D152344

See Also: → 1780517
Blocks: 1781061
Pushed by arai_a@mac.com:
https://hg.mozilla.org/integration/autoland/rev/0a7886cefc68
Part 1: Add more functions to eager evaluation allowlist. r=nchevobbe
https://hg.mozilla.org/integration/autoland/rev/2825d06d411b
Part 2: Always use callContentFunction and constructContentFunction for possibly user-provided functions. r=jandem
https://hg.mozilla.org/integration/autoland/rev/58d74b50b7e7
Part 3: Add JSOp::CallContent and JSOp::NewContent. r=jandem
https://hg.mozilla.org/integration/autoland/rev/ba24faeb44f5
Part 4: Add CallReason::CallContent. r=jandem
https://hg.mozilla.org/integration/autoland/rev/54bafa2d55b9
Part 5: Call onNativeCall for callContentFunction and constructContentFunction in self-hosted JS. r=jandem
https://hg.mozilla.org/integration/autoland/rev/df0bf2b761a7
Part 6: Add eager evaluation testcase. r=nchevobbe
Blocks: 1812905
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: