Thursday 16 February 2023

Lucee bug surfaced by CFWheels's "architecture"


Well here's a fine use of my frickin time.

Lucee's released a new "stable" version and we're being encouraged to use it. Today I upgraded my dev environment to Lucee (from which seems quite stable and gave us no issues), ran our tests to see if anything obvious went wrong, and a whole lot of them went splat. All of them were variations of this (this is from my repro case, not our codebase):

Lucee Error (expression)
MessageCannot update key [MAORI_NUMBERS] in static scope from component [cfml.vendor.lucee.staticFinal.C], that member is set to final
StacktraceThe Error Occurred in
/var/www/cfml/vendor/lucee/staticFinal/C.cfc: line 5
3: static {
4: final static.MAORI_NUMBERS = ["tahi", "rua", "toru", "wha"]
5: }
6: }

called from /var/www/cfml/vendor/lucee/staticFinal/C.cfc: line 1
called from /var/www/cfml/vendor/lucee/staticFinal/test.cfm: line 2

The code in question is this:

component extends=Base {

    static {
        final static.MAORI_NUMBERS = ["tahi", "rua", "toru", "wha"]

I am not trying to "update key [MAORI_NUMBERS] [etc]", I am simply trying to create the object.

Roll back to code works.

My initial attempt to reproduce the error was just this:

component {

    static {
        final static.MAORI_NUMBERS = ["tahi", "rua", "toru", "wha"]

But that worked fine, no errors.

Notice how I am not extending anything there? This is significant.

What's in Base.cfc? I'm kind of embarrassed to show you this.

component {

    include "include.cfm";

"WTF?" you might legitimately ask. Well: quite. The thing is I found this issue in a CFWheels application, and it's down to CFWheels's "architecture" that this bug in Lucee surfaces.

Look at this… stuff:

component output="false" displayName="Model" {

    include "model/functions.cfm";
    include "global/functions.cfm";
    include "plugins/standalone/injection.cfm";


That's how CFWheels implements its base model class. They are pretending CFML has mixins (it doesn't) by using includes. All their classes seem to be defined as a series of include files. I just… just… I just… … aaaaaaaah!!! Just do me a favour and don't use CFWheels. Trust me.

Anyway, so this is why I'm writing shit code to reproduce this Lucee bug.

In include.cfm I have this:

public function build() {
    return this

(This is also how CFWheels initialises its model class objects: using a sort of factory method. So I'm replicating a pared-down version of that).

And CFWheels creates its objects like this (in test.cfm):

<cfinvoke component="C" method="build" returnVariable="o">

Yeah. <cfinvoke>. Ain't seen one of those in about a decade (and it was old code then…).

And if I run that code, I get the error concerned.

I've tried to pare it back further, but it seems I need the sub class, base class, include combo. Lucee is - it seems - doing something dodgy when it instantiates an object like this. No surprise they didn't catch it during their own tests, because this is a very - um - "edge case" approach to designing code.

Another weird thing is that if I restart my Docker container: the problem goes away. However if I then do something like change the name of include.cfm to includex.cfm or something, the problem comes back. Adding code to either CFC does not bring the issue back (if you see what I mean). It's def down to something about how a base class that has an include in it is first "created" (I hesitate to say "compiled" here, cos then I don't think the issue would magically go away between container restarts: the compiled code seems fine. Just the in-memory code after the compilation the first time round is crocked. Anyhow, I won't try to guess what's wrong, I'll leave that to the Lucee bods. They can have my repro case to help them (here on GitHub: /vendor/lucee/staticFinal).

I encountered a ColdFusion bug during assessing this: time to write that one up. (update: this one has already been reported, as it turns out: CF-4213214).