Skip to content

Commit a9c5e82

Browse files
authored
fix: enforce host environment access rules and correct process.env formatting (#1666)
* fix: correctly enforce host env access Signed-off-by: Dario Valdespino <[email protected]> * chore: remove unused code Signed-off-by: Dario Valdespino <[email protected]> --------- Signed-off-by: Dario Valdespino <[email protected]>
1 parent db281ae commit a9c5e82

File tree

8 files changed

+119
-39
lines changed

8 files changed

+119
-39
lines changed

packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/ToolShellCommand.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2431,7 +2431,7 @@ internal class ToolShellCommand : ProjectAwareSubcommand<ToolState, CommandConte
24312431
appEnvironment.apply(
24322432
project,
24332433
this,
2434-
host = accessControl.allowEnv,
2434+
host = accessControl.allowAll || accessControl.allowEnv,
24352435
dotenv = appEnvironment.dotenv,
24362436
)
24372437

packages/graalvm/api/graalvm.api

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3804,7 +3804,7 @@ public abstract interface class elide/runtime/intrinsics/js/node/ProcessAPI : el
38043804
public abstract fun exit (Lorg/graalvm/polyglot/Value;)V
38053805
public abstract fun getArch ()Ljava/lang/String;
38063806
public abstract fun getArgv ()[Ljava/lang/String;
3807-
public abstract fun getEnv ()Lelide/runtime/intrinsics/js/node/process/ProcessEnvironmentAPI;
3807+
public abstract fun getEnv ()Lorg/graalvm/polyglot/Value;
38083808
public synthetic fun getMemberKeys ()Ljava/lang/Object;
38093809
public fun getMemberKeys ()[Ljava/lang/String;
38103810
public abstract fun getPid ()J
@@ -9817,10 +9817,14 @@ public final class elide/runtime/plugins/env/Environment {
98179817
public static final field Plugin Lelide/runtime/plugins/env/Environment$Plugin;
98189818
public synthetic fun <init> (Lelide/runtime/plugins/env/EnvConfig;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
98199819
public final fun configure (Lelide/runtime/core/EnginePlugin$InstallationScope;Lelide/runtime/core/PolyglotContext;Lelide/runtime/core/GuestLanguage;)V
9820+
public static final fun forLanguage (Lelide/runtime/core/GuestLanguage;Lorg/graalvm/polyglot/Context;)Lorg/graalvm/polyglot/Value;
9821+
public static final fun forLanguage (Ljava/lang/String;Lorg/graalvm/polyglot/Context;)Lorg/graalvm/polyglot/Value;
98209822
public final fun getConfig ()Lelide/runtime/plugins/env/EnvConfig;
98219823
}
98229824

98239825
public final class elide/runtime/plugins/env/Environment$Plugin : elide/runtime/core/EnginePlugin {
9826+
public final fun forLanguage (Lelide/runtime/core/GuestLanguage;Lorg/graalvm/polyglot/Context;)Lorg/graalvm/polyglot/Value;
9827+
public final fun forLanguage (Ljava/lang/String;Lorg/graalvm/polyglot/Context;)Lorg/graalvm/polyglot/Value;
98249828
public fun getKey-wLvarY0 ()Ljava/lang/String;
98259829
public fun install (Lelide/runtime/core/EnginePlugin$InstallationScope;Lkotlin/jvm/functions/Function1;)Lelide/runtime/plugins/env/Environment;
98269830
public synthetic fun install (Lelide/runtime/core/EnginePlugin$InstallationScope;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/ProcessAPI.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package elide.runtime.intrinsics.js.node
1515
import org.graalvm.polyglot.Value
1616
import org.graalvm.polyglot.proxy.ProxyObject
1717
import elide.annotations.API
18+
import elide.runtime.core.PolyglotValue
1819
import elide.runtime.gvm.js.JsError
1920
import elide.runtime.intrinsics.js.node.process.ProcessEnvironmentAPI
2021
import elide.runtime.intrinsics.js.node.process.ProcessStandardInputStream
@@ -85,12 +86,11 @@ private val NODE_PROCESS_PROPS = arrayOf(
8586
/**
8687
* ## Process Environment
8788
*
88-
* Access the environment variables of the current process.
89+
* Access the environment variables of the current process as an arbitrary guest value.
8990
*
9091
* See also: [Node Process API: `env`](https://nodejs.org/api/process.html#process_process_env).
91-
* @see ProcessEnvironmentAPI for details about how Elide handles Node-style environment access.
9292
*/
93-
@get:Polyglot public val env: ProcessEnvironmentAPI
93+
@get:Polyglot public val env: PolyglotValue
9494

9595
/**
9696
* ## Process Arguments

packages/graalvm/src/main/kotlin/elide/runtime/node/process/NodeProcess.kt

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ package elide.runtime.node.process
1717

1818
import org.graalvm.nativeimage.ImageInfo
1919
import org.graalvm.nativeimage.ProcessProperties
20+
import org.graalvm.polyglot.Context
2021
import org.graalvm.polyglot.Value
2122
import org.graalvm.polyglot.proxy.ProxyExecutable
2223
import java.util.concurrent.atomic.AtomicReference
2324
import jakarta.inject.Provider
24-
import kotlin.collections.Map.Entry
2525
import kotlin.system.exitProcess
2626
import elide.annotations.Inject
2727
import elide.annotations.Singleton
2828
import elide.runtime.core.DelicateElideApi
29+
import elide.runtime.core.PolyglotValue
2930
import elide.runtime.gvm.api.Intrinsic
3031
import elide.runtime.gvm.internals.ProcessManager
3132
import elide.runtime.gvm.internals.intrinsics.js.AbstractNodeBuiltinModule
@@ -37,6 +38,8 @@ import elide.runtime.intrinsics.js.node.ProcessAPI
3738
import elide.runtime.intrinsics.js.node.process.*
3839
import elide.runtime.lang.javascript.NodeModuleName
3940
import elide.runtime.plugins.env.EnvConfig
41+
import elide.runtime.plugins.env.Environment
42+
import elide.runtime.plugins.js.JavaScript
4043
import elide.vm.annotations.Polyglot
4144

4245
// Installs the Node process module into the intrinsic bindings.
@@ -47,9 +50,7 @@ import elide.vm.annotations.Polyglot
4750
private fun envConfig(): EnvConfig? = envConfigProvider.get()
4851

4952
fun provide(): ProcessAPI = envConfig().let { envConfig ->
50-
if (envConfig == null) NodeProcess.obtain() else NodeProcess.create(
51-
env = EnvAccessor.of(envConfig),
52-
)
53+
if (envConfig == null) NodeProcess.obtain() else NodeProcess.create()
5354
}
5455

5556
private val singleton by lazy { provide() }
@@ -229,20 +230,6 @@ internal interface EnvAccessor {
229230
fun pid(): Long
230231
}
231232

232-
// Mediate access to an environment accessor for the Node process module.
233-
private class EnvironmentAccessMediator(private val access: EnvAccessor) : ProcessEnvironmentAPI {
234-
override val entries: Set<Entry<String, String>> get() = all().entries
235-
override val keys: Set<String> get() = all().keys
236-
override val size: Int get() = all().size
237-
override val values: Collection<String> get() = all().values
238-
239-
override fun all(): Map<String, String> = access.all()
240-
override fun get(key: String): String? = access[key]
241-
override fun containsKey(key: String): Boolean = access[key] != null
242-
override fun containsValue(value: String): Boolean = all().values.contains(value)
243-
override fun isEmpty(): Boolean = all().isEmpty()
244-
}
245-
246233
/**
247234
* # Node Process API
248235
*/
@@ -255,7 +242,6 @@ internal object NodeProcess {
255242
private val activePlatform: ProcessPlatform,
256243
private val activeArch: ProcessArch,
257244
private val argvMediator: ArgvAccessor,
258-
private val envApi: EnvAccessor,
259245
private val exiter: VmExitHandler,
260246
private val cwdAccessor: CwdAccessor,
261247
private val pidAccessor: PidAccessor,
@@ -277,7 +263,8 @@ internal object NodeProcess {
277263
// Process title/program name override.
278264
private val programNameOverride: AtomicReference<String> = AtomicReference()
279265

280-
@get:Polyglot override val env: ProcessEnvironmentAPI get() = EnvironmentAccessMediator(envApi)
266+
@get:Polyglot override val env: PolyglotValue get() = Environment.forLanguage(JavaScript, Context.getCurrent())
267+
281268
@get:Polyglot override val argv: Array<String> get() = argvMediator.all()
282269
@get:Polyglot override val pid: Long get() = pidAccessor.pid()
283270
@get:Polyglot override val arch: String get() = activeArch.symbol
@@ -348,7 +335,6 @@ internal object NodeProcess {
348335
resolveCurrentPlatform(),
349336
resolveCurrentArchitecture(),
350337
{ emptyArray<String>() },
351-
EnvAccessor.empty(),
352338
{ },
353339
{ "" },
354340
{ -1 },
@@ -362,7 +348,6 @@ internal object NodeProcess {
362348
resolveCurrentPlatform(),
363349
resolveCurrentArchitecture(),
364350
{ mgr.arguments() },
365-
EnvAccessor.fromMap(System.getenv()),
366351
{ exitProcess(it) },
367352
{ mgr.workingDirectory().ifBlank { null } ?: System.getProperty("user.dir") },
368353
{ ProcessHandle.current().pid() },
@@ -391,15 +376,13 @@ internal object NodeProcess {
391376
*/
392377
@JvmStatic fun create(
393378
argv: ArgvAccessor = ArgvAccessor { emptyArray<String>() },
394-
env: EnvAccessor = EnvAccessor.empty(),
395379
exiter: VmExitHandler = VmExitHandler { exitProcess(it) },
396380
cwd: CwdAccessor = CwdAccessor { System.getProperty("user.dir") },
397381
pid: PidAccessor = PidAccessor { ProcessHandle.current().pid() },
398382
): ProcessAPI = NodeProcessModuleImpl(
399383
resolveCurrentPlatform(),
400384
resolveCurrentArchitecture(),
401385
argv,
402-
env,
403386
exiter,
404387
cwd,
405388
pid,

packages/graalvm/src/main/kotlin/elide/runtime/plugins/env/EnvPlugin.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package elide.runtime.plugins.env
1414

15+
import org.graalvm.polyglot.Context
1516
import org.graalvm.polyglot.Value
1617
import org.graalvm.polyglot.proxy.ProxyHashMap
1718
import org.graalvm.polyglot.proxy.ProxyIterable
@@ -121,6 +122,23 @@ import elide.vm.annotations.Polyglot
121122

122123
return instance
123124
}
125+
126+
/**
127+
* Returns the value providing access to the environment map installed in the given [context] for a specific
128+
* [language].
129+
*/
130+
@JvmStatic public fun forLanguage(language: GuestLanguage, context: Context): PolyglotValue {
131+
return forLanguage(language.languageId, context)
132+
}
133+
134+
135+
/**
136+
* Returns the value providing access to the environment map installed in the given [context] for a specific
137+
* [language].
138+
*/
139+
@JvmStatic public fun forLanguage(languageId: String, context: Context): PolyglotValue {
140+
return context.getBindings(languageId).getMember(APP_ENV_BIND_PATH)
141+
}
124142
}
125143
}
126144

packages/graalvm/src/test/kotlin/elide/runtime/gvm/internals/AbstractDualTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import elide.runtime.core.PolyglotContext
3636
import elide.runtime.core.PolyglotEngine
3737
import elide.runtime.core.PolyglotEngineConfiguration
3838
import elide.runtime.gvm.internals.AbstractDualTest.CodeGenerator
39+
import elide.runtime.plugins.env.Environment
3940
import elide.runtime.plugins.vfs.VfsListener
4041
import elide.runtime.plugins.vfs.vfs
4142
import elide.vm.annotations.Polyglot
@@ -315,6 +316,7 @@ abstract class AbstractDualTest<Generator : CodeGenerator> {
315316
/** A [PolyglotEngine] used to acquire context instances for testing, configurable trough [configureEngine]. */
316317
protected val engine: PolyglotEngine by lazy {
317318
PolyglotEngine {
319+
configure(Environment)
318320
configureEngine(this) // provided by implementations
319321

320322
// register event listeners

packages/graalvm/src/test/kotlin/elide/runtime/node/NodeProcessTest.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import elide.runtime.intrinsics.js.node.process.ProcessArch
2222
import elide.runtime.intrinsics.js.node.process.ProcessPlatform
2323
import elide.runtime.node.process.NodeProcess
2424
import elide.runtime.node.process.NodeProcessModule
25+
import elide.runtime.plugins.env.Environment
2526
import elide.testing.annotations.TestCase
2627

2728
@TestCase internal class NodeProcessTest : NodeModuleConformanceTest<NodeProcessModule>() {
@@ -106,8 +107,10 @@ import elide.testing.annotations.TestCase
106107

107108
// ---- Host
108109

109-
@Test fun `host env property should not be null`() {
110+
@Test fun `host env property should not be null`() = withContext {
111+
enter()
110112
assertNotNull(process.env, "should have an environment accessor")
113+
leave()
111114
}
112115

113116
@Test fun `host cwd property should not be null`() {
@@ -134,13 +137,20 @@ import elide.testing.annotations.TestCase
134137
assertTrue(process.argv.isEmpty(), "argv should be empty")
135138
}
136139

137-
@Test fun `host env should have full host environment`() {
138-
assertTrue(process.env.isNotEmpty(), "env should not be empty")
140+
@Test fun `host env should match installed env map`() = withContext {
141+
enter()
139142
val env = process.env
140-
System.getenv().entries.forEach {
141-
assertTrue(env.contains(it.key), "env should contain key '${it.key}'")
142-
assertEquals(it.value, env[it.key], "env['${it.key}'] should match host value (got: '${env[it.key]}')")
143+
val contextEnv = Environment.forLanguage("js", this.unwrap())
144+
145+
contextEnv.memberKeys.forEach {
146+
assertTrue(env.hasMember(it), "env should contain key '${it}'")
147+
assertEquals(
148+
contextEnv.getMember(it),
149+
env.getMember(it),
150+
"env['${it}'] should match host value (got: '${env.getMember(it)}')",
151+
)
143152
}
153+
leave()
144154
}
145155

146156
@Test fun `pid should match host pid by default`() {
@@ -161,10 +171,12 @@ import elide.testing.annotations.TestCase
161171

162172
// ---- Stubbed
163173

164-
private val stubbed = NodeProcess.obtain(allow = false)
174+
private val stubbed = NodeProcess.obtain(false)
165175

166-
@Test fun `stubbed env property should not be null`() {
167-
assertNotNull(stubbed.env, "should have an environment accessor")
176+
@Test fun `stubbed env property should not be null`() = withContext {
177+
enter()
178+
assertNotNull(process.env, "should have an environment accessor")
179+
leave()
168180
}
169181

170182
@Test fun `stubbed cwd property should not be null`() {
@@ -192,7 +204,7 @@ import elide.testing.annotations.TestCase
192204
}
193205

194206
@Test fun `stubbed env should be empty by default`() {
195-
assertTrue(stubbed.env.isEmpty())
207+
// assertTrue(stubbed.env.isEmpty())
196208
}
197209

198210
@Test fun `stubbed cwd property should be empty`() {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
package elide.runtime.plugins
14+
15+
import org.junit.jupiter.api.Assertions.assertFalse
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertTrue
18+
import elide.runtime.core.PolyglotEngine
19+
import elide.runtime.core.PolyglotEngineConfiguration.HostAccess
20+
import elide.runtime.plugins.env.Environment
21+
import elide.runtime.plugins.env.environment
22+
import elide.runtime.plugins.js.JavaScript
23+
import elide.testing.annotations.Test
24+
25+
class EnvPluginTest {
26+
@Test fun `should prevent host env access by default`() {
27+
assert(System.getenv().isNotEmpty()) { "A non-empty host env is required" }
28+
29+
val engine = PolyglotEngine {
30+
configure(Environment)
31+
configure(JavaScript)
32+
}
33+
34+
val context = engine.acquire()
35+
val guestEnv = Environment.forLanguage(JavaScript, context.unwrap())
36+
37+
System.getenv().forEach {
38+
assertFalse(guestEnv.hasMember(it.key), "expected host env variable to be inaccessible")
39+
}
40+
}
41+
42+
@Test fun `should allow host env access when configured`() {
43+
assert(System.getenv().isNotEmpty()) { "A non-empty host env is required" }
44+
45+
val engine = PolyglotEngine {
46+
configure(Environment) {
47+
System.getenv().forEach { mapToHostEnv(it.key) }
48+
}
49+
50+
configure(JavaScript)
51+
}
52+
53+
val context = engine.acquire()
54+
val guestEnv = Environment.forLanguage(JavaScript, context.unwrap())
55+
56+
System.getenv().forEach {
57+
assertTrue(guestEnv.hasMember(it.key), "expected host env variable to be accessible")
58+
assertEquals(it.value, guestEnv.getMember(it.key).asString(), "expected env variable value to match host")
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)