On the Future of Ares and JEP 411 #113
Replies: 12 comments
-
Note that the example in https://inside.java/2021/04/23/security-and-sandboxing-post-securitymanager/ was removed together with its code. The article does not mention why. What was shown was basically registering something that traversed the byte code during class loading of classes of a certain module and looked for method calls to the JDK and replace critical/dangerous ones with throwing an exception. |
Beta Was this translation helpful? Give feedback.
-
There had been an update to JEP-411 as described in https://mail.openjdk.java.net/pipermail/security-dev/2021-July/026806.html. The page now describes the plan to phase out the security manager starting with JDK-18. Applications cannot install a security manager anymore without explicit permission by the user. |
Beta Was this translation helpful? Give feedback.
-
Note that you might be able to carry on using Ares with the |
Beta Was this translation helpful? Give feedback.
-
I don't see an easy solution here, either. In my eyes, the "container" approach is the most promising one for security. This would also solve the problem of "messing with the wrong files": the container could only be given read access to the student's class files and nothing else (except possibly test-specific file resources). Manual assessment could then be done without the container (to allow for easy debugging of student code, and to allow correcting without installing and setting up the used container software.) The problem with the "byte code manipulation" approach is that we either possibly have holes in security, or holes in what students should be allowed to use. The problem arises because we cannot look through the entire JDK library beforehand. But what if Ares contains only a very restrictive whitelist, and manual assessors are asked interactively to allow or deny access to certain features? To summarize this idea: Ares could run in two modes, called "automatic" and "manual" for example. |
Beta Was this translation helpful? Give feedback.
-
Some more ideas and thoughts for automatic mode. The student code still shouldn't be allowed any access to test code. Otherwise, it would be too easy to find side-channels for leaking hidden tests despite the container being denied most forms of communication. A simple example I can think of right now is letting the submission either return something incorrect or crash according to some rules, leaking hidden tests bit by bit. Admittedly, this would be very slow, but not too slow. (I have actually used this method successfully in the past.) This makes some interface between the tester and submission JVMs neccessary. I think System.in/out is the simplest, most obvious way. Of course, this requires some part of Ares to run in the submission JVM and control (the real) System.in/out; let's call this part the "submission controller". It could implement the submission JVM's main method, which immediately takes control of System.in/out and replaces them with virtualized implementations. Keep in mind that student code is allowed to do anything (inside the submission JVM), so we must not assume the things the submission controller sends back to be safe, or the things we send to the controller to be protected. It follows that it must not be neccessary to transmit entire Java objects (like Java serialization does), because it would probably require submission-controlled objects to live in the tester JVM, which would open the very security holes we are trying to solve by separating the JVMs. Transmitting objects from the tester to the submission JVM doesn't seem as dangerous (as long as no accidental references to some hidden things are included), but is still not very desirable. A possible solution is to incorporate some reflection logic in the submission controller: The controller could implement commands to inspect submisison JVM-side objects itself. Objects could then be identified via a submission controller-generated ID. I imagine the protocol between tester and submission controller to look roughly like this:
Again, the tester must never assume replies to be genuine. The tester side could hide this split into tester and submisison JVMs by using |
Beta Was this translation helpful? Give feedback.
-
Thanks for your input, Daniel.
This refers to TUM Judge and GAD in the pre-Artemis times, if I remember correctly. What you describe sounds similar to remote debugging, and goes more in a direction like IT-Sec where the communication channel is very limited. Regarding security, this is great. The only problem I see here is usability. Several exercise creators already told me that Ares is cumbersome to use, documentation is missing and testing gets more "exotic", which causes the tests to be harder to understand for students, too. So while I like the idea from a security point of view, you would not get anybody to use it (based on my experience). We would also need to be cautious that such "automatic" and "manual" modes are compatible and, most importantly, that they don't cause additional effort for exercise creators. I still view the byte code manipulation (e.g. with ByteBuddy) as the most promising solution:
Here I thought of a modified approach: We cannot look through the entire JDK, but that job has been already done, and maybe we could profit from that. All positions where the security manager is currently called would be the candidates for custom checks. This would be feasible exactly if we can manipulate the byte code in all necessary JDK classes. As an added benefit of that, we could make in more restrictive in some places and less restrictive in others, as we don't need everything and, particularly, don't require compatibility.
My only problem is time and incentive here. This is a big job, of course. On the upside, such an implementation could be beneficial for more than just our educational context and may get more recognition and possibly outside support as well. |
Beta Was this translation helpful? Give feedback.
-
I started implementing both ideas (verifying classfiles and separating student code). The implementations are in https://github.com/Haspamelodica/net.haspamelodica.classfileverifier and https://github.com/Haspamelodica/net.haspamelodica.studentcodeseparator. Both are not very fleshed out and probably pretty enigmatic. I'll try to make it easier to understand which part of those repositories is meant to be a part of ARES, which of test code by the exercise creator, and which student code when I find the time. VerifierSo far, verifying class files (even finding out which methods of which classes are called) seems a very difficult job, especially with The core of the verifier is a custom classloader which intercepts all class loading requests by defining all classes. There is a pitfall here: If the loading of a class is delegated to the parent of a custom classloader, and this class then loads other classes, the JVM will directly use the parent, not the custom classloader. This means the custom classloader has to define all classes by itself to catch all loaded classes. However, custom class loaders are not allowed to define classes in Student code separationSo far, I only implemented the interface which exercise creators could use to call student code, as this needs to be convenient enough for this solution to be acceptable. The reflective invocation and serialization parts aren't implemented yet, but on the other hand don't seem to cause problems not solvable by plain Java (and reflection). The interface is designed to be able to transparently call student code either on the same or on a different JVM to make it possible to execute student code in the same JVM without fear of incompabilities between "automatic" and "manual" modes. (The names "automatic" and "manual" are misnomers now, because this solution wouldn't be able to ask the corrector if a used feature is safe out-of-the-box.) Interestingly, the interface for separating student code seems to need the same information needed for structural tests. This means that maybe the structural test oracle doesn't need to be a JSON file anymore, but instead could be expressed with annotations on the separator configuration classes. Debugging student code also seems feasible by letting the corrector attach a debugger to the student-side JVM. Jumps between student and tester code would still be hard to debug, but usually the parts needing to be debugged are in the student code alone, so this doesn't seem a huge problem. Something I haven't thought of so far is how to design safe callbacks of student code to some class provided by tester code. One solution is to load needed tester classes into the student JVM. This might be undesirable, however, if the tester class needs some information about private test cases to respond: The student JVM must be assumed to be controlled entirely by the student. |
Beta Was this translation helpful? Give feedback.
-
Another problem with student code separation is that all objects student code comes in contact with have to live in the student JVM, even "primitive" objects like strings, lists, etc. This makes passing test inputs to student code hard. I have some ideas how to solve this, but haven't implemented them so far. |
Beta Was this translation helpful? Give feedback.
-
I cleaned up the student code separator. Its exercise interface looks very promising! The following two classes are the only ones needed to create an exercise: ExampleExercisepackage net.haspamelodica.studentcodeseparator;
[imports]
public class ExampleExercise
{
public static void main(String[] args)
{
// An instance of StudentSide would be provided by Ares, not created by the tester.
// Also, Ares would not use a ReflectiveStudentSideCommunicator in the tester JVM.
StudentSide studentSide = new StudentSideImpl<>(ExampleExercise.class.getClassLoader(),
new ReflectiveStudentSideCommunicator());
// The StudentSide can (only) be used to obtain instances (implementations) of Prototypes.
// Prototypes provide access to everything static of a class:
// constructors, static fields, static methods.
MyClass.Prototype MyClassP = studentSide.createPrototype(MyClass.Prototype.class);
System.out.println("EXERCISE: --- Testing student-side static things");
// A prototype can be used to call static methods, ...
System.out.println("EXERCISE: staticMethod() returned " + MyClassP.staticMethod());
// ...to set static fields, ...
System.out.println("EXERCISE: Setting myStaticField to \"hello\"");
MyClassP.myStaticField("hello");
System.out.println("EXERCISE: staticMethod() returned " + MyClassP.staticMethod());
// ... and to read static fields.
System.out.println("EXERCISE: myStaticField has value \"" + MyClassP.myStaticField() + "\"");
System.out.println("\nEXERCISE: --- Testing student-side non-static things");
// A prototype can also be used to create instances of student-side objects (SSOs).
System.out.println("EXERCISE: Creating instance with \"Hello World\"");
MyClass instance = MyClassP.new_("Hello World");
// A SSO can be used to call instance methods, to set instance fields, and to read instance fields.
System.out.println("EXERCISE: myField has value \"" + instance.myField() + "\"");
System.out.println("EXERCISE: Setting myField to \"foobar\"");
instance.myField("foobar");
instance.method();
System.out.println("EXERCISE: myField has value \"" + instance.myField() + "\"");
// The names in the exercise-side prototypes / SSOs don't have to match those in the student classes:
// They can be overridden (exercise-side) using an annotation.
System.out.println("EXERCISE: thirdMethod(\"test\") returned " + instance.thirdMethod("test"));
System.out.println("\nEXERCISE: --- Testing non-abstract methods");
// Prototype classes (and SSO classes) can contain methods
// implemented in the prototype / SSO class itself, although I'm not sure where this would be useful.
System.out.println(MyClassP.test2());
}
} MyClasspackage net.haspamelodica.studentcodeseparator;
[imports]
@StudentSideObjectKind(CLASS)
// In a real exercise, it wouldn't be neccessary to use a different name for student-side object and implementation
// because both classes never get loaded in the same JVM anyway.
@OverrideStudentSideName("net.haspamelodica.studentcodeseparator.MyClassImpl")
public interface MyClass extends StudentSideObject
{
@StudentSideObjectMethodKind(INSTANCE_METHOD)
public void method();
@StudentSideObjectMethodKind(INSTANCE_METHOD)
@OverrideStudentSideName("otherThirdMethod")
public int thirdMethod(String param);
@StudentSideObjectMethodKind(FIELD_GETTER)
public String myField();
@StudentSideObjectMethodKind(FIELD_SETTER)
public void myField(String value);
public static interface Prototype extends StudentSidePrototype<MyClass>
{
@StudentSidePrototypeMethodKind(CONSTRUCTOR)
public MyClass new_();
@StudentSidePrototypeMethodKind(CONSTRUCTOR)
public MyClass new_(String param);
@StudentSidePrototypeMethodKind(STATIC_METHOD)
public int staticMethod();
@StudentSidePrototypeMethodKind(STATIC_FIELD_GETTER)
public String myStaticField();
@StudentSidePrototypeMethodKind(STATIC_FIELD_SETTER)
public void myStaticField(String value);
public static void test()
{}
public default String test2()
{
System.out.println("test2");
return test3();
}
private String test3()
{
return "hallo";
}
}
} The student code is completely oblivious to the fact it isn't invoked directly: MyClassImplpackage net.haspamelodica.studentcodeseparator;
public class MyClassImpl
{
private static String myStaticField = "Hello World";
public static int staticMethod()
{
int result = myStaticField.length();
System.out.println(" STUDENT: staticMethod() called; "
+ "myStaticField is \"" + myStaticField + "\" (result is " + result + ")");
myStaticField = "Changed by staticMethod()";
return result;
}
private String myField;
public MyClassImpl(String myField)
{
this.myField = myField + " with changes by student-side constructor";
}
public void method()
{
System.out.println(" STUDENT: method() called. myField has value \"" + myField + "\"");
myField = "myField changed by student";
}
public int otherThirdMethod(String input)
{
System.out.println(" STUDENT: otherThirdMethod(\"" + input + "\")");
return input.length();
}
} There are still two unsolved problems: how to pass parameters to and from the student JVM safely, and how to model class hierarchies. I have some thoughts about this; they are described briefly in StudentSideImpl. |
Beta Was this translation helpful? Give feedback.
-
Currently, the names of student-side classes, methods, and fields have to be repeated in the exercise code. Usually, this kind of unsafety would be regarded bad, but I think it's a good thing in our context: Students can and will write code with an incorrect interface; this shouldn't result in the tests not compiling (and the Artemis build failing), but only in one or two associated tests failing. |
Beta Was this translation helpful? Give feedback.
-
Regarding your suggestions: I'm not sure about that, from a technical perspective, it can work, but I don't think anyone would use something that is remotely more complicated or cumbersome than the current solution. Then again, I didn't have the time to look deep enough into it. I'm currently not working at TUM anymore, so you can suggest and test that in practice. I just know that people are already complaining about the complexity and difficult usage of Ares now, and tend to ditch it altogether if more serious issues appear. So, we need to be careful here. Nevertheless, security is an important concern, and I can imagine courses adopting another solution (e.g. yours) once Ares / SecurityManager-based once cease to work (which I expect to be soon, see https://openjdk.java.net/jeps/411#Description). You can coordinate with the course instructor and the Artemis leads (e.g. Stephan) how this can be used or integrated easily, and replace Ares or turn out to be Ares 2.0.0. (I know already answered you in private but wanted to add it to this conversation as well) |
Beta Was this translation helpful? Give feedback.
-
Update: In addition to JEP-411, JDK-20 and later versions will not feature Alternative mechanisms include bytecode manipulation, which is for example used as a replacement in |
Beta Was this translation helpful? Give feedback.
-
In this post, I want to share some thoughts on the future of Ares and why I have been a bit inactive here lately.
Status
Two weeks ago, I found out that the JEP 411 proposes to deprecate the security manager for removal. In short: managing Java security using the security manager is increasingly difficult and associated with a high maintenance cost compared to its usefulness in most modern applications.
The JEP 411 has now been targeted for JDK 17 and I expect a complete removal of the
SecurityManager
in Java 18-20, given the recent efforts in getting rid of old and today seldom used java features (see how many points listed for JDK 17 mention deprecation or removal). There is no direct replacement planned, apart from a special setting to preventSystem.exit
.Consequences
This means that Ares - as it is now implemented - has no future (long term). Almost all the security features of Ares are realized by the
ArtemisSecurityManager
, a security manager subclass that has a unique behavior due to the generally unusual problems that appear when student (and therefore, unknown) code is tested.Solutions
Almost none, realistically. I will give an overview of what is possible now and might be possible in the future. Whatever we do, it will not be easy if we want to keep most of the features of Ares. The project slogan "A Unit 5 Extension for Easy and Secure Artemis Java Testing" may no longer be accurate.
1. Just use a container
The default in Artemis and many other automatic assessment platforms for Java programming exercises. This provides mostly security concerning the system executing the tests, but not the tests itself. It does not provide security against accidental mistakes, which are however most common. This includes: killing the test process, too many threads, endless loops, messing with the wrong files, .... It will also not solve students circumventing the tests or reading test code.
Another problem is that this is a far more cumbersome test execution. Maybe not for the test systems, but for us humans and manual correctors which execute and debug code on their systems. They need this security even more than the container of a build agent.
In my eyes, just using a container is not an option for the aforementioned reasons. Some additional tools may be able to restrict the access and resources more on process level, but many problems are still the same, especially the manual assessment.
2. Use a JVM inside a JVM
GraalVM makes running Java on Truffle possible, essentially allowing to run a JVM inside a JVM. This gives the host JVM some control over the inner JVM and to set very specific resource restrictions.
But there are some issues, one being that the GraalVM's abilities are according to the documentation not suited as a sandbox or to ensure security. Another is that access restrictions do not work for Java on Truffle. Lastly, the resource limits are only available for the enterprise version.
Of course, we also have additional difficulties because everyone would need to use GraalVM everywhere where the tests need to be run, and the test code would look a lot different (I assume worse than Java reflection). But at least we can be certain that the current Ares API could not be mapped to GraalVM in any way (except with extreme effort).
3. Byte code manipulation
This is the most promising approach in my opinion to maintain the features as well as the API completely or almost completely. What do we have to do? Please have a look into the following article by Ron Pressler, most importantly, the "Shallow Java Sandboxes" section at the bottom: https://inside.java/2021/04/23/security-and-sandboxing-post-securitymanager/.
This requires to use the module system to provide a custom class loader for the student code (module) that checks the byte code when the JVM loads a student class for the first time. The power of that approach is that it is very flexible and allows us to check numerous properties the code must or must not have and manipulate the byte code to throw exceptions if needed. We could also add some additional checks that we can add to the byte code before security relevant methods are called (e.g. before actions on files, we could check the path that is about to be used). But this power and flexibility will likely turn out to be a curse as well.
We have two options here:
You might be able to see that this fully complete is the problem here.
Collecting all JDK features students can safely use is an immense task and might differ for different exercises.
System.exit
(as shown in the article) is not sufficient becauseRuntime.halt
exists as well. All the ways how files and resources can be opened are even worse, without much looking up I can already listjava.io.File
,java.nio.file.Files
,java.nio.channels.FileChannel
,java.net.URL
, ... This is impossible, we would need to reinvent the security manager, but with checks on an even higher level (essentially every action that can have a security manager check today in its stack trace would need to be checked in the future, this can easily be exponential).Currently, both points apply to Ares' functionality: prevent bad actions and some java features (threads, sockets) need to be allowed explicitly. I could see this working to some degree, but only with a very limited scope (
java.lang
andjava.util
for example).If this can be hidden enough in Ares with the class loading, I think it could work almost unchanged in comfort and security, also for manual assessors and debugging.
4. Don't do security
We can simply remove most of the security functionality from Ares. Things like IO-testing, the public/hidden test cases, deadlines, localization, exception post-processing, structural tests, reflection utilities, the
System.exit
-protection and strict timeouts would remain. But some of those features would be compromised by removing the security aspect, and Ares would lose a large portion of its primary purpose: to be a JUnit 5 extension for easy and secure Artemis Java testing.5. Misuse the debugger
We could misuse the debug protocol or similar tools and features to control the application. This could be compared to a mix of point two and three, and be the worst solution. It would end up hopelessly complex because we need to control the JVM fine-grained and precise while not keeping the current Ares API. We have no idea how debugging an application secured by debugging itself at the same time would work out.
The article in point three also mentions the JFR, the Java Flight Recorder, but this is not a solution either because we need to prevent accidental and malicious actions. Recording breaking the rules will not help here, also, we have problems with usability and manual assessment and debugging again.
Conclusion
Having spent a lot of time researching and examining alternatives, I am disappointed at the lack of viable solutions to this problem without security manager. While I know that the
SecurityManager
is complex and is often not used correctly (I completely add theArtemisSecurityManager
in Ares to that list as well, it definitely has its issues), the alternatives don't look better, they are, in fact, for our special use case worse. We have a lot less control and way more work to do. The security our solution, Ares, provides may not be prefect, but it is a high enough hurdle to prevent cheating (attempts to circumvent it can be identified and searched for).On a personal note, I will likely not provide a solution anymore as this requires completely reengineering about 1/3 to 1/2 of Ares' source code and this endeavor will take a lot of time and even more expertise than I have.
This means, consequentially, that I expect Ares not to work anymore for courses in the next 1-3 years if they use the latest Java versions.
Christian Femers, 2021-05-25
What are your thoughts on that? Do you have ideas that could help? Do you know of better solutions?
Update from 2022-11-09:
In addition to JEP-411, JDK-20 and later versions will not feature
Thread.stop()
anymore, so Ares' timeout mechanism stops working as well. For more details, see https://inside.java/2022/11/09/quality-heads-up/Alternative mechanisms include bytecode manipulation, which is for example used as a replacement in
jshell
, see https://bugs.openjdk.org/browse/JDK-8289613 for the details and openjdk/jdk#10166 for the changes.Beta Was this translation helpful? Give feedback.
All reactions