-
Notifications
You must be signed in to change notification settings - Fork 0
Function serialization
Super supports both function call serialization and function body serialization.
Function calls are easy to serialize, they just use a type or field and points to a function signature:
public static void main(String[] args) {
Super.FunctionCall functionCall = Super.newFunctionCall()
.type("DatabaseService")
.functionName("save")
.parameters(TypeMatcher.anyType())
.returnType(TypeMatcher.anyType())
}Function bodies are a bit harder to serialize, given the nature that they are just a sequence of bytecodes operating in a specific context, and they are not as simple as they look, for example, you could declare anonymous type, lambda bodies (which generate functions with body too), so to achieve that, Super provides different ways of serializing function bodies while still achieving static dispatch, and providing almost zero overhead function execution, see the examples below.
fun main(args: Array<String>) {
val userData = newData("User") {
props<String>("name", "email")
}
val saveFunction = Super.newFunctionDescriptor("save")
.parameters(TypeMatcher.ANY)
.returnType(TypeMatcher.ANY)
val shape = Super.newShape("databaseService")
.functions(saveFunction)
val functionBody = Super.newFunctionBody()
.shapes(shape)
.fromKores(Instructions.fromVarArgs(
saveFunction.asInvocation(InvokeType.INVOKE_VIRTUAL, shape, listOf(userData.dataVariableAccess("user")))
))
}As you can see, you could use Kores-Base API to describe method body, as Kores uses Data-classes to represent its structures, it is very portable to serialization, however, Kores learning curve is veery high and documentation very poor, so an alternative is to use Javaparser, you write Java source code and Super takes care of converting it to Kores structure, see the example below (you will also need Super-JavaParser):
fun main(args: Array<String>) {
val userData = newData("User") {
props<String>("name", "email")
}
val saveFunction = Super.newFunctionDescriptor("save")
.parameters(TypeMatcher.ANY)
.returnType(TypeMatcher.ANY)
val shape = Super.newShape("databaseService")
.functions(saveFunction)
val functionBody = Super.newFunctionBody()
.shapes(shape)
.usingJavaParser()
.source("databaseService.save(this.user)")
}However, this comes with a very visible problem, it introduces dynamic code, which relies on Runtime Validation of Source Code as well as Validation of Kores Structure against Shape and Data, to solve this, you could completely rely on static code, however, this involves post-processing, which will generate a JSON describing your function body using Kores structure, see an example below:
interface DatabaseService {
fun save(any: Any): Any
}
fun main(args: Array<String>) {
val userData = newData("User") {
props<String>("name", "email")
}
val saveFunction = Super.newFunctionDescriptor("save")
.parameters(TypeMatcher.ANY)
.returnType(TypeMatcher.ANY)
val shape = Super.newShape("databaseService")
.functions(saveFunction)
val functionBody = Super.newFunctionBody()
.shapes(shape)
.usingPostProcessing()
.link<DatabaseService>(shape)
.lambda {
val user = parameter<Any>("user", userData.asType).value
val databaseService = field<DatabaseService>("databaseService", shape.asType).value
databaseService.save(user)
}
}Note that, the last one is a WIP feature, post-processing involves a hard work to get nearly perfect, and currently the only possible way of post-processing these classes is using the CLI of the project reponsible for the processing:
java -jar SuperProcessor.jar --from=build/ --out=processor_json/And these JSON files should be included inside the Jar or module in META-INF.
And as last resort for function execution, you could provide an entire class to execute the code, the class has access to local variables as well to exposed fields and methods, but in this case, there is some overhead. This feature is provided by Super-ExecClass:
@Shape
interface DatabaseService {
fun save(any: Any): Any
}
@DataShape
interface User {
val name: String
val email: String
}
class DatabaseSaver {
@Exec(locals = ["user"], fields = ["databaseService"])
fun executor(locals: Locals, fields: Fields, executionInfo: ExecutionInfo): Any {
val user: User = locals.get("user", type<User>())
val databaseService: DatabaseService = fields.get("databaseService", type<DatabaseService>())
return databaseService.save(user)
}
}The overhead here is introduced by these elements:
- Locals, Fields and ExecutionInfo instantiation (and all the creation process)
- Locals access through a LocalsProvider (and the validation and resolution process)
- Fields access through a FieldsProvider (and the validation and resolution process)
- Wrapper generation around original types, thus providing the ability to invoke their methods through defined interfaces.
There is obviously a bunch of limitations, for example, serialized function invocations could only invoke functions that does exists in the deserializing target, also you could not use any of the types which are available in the scope, unless you specify Shapes which will be used to find types which fits-in, before allowing the invocation to happen.
Also, running codes remotely must be made with care, Super does some validations to check if the code WILL run in the target deserialization context, however it does not prevent reflection code to be executed, neither disallow field accesses which are not exposed. The expose just publishes these functions and properties as linkable, allowing them to be resolved statically and correctly, but does not prevent malicious code from accessing other available fields (but it will not be able to guarantee the static linkage and reliabilty).
If you care about security and wants to use Super remote code execution, do not allow anyone other than the developer to write the code to execute, also make sure to prevent code injection through query parameters, request body, text files, uploaded files, and so on. Super is not intended to have a super security system, because code execution involves so much variables that it will not be possible without limiting the user to the max.