godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Enhance `Expression` features for more complex user scripting (Implement `FunctionExpression`) #7994

Open ryanabx opened 1 year ago

ryanabx commented 1 year ago

Describe the project you are working on

Godot, and on the side a Tabletop sandbox where users can create custom scripts to run tabletop games of their choice.

Describe the problem or limitation you are having in your project

I am considering many options for the method of user scripting. I have an implementation in GDScript, but GDScript is inherently insecure in its current form (see: https://github.com/godotengine/godot-proposals/issues/5010), and have alternatively been looking at Godot-WASM https://github.com/ashtonmeuser/godot-wasm and Godot_luaAPI https://github.com/WeaselGames/godot_luaAPI . These are fine projects, but I believe a first party solution to simple, secure user scripts should be available.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

An overhaul of the current Expression system should be considered. Alternatively a new class could be created for a more aptly defined use-case, perhaps FunctionExpression or something of the sort. This system is open to discussion to ensure that the any function is properly sandboxed based on the developer's allowed inputs to the user's function expression.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Some of the features I would like to implement in such an overhaul are:

An example of all these features in one expression would look like:


class MyOtherClass:
    var sanctioned_property := 4
    var unsanctioned_property := get_node("..")
    def sanctioned_function() -> int:
        return randi_range(0, 10)

    def unsanctioned_function() -> Variant:
        return OS.shell_open("http://my-secret-website.com/")

class MyClass:
    var other:= MyOtherClass.new()

    def get_my_other_class() -> MyOtherClass:
        return other

    def do_expression():
        var ex := FunctionExpression.new()
        ex.parse(
            "
            var res := x + get_my_other_class().sanctioned_property
            for i in range(NUM_ITERATIONS):
                i += get_my_other_class().sanctioned_function()
            return res
            ", # Function string
            input_variables = ["x"], # Input variables, defined as a PackedStringArray
            constants = {"NUM_ITERATIONS": 10}, # Constants, for the mock-up this is entered in as a dict
            sanctioned_functions = {
                MyOtherClass: ["sanctioned_function"],
                @globalscope: ["range"],
                MyClass: ["get_my_other_class"]
            } # Sanctioned functions. Defined as a dictionary for this mock-up, but open to discussion.
              # No functions outside of these can be called by the expression.
            sanctioned_properties = {
                MyOtherClass: ["sanctioned_property"]
            } # Sanctioned properties. No Properties outside of these can be accessed by the expression.
        )
        var result := ex.execute([2], base_instance = self) # Executing the function expression
        print(result)

If the FunctionExpression attempts to call a function that is not sanctioned for it, then it will act as if the function does not exist, most likely producing an error of some sort.

If this enhancement will not be used often, can it be worked around with a few lines of script?

N/A

Is there a reason why this should be core and not an add-on in the asset library?

N/A

ryanabx commented 1 year ago

In the past, there have been different proposed ways to implement secure user scripting. This is one of the ways that I believe has not been proposed in the past. I might additionally create other proposals of other ways the problem of secure user scripting can be solved.

Calinou commented 1 year ago

I feel this would add a lot of complexity, not to mention making Expression Turing-complete kind of breaks its point (its scripting abilities are limited on purpose).

Allowing GDScript to be sandboxed is probably a better goal in the long run.

ryanabx commented 1 year ago

I feel this would add a lot of complexity, not to mention making Expression Turing-complete kind of breaks its point (its scripting abilities are limited on purpose).

Allowing GDScript to be sandboxed is probably a better goal in the long run.

I agree that sandboxing GDScript is a more powerful long-term goal. I think that this could be a middle-ground where you might want something more than a simple Expression but not something that allows the user to do essentially what they want save for the singleton proxies described in Reduz's proposal.

At the very least, a way to limit the functions the user can utilize would be a wonderful tool. I created a second proposal with an alternative method of doing so (using a new subclass of the GDScript class, and changes to the GDScript analyzer to account for that change)

I think that either this proposal or the other one accomplish that goal, but perhaps the other one accomplishes it with less work.

For the other proposal, see: https://github.com/godotengine/godot-proposals/issues/7996