Skip to content

Fix accessing properties defined by a bound function#2299

Closed
lkosson wants to merge 1 commit into
sebastienros:mainfrom
lkosson:bound-function-property
Closed

Fix accessing properties defined by a bound function#2299
lkosson wants to merge 1 commit into
sebastienros:mainfrom
lkosson:bound-function-property

Conversation

@lkosson

@lkosson lkosson commented Mar 1, 2026

Copy link
Copy Markdown

When an object property is defined as a getter, which in turn is a bound function, accessing the property results in InvalidCastException in ObjectInstance.UnwrapFromGetter, because getter variable is Jint.Native.Function.BindFunction instead of Jint.Native.Function.Function.

Explicit test for BindFunction is not ideal and perhaps ICallable should be used, but I'm not sure how to properly pass it to Engine.

Test code to reproduce the behavior:

Console.WriteLine(new Engine().Evaluate("""
	const holder = {
		x: 42,
		getter() { return this.x; }
	};

	const target = {};

	Object.defineProperty(target, "prop", { get: holder.getter.bind(holder) });

	target.prop;
"""));

Before fix: throws InvalidCastException
After fix: prints out 42

@lahma

lahma commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

Let's see copilot's review, also each bug fic should contain also a test case proving the fix.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a runtime crash when an accessor property’s getter is a bound function, which currently triggers an InvalidCastException during property access in ObjectInstance.UnwrapFromGetter.

Changes:

  • Adds special-case handling for Function.BindFunction in ObjectInstance.UnwrapFromGetter to avoid casting it to Function.Function.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +422 to 425
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject);

var functionInstance = (Function.Function) getter;
return functionInstance._engine.Call(functionInstance, thisObject);

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bindFunction._engine.Call(bindFunction, thisObject) resolves to Engine.Call(JsValue, params JsValue[] arguments), meaning thisObject is passed as a function argument and the getter is invoked with this=undefined. That changes observable behavior (e.g., arguments.length inside the getter becomes 1) and isn’t the correct way to invoke an accessor getter.

Call the overload that sets the receiver as this and passes no arguments (e.g., Call(callable, thisObject, Arguments.Empty) / Call((ICallable)callable, thisObject, Arguments.Empty, ...)).

Suggested change
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject);
var functionInstance = (Function.Function) getter;
return functionInstance._engine.Call(functionInstance, thisObject);
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject, Arguments.Empty);
var functionInstance = (Function.Function) getter;
return functionInstance._engine.Call(functionInstance, thisObject, Arguments.Empty);

Copilot uses AI. Check for mistakes.
Comment on lines +422 to 425
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject);

var functionInstance = (Function.Function) getter;
return functionInstance._engine.Call(functionInstance, thisObject);

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accessor invocation path is still tightly coupled to specific concrete function types (BindFunction vs Function.Function) and will continue to throw InvalidCastException for any other callable getter that isn’t Function.Function. Elsewhere in this file (e.g., TryGetValue) getters are treated as ICallable.

Consider switching UnwrapFromGetter to use IsCallable/ICallable (and then invoking via Engine.Call) so all valid callable getter types are supported without adding more special-cases over time.

Suggested change
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject);
var functionInstance = (Function.Function) getter;
return functionInstance._engine.Call(functionInstance, thisObject);
if (!getter.IsCallable())
{
return Undefined;
}
var callable = (ICallable) getter;
return callable.Call(thisObject, Arguments.Empty);

Copilot uses AI. Check for mistakes.
Comment on lines +422 to +423
if (getter is Function.BindFunction bindFunction) return bindFunction._engine.Call(bindFunction, thisObject);

Copilot AI Mar 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes a specific regression scenario (accessor getter is a bound function), but the PR doesn’t add a regression test to ensure it stays fixed. Please add a test that defines an accessor via Object.defineProperty(..., { get: someFn.bind(...) }) and asserts the property access returns the expected value (and ideally that arguments.length inside the getter remains 0).

Copilot uses AI. Check for mistakes.
@lahma

lahma commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

Created issue #2300 so we can process this more easily.

@lahma

lahma commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

A more robust fix was made in #2301 and now fixed in main.

@lahma lahma closed this Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants