From d3bde22d8f0d21f25cbe91867291b8567b0fe57b Mon Sep 17 00:00:00 2001 From: "Thomas E. Enebo" Date: Sat, 5 Jul 2025 14:48:54 -0500 Subject: [PATCH 1/4] Quick stab and limited profile runtime This was quickest and minimal needed to work to get a profile which can load a Ruby runtime. It is not really correct and is meant as a starting point. There are some obvious problems with profile as it is defined. Basic fundamental things like primal Exception types need to be loaded. Allowing people to omit them is a footgun. There is obvious issues with ommitting jruby/kernel. Some of that is required but at the same time it likely hits profile excluded types. Regex is a major source of DOS so it must be excludable but at the same time I suspect we call it many places internally. Methods which use types which are excludable (like Regexp) probably should be aware of excluded types and not bind. This would be a MAJOR amount of work but it would fit into idea of a dependency graph. Likewise we could make a much smarter type declaration where something like: ```java return defineClass(context, "Integer", Numeric, NOT_ALLOCATABLE_ALLOCATOR). reifiedClass(RubyInteger.class). kindOf(new RubyModule.JavaClassKindOf(RubyInteger.class)). classIndex(ClassIndex.INTEGER). defineMethods(context, RubyInteger.class). tap(c-> c.singletonClass(context).undefMethods(context, "new")); ``` (we are only pretending with this example as Integer is too primal to Ruby to be allowed to be excluded) we would want to add something to this which would know that it cannot defineClass unless "Numeric" has been defined. ```java return requires("Numeric", "Fixnum", "Bignum"). defineClass(context, "Integer", Numeric, NOT_ALLOCATABLE_ALLOCATOR). reifiedClass(RubyInteger.class). kindOf(new RubyModule.JavaClassKindOf(RubyInteger.class)). classIndex(ClassIndex.INTEGER). defineMethods(context, RubyInteger.class). tap(c-> c.singletonClass(context).undefMethods(context, "new")); ``` This would end up simplifying the smattering of if's in Ruby to just declare these dependencies in the setup method for the type. --- core/src/main/java/org/jruby/Ruby.java | 38 ++++++++++------ core/src/main/java/org/jruby/RubyGlobal.java | 4 +- .../jruby/javasupport/JavaEmbedUtilsTest.java | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/org/jruby/Ruby.java b/core/src/main/java/org/jruby/Ruby.java index 9dd148c1cc5..fad8ebbd1f4 100644 --- a/core/src/main/java/org/jruby/Ruby.java +++ b/core/src/main/java/org/jruby/Ruby.java @@ -507,13 +507,21 @@ private Ruby(RubyInstanceConfig config) { initExceptions(context); // Thread library utilities - mutexClass = Mutex.setup(context, threadClass, objectClass); - conditionVariableClass = ConditionVariable.setup(context, threadClass, objectClass); - queueClass = Queue.setup(context, threadClass, objectClass); - closedQueueError = Queue.setupError(context, queueClass, stopIteration, objectClass); - sizedQueueClass = SizedQueue.setup(context, threadClass, queueClass, objectClass); - - fiberClass = new ThreadFiberLibrary().createFiberClass(context, objectClass); + if (profile.allowClass("Thread")) { + mutexClass = Mutex.setup(context, threadClass, objectClass); + conditionVariableClass = ConditionVariable.setup(context, threadClass, objectClass); + queueClass = Queue.setup(context, threadClass, objectClass); + closedQueueError = Queue.setupError(context, queueClass, stopIteration, objectClass); + sizedQueueClass = SizedQueue.setup(context, threadClass, queueClass, objectClass); + fiberClass = new ThreadFiberLibrary().createFiberClass(context, objectClass); + } else { + mutexClass = null; + conditionVariableClass = null; + queueClass = null; + closedQueueError = null; + sizedQueueClass = null; + fiberClass = null; + } dataClass = RubyData.createDataClass(context, objectClass); @@ -1676,12 +1684,12 @@ private void initExceptions(ThreadContext context) { ifAllowed("KeyError", (ruby) -> keyError = RubyKeyError.define(context, indexError)); ifAllowed("DomainError", (ruby) -> mathDomainError = RubyDomainError.define(context, argumentError, mathModule)); - setRegexpTimeoutError(regexpClass.defineClassUnder(context, "TimeoutError", getRegexpError(), RubyRegexpError::new)); + ifAllowed("Regex", (ruby) -> setRegexpTimeoutError(regexpClass.defineClassUnder(context, "TimeoutError", getRegexpError(), RubyRegexpError::new))); RubyClass runtimeError = this.runtimeError; - ObjectAllocator runtimeErrorAllocator = runtimeError.getAllocator(); - if (Options.FIBER_SCHEDULER.load()) { + if (profile.allowClass("Thread") && Options.FIBER_SCHEDULER.load()) { + ObjectAllocator runtimeErrorAllocator = runtimeError.getAllocator(); bufferLockedError = ioBufferClass.defineClassUnder(context, "LockedError", runtimeError, runtimeErrorAllocator); bufferAllocationError = ioBufferClass.defineClassUnder(context, "AllocationError", runtimeError, runtimeErrorAllocator); bufferAccessError = ioBufferClass.defineClassUnder(context, "AccessError", runtimeError, runtimeErrorAllocator); @@ -1764,7 +1772,7 @@ private void initJavaSupport(ThreadContext context) { // if we can't use reflection, 'jruby' and 'java' won't work; no load. boolean reflectionWorks = doesReflectionWork(); - if (reflectionWorks) { + if (reflectionWorks && profile.allowLoad("java")) { new Java().load(context.runtime, false); new JRubyUtilLibrary().load(context.runtime, false); @@ -1775,7 +1783,9 @@ private void initJavaSupport(ThreadContext context) { private void initRubyKernel() { // load Ruby parts of core - loadService.loadFromClassLoader(getClassLoader(), "jruby/kernel.rb", false); + if (profile.allowLoad("jruby/kernel")) { + loadService.loadFromClassLoader(getClassLoader(), "jruby/kernel.rb", false); + } } private void initRubyPreludes() { @@ -1783,7 +1793,9 @@ private void initRubyPreludes() { if (RubyInstanceConfig.DEBUG_PARSER) return; // load Ruby parts of core - loadService.loadFromClassLoader(getClassLoader(), "jruby/preludes.rb", false); + if (profile.allowLoad("jruby/preludes")) { + loadService.loadFromClassLoader(getClassLoader(), "jruby/preludes.rb", false); + } } public IRManager getIRManager() { diff --git a/core/src/main/java/org/jruby/RubyGlobal.java b/core/src/main/java/org/jruby/RubyGlobal.java index 2e58c1c9810..6ef275d5b7e 100644 --- a/core/src/main/java/org/jruby/RubyGlobal.java +++ b/core/src/main/java/org/jruby/RubyGlobal.java @@ -199,7 +199,9 @@ public static RubyHash createGlobalsAndENV(ThreadContext context, GlobalVariable runtime.defineVariable(new LastlineGlobalVariable(runtime, "$_"), FRAME); runtime.defineVariable(new LastExitStatusVariable(runtime, "$?"), THREAD); - runtime.defineVariable(new ErrorInfoGlobalVariable(runtime, "$!", context.nil), THREAD); + if (runtime.getProfile().allowClass("Thread")) { + runtime.defineVariable(new ErrorInfoGlobalVariable(runtime, "$!", context.nil), THREAD); + } runtime.defineVariable(new NonEffectiveGlobalVariable(runtime, "$=", context.fals), GLOBAL); if(instanceConfig.getInputFieldSeparator() == null) { diff --git a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java index ce4bf4c39d6..cddd30a1662 100644 --- a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java +++ b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java @@ -16,6 +16,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import org.jruby.Profile; +import org.jruby.RubyFixnum; import org.jruby.java.proxies.ConcreteJavaProxy; import org.jruby.java.proxies.JavaProxy; import org.jruby.runtime.ThreadContext; @@ -64,6 +66,48 @@ public void testAddClassloaderToLoadPathOnTCCL() throws Exception { assertEquals(result, "uri:" + url); } + class CustomProfile implements Profile { + private List classAllow = List.of("String", "Fixnum", "Integer", "Numeric", "Hash", "Array", + "Thread", "ThreadGroup", "RubyError", "StopIteration", "LoadError", "ArgumentError", "Encoding", + "EncodingError", "StandardError", "Exception"); + + @Override + public boolean allowBuiltin(String name) { + return false; + } + + @Override + public boolean allowClass(String name) { + return classAllow.contains(name); + } + + @Override + public boolean allowModule(String name) { + return false; + } + + @Override + public boolean allowLoad(String name) { + return false; + } + + @Override + public boolean allowRequire(String name) { + return false; + } + } + + @Test + public void testRestrictedProfile() throws Exception { + RubyInstanceConfig config = new RubyInstanceConfig(); + config.setDisableGems(true); + config.setProfile(new CustomProfile()); + + Ruby runtime = Ruby.newInstance(config); + assertEquals(20L, ((RubyFixnum) runtime.evalScriptlet("def double(a); a * 2; end; double(10)")).getValue()); + //Ruby runtime = JavaEmbedUtils.initialize(EMPTY, config); + } + @Test public void testAddClassloaderToLoadPathOnNoneTCCL() throws Exception { RubyInstanceConfig config = new RubyInstanceConfig(); From 92b4cf8268d8858523dd85061a048bed2901518c Mon Sep 17 00:00:00 2001 From: "Thomas E. Enebo" Date: Wed, 9 Jul 2025 14:10:15 -0500 Subject: [PATCH 2/4] What it takes to make JI load --- core/src/main/java/org/jruby/Ruby.java | 2 +- .../test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/jruby/Ruby.java b/core/src/main/java/org/jruby/Ruby.java index fad8ebbd1f4..e2830667fa6 100644 --- a/core/src/main/java/org/jruby/Ruby.java +++ b/core/src/main/java/org/jruby/Ruby.java @@ -1772,7 +1772,7 @@ private void initJavaSupport(ThreadContext context) { // if we can't use reflection, 'jruby' and 'java' won't work; no load. boolean reflectionWorks = doesReflectionWork(); - if (reflectionWorks && profile.allowLoad("java")) { + if (reflectionWorks) { new Java().load(context.runtime, false); new JRubyUtilLibrary().load(context.runtime, false); diff --git a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java index cddd30a1662..11b404bcf95 100644 --- a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java +++ b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java @@ -70,6 +70,8 @@ class CustomProfile implements Profile { private List classAllow = List.of("String", "Fixnum", "Integer", "Numeric", "Hash", "Array", "Thread", "ThreadGroup", "RubyError", "StopIteration", "LoadError", "ArgumentError", "Encoding", "EncodingError", "StandardError", "Exception"); + private List loadAllow = List.of("jruby/java.rb", "jruby/java/core_ext.rb", "jruby/java/java_ext.rb", + "jruby/java/core_ext/object.rb"); @Override public boolean allowBuiltin(String name) { @@ -88,7 +90,7 @@ public boolean allowModule(String name) { @Override public boolean allowLoad(String name) { - return false; + return loadAllow.contains(name); } @Override From 18094d5561d13a69a5f93dbd4596e2fe54657b4a Mon Sep 17 00:00:00 2001 From: "Thomas E. Enebo" Date: Wed, 9 Jul 2025 14:58:27 -0500 Subject: [PATCH 3/4] Add NameError and some more examples of calling into restricted profile --- .../org/jruby/javasupport/JavaEmbedUtilsTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java index 11b404bcf95..1e3f70fcbbf 100644 --- a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java +++ b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java @@ -13,11 +13,14 @@ import static org.jruby.api.Create.newEmptyArray; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.jruby.Profile; import org.jruby.RubyFixnum; +import org.jruby.RubyString; +import org.jruby.exceptions.NameError; import org.jruby.java.proxies.ConcreteJavaProxy; import org.jruby.java.proxies.JavaProxy; import org.jruby.runtime.ThreadContext; @@ -69,7 +72,7 @@ public void testAddClassloaderToLoadPathOnTCCL() throws Exception { class CustomProfile implements Profile { private List classAllow = List.of("String", "Fixnum", "Integer", "Numeric", "Hash", "Array", "Thread", "ThreadGroup", "RubyError", "StopIteration", "LoadError", "ArgumentError", "Encoding", - "EncodingError", "StandardError", "Exception"); + "EncodingError", "StandardError", "Exception", "NameError"); private List loadAllow = List.of("jruby/java.rb", "jruby/java/core_ext.rb", "jruby/java/java_ext.rb", "jruby/java/core_ext/object.rb"); @@ -107,7 +110,11 @@ public void testRestrictedProfile() throws Exception { Ruby runtime = Ruby.newInstance(config); assertEquals(20L, ((RubyFixnum) runtime.evalScriptlet("def double(a); a * 2; end; double(10)")).getValue()); - //Ruby runtime = JavaEmbedUtils.initialize(EMPTY, config); + assertThrows(NameError.class, () -> runtime.evalScriptlet("File.open('test.tmp')")); + assertThrows(NameError.class, () -> runtime.evalScriptlet("UDPSocket.new")); + assertEquals("cute_cats",((RubyString)runtime.evalScriptlet("\"cute_cats\"")).getValue()); + assertEquals("cute_cat",((RubyString)runtime.evalScriptlet("\"cute_cats\".delete('s')")).getValue()); + //assertThrows(NameError.class, () -> runtime.evalScriptlet("IO.sysopen('test.tmp')")); } @Test From ca347bb0de00a09c5a6f6a09b2d8fe8a45bbb6f8 Mon Sep 17 00:00:00 2001 From: "Thomas E. Enebo" Date: Fri, 11 Jul 2025 09:46:10 -0500 Subject: [PATCH 4/4] SyntaxError and its parent must be loaded --- .../test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java index 1e3f70fcbbf..116159378a0 100644 --- a/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java +++ b/core/src/test/java/org/jruby/javasupport/JavaEmbedUtilsTest.java @@ -21,6 +21,7 @@ import org.jruby.RubyFixnum; import org.jruby.RubyString; import org.jruby.exceptions.NameError; +import org.jruby.exceptions.SyntaxError; import org.jruby.java.proxies.ConcreteJavaProxy; import org.jruby.java.proxies.JavaProxy; import org.jruby.runtime.ThreadContext; @@ -72,7 +73,7 @@ public void testAddClassloaderToLoadPathOnTCCL() throws Exception { class CustomProfile implements Profile { private List classAllow = List.of("String", "Fixnum", "Integer", "Numeric", "Hash", "Array", "Thread", "ThreadGroup", "RubyError", "StopIteration", "LoadError", "ArgumentError", "Encoding", - "EncodingError", "StandardError", "Exception", "NameError"); + "EncodingError", "StandardError", "Exception", "NameError", "SyntaxError", "ScriptError"); private List loadAllow = List.of("jruby/java.rb", "jruby/java/core_ext.rb", "jruby/java/java_ext.rb", "jruby/java/core_ext/object.rb"); @@ -114,6 +115,7 @@ public void testRestrictedProfile() throws Exception { assertThrows(NameError.class, () -> runtime.evalScriptlet("UDPSocket.new")); assertEquals("cute_cats",((RubyString)runtime.evalScriptlet("\"cute_cats\"")).getValue()); assertEquals("cute_cat",((RubyString)runtime.evalScriptlet("\"cute_cats\".delete('s')")).getValue()); + assertThrows(SyntaxError.class, () -> runtime.evalScriptlet("puts 'you shouldn't see this'")); //assertThrows(NameError.class, () -> runtime.evalScriptlet("IO.sysopen('test.tmp')")); }