I've created a new 'Math Expression' node

Hi dear contributors and devs :raised_hand_with_fingers_splayed:

This weekend I’ve created a Math Expression node that dynamically adds sockets on the fly, and I would like some testers.

It’s all implemented in Python using the GeometryCustomNode type. (See this archived topic if you’re interested in how I managed to do this.)

How It Works

It relies on Python’s exec(). The math expression string is sanitized and then transformed using the ast module into functions that automatically arrange a node tree (similar to @Wannes’ plugin, but integrated directly within a node).

The project is part of the free ‘NodeBooster’ addon, which includes other nodes (such as a Python evaluator node) along with additional features.

Download it on Github:

Documentation

With this node you’ll be able to evaluate a math expression using various math nodes. The variables will be automatically created when you confirm the expression.

About:

  • The following operators are supported: +, -, *, /, %, //, **, and the exponentiation notation using ².
  • Many functions are recognized, such as sin(a), nroot(a,n), lerp(fac,a,b), and many others. Please see the full list in your node editor under N Panel > Node Booster > Active Node > Glossary for function names, their arguments, and descriptions.
  • Macros for irrational numbers are available. The macros Pi, eNum, and Gold can be automatically assigned to their corresponding Unicode symbols (π, 𝑒, and φ), which will be recognized and used. Make sure to press the π button to activate these macros.
  • Algebraic notations are supported (also known as implicit multiplication). You can work with expressions such as 2ab(4ac+π)², where variable names are split into individual characters (e.g., abc is interpreted as a*b*c). To activate this feature, make sure the ab button is toggled ON!
    • Note that function names will still be recognized and prioritized. For example, if you use sin, the interpreter will treat the sequence as a function rather than as individual variables.
    • If you use this algebraic notation mode, ensure there is no ambiguity in the order of operations. For instance, 1/2ab does not yield the same result as 1/(2ab).
  • If anything goes wrong, you will be notified with an error message.
  • In the N Panel > Node Booster > Active Node, you can bake the Plugin Node into a Blender Node Group to share with third parties who do not have the tool.
  • Debug: You can see how the node operates behind the scenes. Your expression is first sanitized and then transformed into a function expression, which is executed by the plugin to create a hidden node tree. Go to N Panel > Node Booster > Active Node > Development to view these debug expressions.
    • Pro Tip: You will also see a ‘NodeTree’ template beneath these debug strings. You can drag and drop the NodeGroup icon (located to the left of the template) to add the hidden node tree to your tree. This allows you to observe what the plugin is doing behind the scenes.

Limitations:

  • Currently, only Geometry Node Tree types are supported.

Possibilities for Improvement:

  • This node has a lot of potential. We could implement dynamic Int or Bool types and evaluate comparison operations easily. More functions, such as ispair(a) or isneg(a), could be added (although adding these functionalities might prevent the node from being ported to the compositor and shader editor).
  • This node could evolve into a more sophisticated Advanced Expressions system, allowing the creation of multi-type variables (e.g., Int, Bool, Vector, Quaternion, and Matrix) to perform more advanced 3D math using vector or matrix notations and functions. The socket type could dynamically swap, and we could even write simple VEX-like code similar to Houdini. If you’d like to see such a project come to life, please consider a donation.

I plan to propose this node as an extension, but if Blender developers decide to integrate Python backend nodes as a fully shipped feature with Blender, I could contribute to that project.

Cheers :champagne:
-Dorian

17 Likes

I only tried the addon very briefly just now, and that worked, nice!

I think it’s good to have something like this on the extensions platform. We do want to have built-in expressions support at some point but there is no clear timeline for that yet. This node group based solution should work quite well. Especially because already built groups should keep working even if the add-on breaks for whatever reason in the future or it is superseeded by built-in functionality.

There are currently no plans to support evaluating Python code during Geometry Nodes evaluation so it probably doesn’t make sense to wait for that. This node group based solution should be quite a bit more performant as well (while also being more limited of course).

On an implementation side, I don’t fully understand why it needs exec. It seems like you should be able to work just with the AST. That’s mainly a security aspect. One needs to be careful with calling exec in general and especially on user-controlled inputs like an expression. I didn’t look much into your code though, so I only have a vague idea of what it does.

1 Like

Thank you for your enthusiasm @jacqueslucke :smiley: Highly appreciated!
Looking forward to launch it on the Extension Platform then! (@nickberckley we are going to be in contact soon :slight_smile: )

Especially because already built groups should keep working even if the add-on breaks for whatever reason in the future or it is superseeded by built-in functionality.

About that… unfortunately if a CustomNodeGroup class is unregistered, the content of the nodetree is ignored. ( For example if we were to pass a geometry in a CustomNodeGroup input/output and use the Transform Geometry node within the custom_node.nodetree, the data will disappear).

I thougt it was a behavior to expect, but perhaps it is a bug i should report?

On an implementation side, I don’t fully understand why it needs exec .

That’s because i would like this concept to evolve into nex (node expression), where the user would select a python script such as

#create input sockets with default values
x:infloat = 0.5 
y:invec = (0.1, 0.5, 0.9)

#do math between python types and nex types
this = 0.5
scaled_vec = (x * y)/this

import bpy
f = bpy.context.scene.frame_current
add_vec = add_vec + Vector((f,)*3)

#and initialize sockets
c:outvec = add_vec

& the concept can only work using exec()

Sounds like something we should support. Yes, a bug report with a simple test file would be useful. I added it to the agenda for the next module meeting.

I can see that it makes it easier, but I’m pretty sure that saying that it can only work using exec is wrong. I did that in the past too tbh, but wouldn’t do it the same way nowadays. I’m mainly asking to take security into account here.

Blender has the Auto Run Python Scripts option and if it is turned off, opening .blend files should not just run arbitrary Python code. However, if I had your add-on installed, it would probably be easy to craft .blend files that run arbitrary (harmful) Python code even if the option is disabled.

4 Likes

Sounds like something we should support. Yes, a bug report with a simple test file would be useful. I added it to the agenda for the next module meeting.

Okay i will report a few bugs related to GeometryNodeCustomGroup tomorrow or this weekend :slight_smile:

I’m mainly asking to take security into account here.

I totally see your point, thank you for pointing that down.

That’s why this afternoon i stripped down down any possibilities of using other than the ones provided. There’s a way to do that like this :slight_smile:

    local_vars = {f.__name__:f for f in USER_FUNCTIONS}
    # this will get rid of any builtins functionalities.
    # the user has no choice but to use the math functions i listed
    global_vars = {"__builtins__": {}} 
    exec(api_expression, global_vars, local_vars)

I guess we’ll evaluate any other security risks on extension reviews.

Blender has the Auto Run Python Scripts option

I will add this option as a check then, assuming it’s exposed to the py api

That’s strange… With ShaderCustomNodeGroups this used to work.

I haven’t tested this in newer versions, but as long as the nodetree is in the blend file, its stored values should still be present when the addon isn’t present… (Also haven’t tested this with GN custom nodes… don’t know if this is a particular case here)

However, all the other CustomNodeGroup functionality will be missing. And that includes UI, updates, or direct changes to output sockets (as in your PythonApi node).

There’s no safe way to use exec(). You can’t sanitize it to patch all possible holes. The official docs say “calling exec() with user-supplied input may lead to security vulnerabilities.” This cannot, under any circumstances, be officially incorporated into an official Blender release

There’s no safe way to use exec(). You can’t sanitize it to patch all possible holes

Show me, please!

    superdangerous_text = "?"
    #sanatized exec below
    assert '_' not in superdangerous_text 
    exec(superdangerous_text, {"__builtins__": {}} , {})

This cannot, under any circumstances, be officially incorporated into an official Blender release

Got a response for jacques, it will be for the extension platform along with other nodes and functionalities

Hmmm
I’ve did some tests last month and the geometry disapeared. Perhaps it’s related only to Geometry types. :thinking:

Just letting you know that we don’t accept exec() and eval() on extensions platform either.

2 Likes

@nickberckley And what if the plugin in question is based on python scripts?
i could rework the math expression node with ast, but what about nodes such as this one whose whole concept rely on passing python evaluated values?

image

I would have to see/test that to know final answer, to see how safe it can be, but most probably we will not allow it

Compliance to rules and Blender Foundation governance. Got it.
Not quite the open and free spirit I was expecting, but understood.

Here’s a simple exploit that runs echo "pwned" (even though the built-in functions have been “cleared”):

exec(b'\x5b\x78\x20\x66\x6f\x72\x20\x78\x20\x69\x6e\x20\x20\x5b\x5d\x2e\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f\x2e\x5f\x5f\x62\x61\x73\x65\x5f\x5f\x2e\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f\x28\x29\x20\x69\x66\x20\x78\x2e\x5f\x5f\x6e\x61\x6d\x65\x5f\x5f\x20\x3d\x3d\x20\x22\x42\x75\x69\x6c\x74\x69\x6e\x49\x6d\x70\x6f\x72\x74\x65\x72\x22\x5d\x5b\x30\x5d\x28\x29\x2e\x6c\x6f\x61\x64\x5f\x6d\x6f\x64\x75\x6c\x65\x28\x22\x6f\x73\x22\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x22\x65\x63\x68\x6f\x20\x70\x77\x6e\x65\x64\x22\x29', {"__builtins__": {}} , {})

Would be good to avoid using exec or eval if we can, which seems like would be the case here with a bit more work.

6 Likes

Here’s a simple exploit that runs echo “pwned” (even though the built-in functions have been “cleared”):

Oh wow! Thank you so much for demonstrating to me :smiley:
I will study this!

Would be good to avoid using exec or eval if we can, which seems like would be the case here with a bit more work.

There’s a misunderstanding here, & there’s a bit of context needed
When i said this

& the concept can only work using exec()

I was not referring to the math expression node, which indeed can be ported to ast, but to the nodal expression ‘language’ i’m building, which is an extension of bpy.

Here’s a video of the ‘concept’ that i was referring to to clear out any miscommunication

4 Likes

I’m very glad to hear this, thanks for being security minded

@jacqueslucke i’ve been reporting the issue right here

MathExpression node could indeed be ported to ast

The math expression node has also been fully ported to the ast module this week.
Only the python related nodes shall evaluate python code. :slight_smile:

Since the expressed concerns (& demonstration :smile:) over security from the community, i’ve made some changes, i can affirm that now these custom python evaluator nodes will never evaluate python code automatically. Thanks to your feedbacks.

Users will always need to give their consent for automatic execution of python snippets on each startup by toggling the 'Allow Automatic Executions’ check boxes. (Similar to the driver warning checkbox on user startup).

nodal expression ‘language’ i’m buildin

I have also good news regarding this ‘NexScript’ i’m working on.
The project is moving fast. Users are now able to do Matrix and Vector math between SocketMatrix, SocketVector and mathutils.Matrix mathutils.Vector and vice-versa.

This is the kind of node-generating langage that is currently possible

#NexDemo.py
import bpy

# Initialization
# first we need to define our socket variables!
myFloatA:infloat
myFloatB:infloat = 0.123 #you can also assign a default value!
myV:invec
myBoo:inbool = True
sockMatrix:inmat

# do math between types  using '+' '-' '*' '/' '%' '**' '//' symbols,
# or functions. (see the full list in 'NodeBooster > Glossary' panel)
c = (myFloatA + myFloatB)/2
c = nroot(sin(c),cos(123)) // myFloatB

# Do comparisons between types using '>' '<' '!=' '==' '<=' '>='
# result will be a socket bool
isequal = myFloatA == myFloatB
islarger = myV > myBoo
# Do bitwise operation with symbols '&', or '|' on socket bool
bothtrue = isequal & islarger

# You can evaluate any python types you wish to, and do operations between socket and python types
frame = bpy.context.scene.frame_current
ActiveLov = bpy.context.object.location
c += abs(frame)

# easily access Vector or Matrix componements.
c += (myV.x ** myV.length) + sockMatrix.translation.z
newvec = combine_xyz(c, frame, ActiveLov.x)

# Do Advanced Matrix and Vector operation 
ActiveMat = bpy.context.object.matrix_world
pytuple = (1,2,3)
TransVec = (sockMatrix.inverted() @ ActiveMat) @ newvec.normalized()
TransVec = sockMatrix @ cross(pytuple,TransVec,)
TransVec = sqrt(TransVec) #math operations can also work entry-wise on vectors.
minElement = min(separate_matrix(sockMatrix)) #get lowest socketfloat element of Matrix.

# types can be itterable
newvalues = []
for i,component in enumerate(TransVec):
    newval = component + minElement + i
    newvalues.append(newval)
TransVec[:] = newvalues

# Because we are using python you can create functions you can reuse too
def entrywise_sinus_on_matrix_elements(SockMatrix):
    new = []
    for f in separate_matrix(SockMatrix):
        new.append(sin(f))
    return combine_matrix(new)

newMat = entrywise_sinus_on_matrix_elements(sockMatrix)

# Then we assign the socket to an output
# you can define a strict output type
# or auutomatically define the output scoket with 'outauto'
BoolOut:outbool = bothtrue
TransVec:outvec = TransVec
myMatrix:outauto = newMat

Behind the scene, the script will automatically generate the equivalent nodes of the python operand and functions. If the user choose to, he can also automatically sync the evaluated python value from the script with the nodetree with no noticeable slowdowns!

Still have a lot of work to do with NexScript, SocketInt, SocketColor, SocketQuaternionRotation still have to be implemented, but it’s getting there :slight_smile:

7 Likes