From 8266101d0726ab8eb8a883b5c23f03eeb707f846 Mon Sep 17 00:00:00 2001 From: Moulins Date: Fri, 14 Nov 2025 01:47:31 +0100 Subject: [PATCH] avm1: Fix some edge cases in `super` logic - `super` accesses shouldn't coerce primitives to objects while crawling the prototype chain; - `super()` calls shouldn't call `__resolve` when resolving the `__constructor__` property. --- core/src/avm1/object.rs | 14 ++- core/src/avm1/object/super_object.rs | 38 +++--- .../tests/swfs/avm1/super_edge_cases/Test.as | 109 ++++++++++++++++++ .../swfs/avm1/super_edge_cases/output.txt | 39 +++++++ .../tests/swfs/avm1/super_edge_cases/test.swf | Bin 0 -> 865 bytes .../swfs/avm1/super_edge_cases/test.toml | 1 + 6 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 tests/tests/swfs/avm1/super_edge_cases/Test.as create mode 100644 tests/tests/swfs/avm1/super_edge_cases/output.txt create mode 100644 tests/tests/swfs/avm1/super_edge_cases/test.swf create mode 100644 tests/tests/swfs/avm1/super_edge_cases/test.toml diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index bfa5bba750f8..de47a22f4ead 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -173,7 +173,7 @@ impl<'gc> Object<'gc> { } else { (self, Value::Object(self)) }; - match search_prototype(proto, name.into(), activation, this, is_slash_path)? { + match search_prototype(proto, name.into(), activation, this, is_slash_path, true)? { Some((value, _depth)) => Ok(value), None => Ok(Value::Undefined), } @@ -280,7 +280,7 @@ impl<'gc> Object<'gc> { } let (method, depth) = - match search_prototype(Value::Object(self), name, activation, self, false)? { + match search_prototype(Value::Object(self), name, activation, self, false, true)? { Some((Value::Object(method), depth)) => (method, depth), _ => return Ok(Value::Undefined), }; @@ -395,6 +395,7 @@ pub fn search_prototype<'gc>( activation: &mut Activation<'_, 'gc>, this: Object<'gc>, is_slash_path: bool, + call_resolve_fn: bool, ) -> Result, u8)>, Error<'gc>> { let mut depth = 0; let orig_proto = proto; @@ -432,9 +433,12 @@ pub fn search_prototype<'gc>( depth += 1; } - if let Some(resolve) = find_resolve_method(orig_proto, activation)? { - let result = resolve.call(istr!("__resolve"), activation, this.into(), &[name.into()])?; - return Ok(Some((result, 0))); + if call_resolve_fn { + if let Some(resolve) = find_resolve_method(orig_proto, activation)? { + let result = + resolve.call(istr!("__resolve"), activation, this.into(), &[name.into()])?; + return Ok(Some((result, 0))); + } } Ok(None) diff --git a/core/src/avm1/object/super_object.rs b/core/src/avm1/object/super_object.rs index 343dbe9f14ec..3b56fcea9096 100644 --- a/core/src/avm1/object/super_object.rs +++ b/core/src/avm1/object/super_object.rs @@ -56,16 +56,22 @@ impl<'gc> SuperObject<'gc> { self.depth } - pub(super) fn base_proto(&self, activation: &mut Activation<'_, 'gc>) -> Object<'gc> { + fn base_proto(&self, activation: &mut Activation<'_, 'gc>) -> Option> { let mut proto = self.this(); for _ in 0..self.depth() { - proto = proto.proto(activation).coerce_to_object(activation); + match proto.proto(activation) { + Value::Object(p) => proto = p, + _ => return None, + } } - proto + Some(proto) } pub(super) fn proto(&self, activation: &mut Activation<'_, 'gc>) -> Value<'gc> { - self.base_proto(activation).proto(activation) + match self.base_proto(activation) { + Some(p) => p.proto(activation), + None => Value::Undefined, + } } pub(super) fn call( @@ -74,10 +80,16 @@ impl<'gc> SuperObject<'gc> { activation: &mut Activation<'_, 'gc>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { - let constructor = self - .base_proto(activation) - .get(istr!("__constructor__"), activation)? - .coerce_to_object(activation); + let Some(proto) = self.base_proto(activation) else { + return Ok(Value::Undefined); + }; + + let constructor = istr!("__constructor__"); + let Some((Value::Object(constructor), _depth)) = + search_prototype(proto.into(), constructor, activation, proto, false, false)? + else { + return Ok(Value::Undefined); + }; let NativeObject::Function(constr) = constructor.native() else { return Ok(Value::Undefined); @@ -102,11 +114,11 @@ impl<'gc> SuperObject<'gc> { reason: ExecutionReason, ) -> Result, Error<'gc>> { let this = self.this(); - let (method, depth) = - match search_prototype(self.proto(activation), name, activation, this, false)? { - Some((Value::Object(method), depth)) => (method, depth), - _ => return Ok(Value::Undefined), - }; + let Some((Value::Object(method), depth)) = + search_prototype(self.proto(activation), name, activation, this, false, true)? + else { + return Ok(Value::Undefined); + }; match method.as_function() { Some(exec) => exec.exec( diff --git a/tests/tests/swfs/avm1/super_edge_cases/Test.as b/tests/tests/swfs/avm1/super_edge_cases/Test.as new file mode 100644 index 000000000000..ebed89cab9e1 --- /dev/null +++ b/tests/tests/swfs/avm1/super_edge_cases/Test.as @@ -0,0 +1,109 @@ +// Compile with: +// mtasc -main -header 200:150:30 Test.as -swf test.swf -version 8 +class Test { + + static function main(current) { + var obj; + + trace("#1: `super.foobar()` never does primitive-to-object coercions"); + + String.prototype.foobar = function() { + trace("String.prototype.foobar called!"); + return "string-foobar"; + }; + + current.__proto__ = { + foobar: function() { + trace("_root.__proto__.foobar called!"); + return "_root-foobar"; + } + }; + + obj = { + foobar: function() { + trace("obj.foobar called!"); + trace("super.foobar(): " + super.foobar()); + return "obj-foobar"; + } + }; + + trace("// obj.__proto__ = _root"); + obj.__proto__ = current; + trace("obj.foobar(): " + obj.foobar()); + + trace("// obj.__proto__ = new String('hello')"); + obj.__proto__ = new String("hello"); + trace("obj.foobar(): " + obj.foobar()); + + + trace("// obj.__proto__ = 'hello'"); + obj.__proto__ = "hello"; + trace("obj.foobar(): " + obj.foobar()); + + trace(""); + trace("#2: `super()` never calls `__resolve`"); + + var __constructor__ = function() { + trace("__constructor__ called!"); + return "constructed"; + }; + + trace("// obj.__constructor__ = ..."); + obj = { + __constructor__ : __constructor__, + foobar: function() { + trace("obj.foobar called!"); + var zuper = super; // to bypass MTASC checks + trace("super(): " + zuper()); + } + }; + obj.foobar(); + + trace("// __proto__.__resolve = () => __constructor__"); + obj.__proto__ = { + __resolve: function(name) { + trace("__resolve called with: " + name); + return __constructor__; + } + }; + obj.foobar(); + + // one extra level of nesting than strictly required. + trace("// __proto__.__proto__.__constructor__ = ..."); + obj.__proto__.__proto__ = { + __constructor__: __constructor__ + }; + obj.foobar(); + + + obj.__proto__.addProperty( + "__constructor__", + function() { + trace("__constructor__ property called!"); + return __constructor__; + }, + null + ); + trace("// __proto__.addProperty('__constructor__', ...)"); + obj.foobar(); + + trace("// __proto__ = makeSuperWith(__proto__)"); + obj.__proto__ = makeSuperWith(obj.__proto__); + obj.foobar(); + + trace("// (__proto__ = _root).__constructor__ = ..."); + (obj.__proto__ = _root).__constructor__ = __constructor__; + obj.foobar(); + + + fscommand("quit"); + } + + static function makeSuperWith(obj) { + var helper = { + __proto__: { __proto__: obj }, + getSuper: function() { return super; } + }; + return helper.getSuper(); + } +} diff --git a/tests/tests/swfs/avm1/super_edge_cases/output.txt b/tests/tests/swfs/avm1/super_edge_cases/output.txt new file mode 100644 index 000000000000..55ea9804e687 --- /dev/null +++ b/tests/tests/swfs/avm1/super_edge_cases/output.txt @@ -0,0 +1,39 @@ +#1: `super.foobar()` never does primitive-to-object coercions +// obj.__proto__ = _root +obj.foobar called! +super.foobar(): undefined +obj.foobar(): obj-foobar +// obj.__proto__ = new String('hello') +obj.foobar called! +String.prototype.foobar called! +super.foobar(): string-foobar +obj.foobar(): obj-foobar +// obj.__proto__ = 'hello' +obj.foobar called! +super.foobar(): undefined +obj.foobar(): obj-foobar + +#2: `super()` never calls `__resolve` +// obj.__constructor__ = ... +obj.foobar called! +super(): undefined +// __proto__.__resolve = () => __constructor__ +obj.foobar called! +super(): undefined +// __proto__.__proto__.__constructor__ = ... +obj.foobar called! +__constructor__ called! +super(): constructed +// __proto__.addProperty('__constructor__', ...) +obj.foobar called! +__constructor__ property called! +__constructor__ called! +super(): constructed +// __proto__ = makeSuperWith(__proto__) +obj.foobar called! +__constructor__ property called! +__constructor__ called! +super(): constructed +// (__proto__ = _root).__constructor__ = ... +obj.foobar called! +super(): undefined diff --git a/tests/tests/swfs/avm1/super_edge_cases/test.swf b/tests/tests/swfs/avm1/super_edge_cases/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..0b40e3ba251d7b1629f6f858d28d03efff77181a GIT binary patch literal 865 zcmV-n1D^atS5pXM2LJ$g+H_Uja?(H)J|qE_{ue7)rMk5W#IbbrvLm#$Gj+UZVW2bK zIFqKZ(pVsvEUkS4d&MOe&iD{MiuXQ-_x0>1B>XiOo87bD`Of*yo=v_(pJC^+} z6__D>XNL-pfSVSnq#NOpE;Dc)c1=A+`P>C-wd|Y@@;wXeBLKGE4(k~fQY7lqGsAG0 zYqzf08EUU%`MA@obK4OW7+(j{0xN5TQOBW0S|}92kLW20D;b1DSYD^a5}l6hBf^;< zonBk~Vwy0V&B|laifBxX-vo-xMx)}}#+gSp^0^6`+-pIuk2zq#wt2~FLii`lG#~8f zFt#DQ@>30<-zPwJ$5HLjZnw=&V;8>lEDlY^B_ABrYK)6Kk6LEag@FPDGRfpG1IOxmKL_J9QWd3n~Ke?>Wn;J1bSa)ympO&U)o7DnzJOgDG%yr*x6hC#bGn! zHwq3wq?{1#;)xabe9(R|kY`i6_Z;$7?CoVQaYc(r=|nH4VNZRJ)7uQjJU%_B9U8|6 rA5VZ^Sg!YzlfN{11BH&Xe