Speedofmastery commited on
Commit
b299500
·
1 Parent(s): b851ab5

LANDRUN INTEGRATION - Kernel-level Linux sandbox with Landlock security

Browse files
app.py CHANGED
@@ -1,18 +1,20 @@
1
  """
2
- FastAPI Universal Code Execution Sandbox for Hugging Face Spaces
3
- Supports: HTML, React, Python, Node.js, Java, Ruby, PHP, Go, Rust, C++, C#, Swift, Kotlin, and more!
4
  """
5
 
6
  from fastapi import FastAPI, Request
7
- from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
8
  from fastapi.middleware.cors import CORSMiddleware
 
 
9
  import os
10
- import asyncio
11
- import shutil
12
 
13
  app = FastAPI()
14
 
15
- # Enable CORS for iframe embedding
16
  app.add_middleware(
17
  CORSMiddleware,
18
  allow_origins=["*"],
@@ -21,951 +23,417 @@ app.add_middleware(
21
  allow_headers=["*"],
22
  )
23
 
24
- # Language configurations
25
- LANGUAGE_CONFIG = {
26
- "python": {
27
- "extension": ".py",
28
- "command": lambda file: ["python3", file],
29
- "runtime_check": "python3"
30
- },
31
- "node": {
32
- "extension": ".mjs",
33
- "command": lambda file: ["node", file],
34
- "runtime_check": "node"
35
- },
36
- "java": {
37
- "extension": ".java",
38
- "compile": lambda file: ["javac", file],
39
- "command": lambda: ["java", "-cp", "/tmp", "Main"],
40
- "runtime_check": "javac",
41
- "needs_compile": True
42
- },
43
- "ruby": {
44
- "extension": ".rb",
45
- "command": lambda file: ["ruby", file],
46
- "runtime_check": "ruby"
47
- },
48
- "php": {
49
- "extension": ".php",
50
- "command": lambda file: ["php", file],
51
- "runtime_check": "php"
52
- },
53
- "go": {
54
- "extension": ".go",
55
- "command": lambda file: ["go", "run", file],
56
- "runtime_check": "go"
57
- },
58
- "rust": {
59
- "extension": ".rs",
60
- "compile": lambda file: ["rustc", file, "-o", "/tmp/rust_output"],
61
- "command": lambda: ["/tmp/rust_output"],
62
- "runtime_check": "rustc",
63
- "needs_compile": True
64
- },
65
- "cpp": {
66
- "extension": ".cpp",
67
- "compile": lambda file: ["g++", file, "-o", "/tmp/cpp_output"],
68
- "command": lambda: ["/tmp/cpp_output"],
69
- "runtime_check": "g++",
70
- "needs_compile": True
71
- },
72
- "c": {
73
- "extension": ".c",
74
- "compile": lambda file: ["gcc", file, "-o", "/tmp/c_output"],
75
- "command": lambda: ["/tmp/c_output"],
76
- "runtime_check": "gcc",
77
- "needs_compile": True
78
- },
79
- "csharp": {
80
- "extension": ".cs",
81
- "compile": lambda file: ["mcs", file, "-out:/tmp/csharp_output.exe"],
82
- "command": lambda: ["mono", "/tmp/csharp_output.exe"],
83
- "runtime_check": "mcs",
84
- "needs_compile": True
85
- },
86
- "swift": {
87
- "extension": ".swift",
88
- "command": lambda file: ["swift", file],
89
- "runtime_check": "swift"
90
- },
91
- "kotlin": {
92
- "extension": ".kt",
93
- "compile": lambda file: ["kotlinc", file, "-include-runtime", "-d", "/tmp/kotlin_output.jar"],
94
- "command": lambda: ["java", "-jar", "/tmp/kotlin_output.jar"],
95
- "runtime_check": "kotlinc",
96
- "needs_compile": True
97
- },
98
- "perl": {
99
- "extension": ".pl",
100
- "command": lambda file: ["perl", file],
101
- "runtime_check": "perl"
102
- },
103
- "lua": {
104
- "extension": ".lua",
105
- "command": lambda file: ["lua", file],
106
- "runtime_check": "lua"
107
- },
108
- "bash": {
109
- "extension": ".sh",
110
- "command": lambda file: ["bash", file],
111
- "runtime_check": "bash"
112
- },
113
- "r": {
114
- "extension": ".r",
115
- "command": lambda file: ["Rscript", file],
116
- "runtime_check": "Rscript"
117
- },
118
- "scala": {
119
- "extension": ".scala",
120
- "command": lambda file: ["scala", file],
121
- "runtime_check": "scala"
122
  }
123
- }
124
-
125
-
126
- @app.get("/health")
127
- async def health():
128
- """Health check for pre-warming the Space"""
129
- return {"status": "ok"}
130
-
131
-
132
- @app.get("/capabilities")
133
- async def capabilities():
134
- """Check which language runtimes are available"""
135
- caps = {}
136
- for lang, config in LANGUAGE_CONFIG.items():
137
- runtime = config.get("runtime_check")
138
- caps[lang] = shutil.which(runtime) is not None
139
- return caps
140
-
141
-
142
- @app.get("/languages")
143
- async def list_languages():
144
- """List all supported languages with availability"""
145
- result = []
146
- for lang, config in LANGUAGE_CONFIG.items():
147
- runtime = config.get("runtime_check")
148
- available = shutil.which(runtime) is not None
149
- result.append({
150
- "language": lang,
151
- "available": available,
152
- "extension": config["extension"],
153
- "needs_compile": config.get("needs_compile", False)
154
- })
155
- return {"languages": result}
156
-
157
-
158
- async def stream_execution(language: str, code: str):
159
- """Execute code for any supported language and stream output"""
160
 
161
- config = LANGUAGE_CONFIG.get(language)
162
  if not config:
163
- yield f"Error: Unsupported language '{language}'\n"
164
- yield "done\n"
165
- return
166
 
167
- # Check runtime availability
168
- runtime = config.get("runtime_check")
169
- if not shutil.which(runtime):
170
- yield f"Error: {language} runtime not available\n"
171
- yield "done\n"
172
- return
173
-
174
- # Determine filename
175
- if language == "java":
176
- code_path = "/tmp/Main.java"
177
- else:
178
- code_path = f"/tmp/code{config['extension']}"
179
-
180
- # Write code to file
181
  try:
182
- with open(code_path, "w") as f:
183
  f.write(code)
184
- except Exception as e:
185
- yield f"Error writing code: {e}\n"
186
- yield "done\n"
187
- return
188
-
189
- try:
190
- # Compile if needed
191
- if config.get("needs_compile"):
192
- compile_cmd = config["compile"](code_path)
193
-
194
- compile_process = await asyncio.create_subprocess_exec(
195
- *compile_cmd,
196
- stdout=asyncio.subprocess.PIPE,
197
- stderr=asyncio.subprocess.STDOUT
198
- )
199
-
200
- # Stream compilation output
201
- while True:
202
- line = await compile_process.stdout.readline()
203
- if not line:
204
- break
205
- yield line.decode('utf-8', errors='replace')
206
-
207
- exit_code = await compile_process.wait()
208
-
209
- if exit_code != 0:
210
- yield f"Compilation failed with exit code {exit_code}\n"
211
- yield "done\n"
212
- return
213
-
214
- # Execute
215
- if config.get("needs_compile") and callable(config["command"]):
216
- run_cmd = config["command"]()
217
- else:
218
- run_cmd = config["command"](code_path)
219
 
220
- run_process = await asyncio.create_subprocess_exec(
221
- *run_cmd,
222
- stdout=asyncio.subprocess.PIPE,
223
- stderr=asyncio.subprocess.STDOUT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  )
225
 
226
- # Stream execution output
227
- while True:
228
- line = await run_process.stdout.readline()
229
- if not line:
230
- break
231
- yield line.decode('utf-8', errors='replace')
232
 
233
- await run_process.wait()
234
- yield "done\n"
 
 
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  except Exception as e:
237
- yield f"Execution error: {e}\n"
238
- yield "done\n"
239
-
240
  finally:
241
- # Clean up
242
- try:
243
- os.remove(code_path)
244
- # Clean up compiled outputs
245
- if language == "java":
246
- os.remove("/tmp/Main.class")
247
- elif language == "rust":
248
- os.remove("/tmp/rust_output")
249
- elif language == "cpp":
250
- os.remove("/tmp/cpp_output")
251
- elif language == "c":
252
- os.remove("/tmp/c_output")
253
- elif language == "csharp":
254
- os.remove("/tmp/csharp_output.exe")
255
- elif language == "kotlin":
256
- os.remove("/tmp/kotlin_output.jar")
257
- except Exception:
258
- pass
259
-
260
- # Schedule exit
261
- asyncio.create_task(delayed_exit())
262
-
263
-
264
- async def delayed_exit():
265
- """Exit the process after a short delay"""
266
- await asyncio.sleep(0.5)
267
- os._exit(0)
268
-
269
-
270
- @app.post("/run")
271
- async def run_code(request: Request):
272
- """Execute code in any supported language and stream output"""
273
- body = await request.json()
274
- language = body.get("language", "").lower()
275
- code = body.get("code", "")
276
-
277
- if not code:
278
- return JSONResponse({"error": "NO_CODE"}, status_code=400)
279
-
280
- if language not in LANGUAGE_CONFIG:
281
- return JSONResponse(
282
- {"error": f"UNSUPPORTED_LANGUAGE: {language}"},
283
- status_code=400
284
- )
285
-
286
- # Check if runtime is available
287
- config = LANGUAGE_CONFIG[language]
288
- runtime = config.get("runtime_check")
289
- if not shutil.which(runtime):
290
- return JSONResponse(
291
- {"error": f"{language.upper()}_NOT_AVAILABLE"},
292
- status_code=503
293
- )
294
-
295
- return StreamingResponse(
296
- stream_execution(language, code),
297
- media_type="text/plain"
298
- )
299
 
300
 
301
  @app.get("/", response_class=HTMLResponse)
302
  async def root():
303
- """Serve the HTML UI with multi-language support"""
304
- html_content = """
305
  <!DOCTYPE html>
306
  <html lang="en">
307
  <head>
308
  <meta charset="UTF-8">
309
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
310
- <title>Universal Code Sandbox</title>
311
-
312
- <!-- CDN Scripts -->
313
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
314
- <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
315
- <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
316
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
317
-
318
  <style>
319
- * {
320
- margin: 0;
321
- padding: 0;
322
- box-sizing: border-box;
323
- }
324
-
325
  body {
326
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
327
- background: #1e1e1e;
328
- color: #d4d4d4;
329
- height: 100vh;
330
- overflow: hidden;
331
- display: flex;
332
- flex-direction: column;
333
- }
334
-
335
- .app-container {
336
- display: flex;
337
- flex-direction: column;
338
- height: 100vh;
339
  padding: 20px;
340
- gap: 15px;
341
  }
342
-
343
- h1 {
344
- color: #4ec9b0;
345
- font-size: 28px;
346
- margin: 0;
 
 
347
  }
348
-
349
- .controls {
350
- display: flex;
351
- gap: 10px;
352
- align-items: center;
353
- flex-wrap: wrap;
354
- flex-shrink: 0;
355
  }
356
-
357
- label {
358
- color: #9cdcfe;
 
 
 
 
 
359
  font-weight: bold;
360
  }
361
-
362
- select, button {
363
- background: #252526;
364
- color: #d4d4d4;
365
- border: 1px solid #3c3c3c;
366
- padding: 10px 15px;
367
- border-radius: 4px;
368
- font-family: inherit;
369
- font-size: 14px;
370
- cursor: pointer;
371
- transition: all 0.2s;
372
  }
373
-
374
- select:hover {
375
- border-color: #007acc;
 
376
  }
377
-
378
- select:focus, button:focus {
379
- outline: none;
380
- border-color: #007acc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
-
383
  button {
384
- background: #007acc;
385
  color: white;
386
  border: none;
 
 
387
  font-weight: bold;
388
- padding: 10px 20px;
389
- }
390
-
391
- button:hover:not(:disabled) {
392
- background: #005a9e;
393
- transform: translateY(-1px);
394
- }
395
-
396
- button:active:not(:disabled) {
397
- transform: translateY(0);
398
  }
399
-
400
  button:disabled {
401
- background: #3c3c3c;
402
  cursor: not-allowed;
403
- opacity: 0.6;
404
  }
405
-
406
- .workspace {
407
- flex: 1;
408
- display: grid;
409
- grid-template-rows: minmax(250px, 1fr) minmax(250px, 1fr);
410
- gap: 15px;
411
- min-height: 0;
412
- overflow: hidden;
413
- }
414
-
415
- .editor-panel {
416
- display: flex;
417
- flex-direction: column;
418
- min-height: 0;
419
- }
420
-
421
- .panel-label {
422
- color: #9cdcfe;
423
- font-weight: bold;
424
- margin-bottom: 8px;
425
- font-size: 13px;
426
- text-transform: uppercase;
427
- letter-spacing: 1px;
428
- }
429
-
430
- textarea {
431
- flex: 1;
432
- background: #252526;
433
  color: #d4d4d4;
434
- border: 1px solid #3c3c3c;
435
- border-radius: 4px;
436
- padding: 15px;
437
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
438
  font-size: 14px;
439
- line-height: 1.6;
440
- resize: none;
441
- outline: none;
442
- min-height: 0;
443
- }
444
-
445
- textarea:focus {
446
- border-color: #007acc;
447
- }
448
-
449
- .output-panel {
450
- display: flex;
451
- flex-direction: column;
452
- min-height: 0;
453
- position: relative;
454
- }
455
-
456
- #output {
457
- flex: 1;
458
- background: #0c0c0c;
459
- color: #00ff00;
460
- border: 1px solid #00ff00;
461
- border-radius: 4px;
462
- padding: 15px;
463
- overflow-y: auto;
464
- font-size: 13px;
465
- line-height: 1.6;
466
  white-space: pre-wrap;
467
- word-wrap: break-word;
468
- font-family: 'Courier New', monospace;
469
- min-height: 0;
470
  }
471
-
472
- #html-preview, #react-root {
473
- flex: 1;
 
 
474
  background: white;
475
- border: 1px solid #3c3c3c;
476
- border-radius: 4px;
477
- overflow: auto;
478
- min-height: 0;
479
- }
480
-
481
- #html-preview {
482
- border: 1px solid #3c3c3c;
483
  }
484
-
485
- .error { color: #f48771; }
486
- .success { color: #4ec9b0; }
487
- .info { color: #9cdcfe; }
488
-
489
- .badge {
490
- display: inline-block;
491
- padding: 3px 8px;
492
- border-radius: 3px;
493
- font-size: 11px;
494
- margin-left: 5px;
495
  font-weight: bold;
496
  }
497
-
498
- .badge-available { background: #4ec9b0; color: #000; }
499
- .badge-unavailable { background: #f48771; color: #000; }
500
-
501
- /* Scrollbar styling */
502
- ::-webkit-scrollbar {
503
- width: 10px;
504
- height: 10px;
505
  }
506
-
507
- ::-webkit-scrollbar-track {
508
- background: #1e1e1e;
 
509
  }
510
-
511
- ::-webkit-scrollbar-thumb {
512
- background: #3c3c3c;
513
- border-radius: 5px;
 
514
  }
515
-
516
- ::-webkit-scrollbar-thumb:hover {
517
- background: #007acc;
 
 
 
 
 
 
518
  }
519
-
520
- @media (max-width: 768px) {
521
- .workspace {
522
- grid-template-rows: 300px 1fr;
523
- }
524
-
525
- h1 {
526
- font-size: 20px;
527
- }
528
  }
529
  </style>
530
  </head>
531
  <body>
532
- <div class="app-container">
533
- <h1>&#128640; Universal Code Sandbox</h1>
 
 
 
 
 
 
534
 
535
- <div class="controls">
536
- <label for="language">Language:</label>
537
- <select id="language" onchange="switchLanguage()">
538
- <optgroup label="Client-Side">
539
- <option value="html">HTML</option>
540
- <option value="react">React/JSX</option>
541
- </optgroup>
542
- <optgroup label="Scripting Languages">
543
  <option value="python">Python</option>
544
- <option value="node">Node.js/JavaScript</option>
545
- <option value="ruby">Ruby</option>
546
- <option value="php">PHP</option>
547
- <option value="perl">Perl</option>
548
- <option value="lua">Lua</option>
549
- <option value="bash">Bash</option>
550
- <option value="r">R</option>
551
- </optgroup>
552
- <optgroup label="Compiled Languages">
553
- <option value="java">Java</option>
554
- <option value="cpp">C++</option>
555
- <option value="c">C</option>
556
- <option value="go">Go</option>
557
- <option value="rust">Rust</option>
558
- <option value="csharp">C#</option>
559
- <option value="swift">Swift</option>
560
- <option value="kotlin">Kotlin</option>
561
- <option value="scala">Scala</option>
562
- </optgroup>
563
- </select>
564
 
565
- <button id="runBtn" onclick="runCode()">&#9654; Run</button>
566
- <button onclick="checkAvailability()">&#128269; Check Available</button>
567
- <span id="status" class="info"></span>
 
 
568
  </div>
569
 
570
- <div class="workspace">
571
- <div class="editor-panel">
572
- <div class="panel-label">&#9670; Code Editor</div>
573
- <textarea id="editor" placeholder="Enter your code here..." spellcheck="false"></textarea>
574
- </div>
575
-
576
- <div class="output-panel">
577
- <div class="panel-label">&#9670; Output / Preview</div>
578
- <pre id="output" style="display: none;"></pre>
579
- <iframe id="html-preview" style="display: none;" sandbox="allow-scripts"></iframe>
580
- <div id="react-root" style="display: none;"></div>
581
  </div>
582
  </div>
583
  </div>
584
 
585
  <script>
586
- const editor = document.getElementById('editor');
587
- const output = document.getElementById('output');
588
- const htmlPreview = document.getElementById('html-preview');
589
- const reactRoot = document.getElementById('react-root');
590
- const languageSelect = document.getElementById('language');
591
- const runBtn = document.getElementById('runBtn');
592
- const status = document.getElementById('status');
593
-
594
- let capabilities = {};
595
-
596
- // Fetch capabilities on load
597
- fetch('/capabilities')
598
- .then(res => res.json())
599
- .then(data => {
600
- capabilities = data;
601
- updateLanguageSelector();
602
- });
603
-
604
- function updateLanguageSelector() {
605
- const options = languageSelect.querySelectorAll('option');
606
- options.forEach(opt => {
607
- const lang = opt.value;
608
- if (lang === 'html' || lang === 'react') {
609
- return; // Client-side always available
610
- }
611
- if (capabilities[lang] !== undefined) {
612
- const badge = capabilities[lang] ?
613
- '<span class="badge badge-available">✓</span>' :
614
- '<span class="badge badge-unavailable">✗</span>';
615
- opt.innerHTML = opt.innerHTML.split('<')[0] + ' ' + (capabilities[lang] ? '✓' : '✗');
616
- }
617
- });
618
- }
619
-
620
- async function checkAvailability() {
621
- status.textContent = '🔍 Checking...';
622
- status.className = 'info';
623
-
624
- const response = await fetch('/languages');
625
- const data = await response.json();
626
-
627
- output.style.display = 'block';
628
- htmlPreview.style.display = 'none';
629
- reactRoot.style.display = 'none';
630
-
631
- output.textContent = 'Available Languages:\\n\\n';
632
- data.languages.forEach(lang => {
633
- const badge = lang.available ? '✅' : '❌';
634
- output.textContent += `${badge} ${lang.language.toUpperCase()} (${lang.extension})\\n`;
635
- });
636
-
637
- status.textContent = '✓ Check complete';
638
- status.className = 'success';
639
- }
640
-
641
  const examples = {
642
- python: `# Python example
643
- print("Hello from Python!")
644
- for i in range(5):
645
- print(f"Number: {i + 1}")
646
- print("Done!")`,
647
-
648
- node: `// Node.js example
649
- console.log('Hello from Node.js!');
650
- for (let i = 0; i < 5; i++) {
651
- console.log('Step ' + (i + 1));
652
- await new Promise(r => setTimeout(r, 200));
653
- }
654
- console.log('Done!');`,
655
-
656
- java: `public class Main {
657
- public static void main(String[] args) {
658
- System.out.println("Hello from Java!");
659
- for (int i = 0; i < 5; i++) {
660
- System.out.println("Step " + (i + 1));
661
- }
662
- System.out.println("Done!");
663
- }
664
- }`,
665
-
666
- ruby: `# Ruby example
667
- puts "Hello from Ruby!"
668
- 5.times do |i|
669
- puts "Step #{i + 1}"
670
- end
671
- puts "Done!"`,
672
-
673
- php: `<?php
674
- echo "Hello from PHP!\\n";
675
- for ($i = 1; $i <= 5; $i++) {
676
- echo "Step $i\\n";
677
- }
678
- echo "Done!\\n";
679
- ?>`,
680
-
681
- go: `package main
682
- import "fmt"
683
-
684
- func main() {
685
- fmt.Println("Hello from Go!")
686
- for i := 1; i <= 5; i++ {
687
- fmt.Printf("Step %d\\n", i)
688
- }
689
- fmt.Println("Done!")
690
- }`,
691
-
692
- rust: `fn main() {
693
- println!("Hello from Rust!");
694
- for i in 1..=5 {
695
- println!("Step {}", i);
696
- }
697
- println!("Done!");
698
- }`,
699
-
700
- cpp: `#include <iostream>
701
- using namespace std;
702
-
703
- int main() {
704
- cout << "Hello from C++!" << endl;
705
- for (int i = 1; i <= 5; i++) {
706
- cout << "Step " << i << endl;
707
- }
708
- cout << "Done!" << endl;
709
- return 0;
710
- }`,
711
-
712
- c: `#include <stdio.h>
713
-
714
- int main() {
715
- printf("Hello from C!\\n");
716
- for (int i = 1; i <= 5; i++) {
717
- printf("Step %d\\n", i);
718
- }
719
- printf("Done!\\n");
720
- return 0;
721
- }`,
722
-
723
- csharp: `using System;
724
 
725
- class Program {
726
- static void Main() {
727
- Console.WriteLine("Hello from C#!");
728
- for (int i = 1; i <= 5; i++) {
729
- Console.WriteLine($"Step {i}");
730
  }
731
- Console.WriteLine("Done!");
732
- }
733
- }`,
734
-
735
- swift: `print("Hello from Swift!")
736
- for i in 1...5 {
737
- print("Step \\(i)")
738
- }
739
- print("Done!")`,
740
-
741
- kotlin: `fun main() {
742
- println("Hello from Kotlin!")
743
- for (i in 1..5) {
744
- println("Step $i")
745
- }
746
- println("Done!")
747
- }`,
748
-
749
- perl: `# Perl example
750
- print "Hello from Perl!\\n";
751
- for my $i (1..5) {
752
- print "Step $i\\n";
753
- }
754
- print "Done!\\n";`,
755
-
756
- lua: `-- Lua example
757
- print("Hello from Lua!")
758
- for i = 1, 5 do
759
- print("Step " .. i)
760
- end
761
- print("Done!")`,
762
-
763
- bash: `#!/bin/bash
764
- echo "Hello from Bash!"
765
- for i in {1..5}; do
766
- echo "Step $i"
767
- done
768
- echo "Done!"`,
769
-
770
- r: `# R example
771
- print("Hello from R!")
772
- for (i in 1:5) {
773
- print(paste("Step", i))
774
- }
775
- print("Done!")`,
776
-
777
- scala: `object Main extends App {
778
- println("Hello from Scala!")
779
- for (i <- 1 to 5) {
780
- println(s"Step $i")
781
- }
782
- println("Done!")
783
- }`,
784
-
785
- html: `<!DOCTYPE html>
786
- <html>
787
- <head>
788
- <style>
789
- body { font-family: Arial; padding: 20px; }
790
- h1 { color: #007acc; }
791
- </style>
792
- </head>
793
- <body>
794
- <h1>Hello from HTML!</h1>
795
- <p>This is rendered client-side.</p>
796
- <button onclick="alert('Clicked!')">Click Me</button>
797
- </body>
798
- </html>`,
799
-
800
- react: `function App() {
801
- const [count, setCount] = React.useState(0);
802
-
803
- return (
804
- <div style={{padding: '20px', fontFamily: 'Arial'}}>
805
- <h1>Hello from React!</h1>
806
- <p>Count: {count}</p>
807
- <button onClick={() => setCount(count + 1)}>
808
- Increment
809
- </button>
810
- </div>
811
- );
812
- }
813
 
814
- ReactDOM.createRoot(document.getElementById('react-root')).render(<App />);`
815
- };
816
-
817
- function switchLanguage() {
818
- const lang = languageSelect.value;
819
- editor.value = examples[lang] || '';
820
-
821
- // Hide all output areas
822
- output.style.display = 'none';
823
- htmlPreview.style.display = 'none';
824
- reactRoot.style.display = 'none';
825
-
826
- // Show appropriate output area
827
- if (lang === 'html') {
828
- htmlPreview.style.display = 'block';
829
- } else if (lang === 'react') {
830
- reactRoot.style.display = 'block';
831
- } else {
832
- output.style.display = 'block';
833
- }
834
-
835
- output.textContent = '';
836
- status.textContent = '';
837
- }
838
-
839
- async function runCode() {
840
- const lang = languageSelect.value;
841
- const code = editor.value.trim();
842
-
843
- if (!code) {
844
- status.textContent = '⚠️ No code to run';
845
- status.className = 'error';
846
- return;
847
- }
848
-
849
- output.textContent = '';
850
- status.textContent = '🚀 Running...';
851
- status.className = 'info';
852
-
853
- if (lang === 'html') {
854
- runHTML(code);
855
- } else if (lang === 'react') {
856
- runReact(code);
857
- } else {
858
- await runServerSide(lang, code);
859
- }
860
- }
861
-
862
- function runHTML(code) {
863
- try {
864
- const clean = DOMPurify.sanitize(code);
865
- htmlPreview.srcdoc = clean;
866
- status.textContent = '✓ Rendered';
867
- status.className = 'success';
868
- } catch (error) {
869
- output.style.display = 'block';
870
- htmlPreview.style.display = 'none';
871
- output.textContent = 'Error: ' + error.message;
872
- status.textContent = '❌ Error';
873
- status.className = 'error';
874
- }
875
- }
876
-
877
- function runReact(code) {
878
- try {
879
- reactRoot.innerHTML = '';
880
- const transformed = Babel.transform(code, {
881
- presets: ['react']
882
- }).code;
883
- eval(transformed);
884
- status.textContent = '✓ Rendered';
885
- status.className = 'success';
886
- } catch (error) {
887
- reactRoot.innerHTML = '<div style="padding: 20px; color: red; font-family: monospace;">' +
888
- '<strong>Error:</strong><br>' + error.message + '</div>';
889
- status.textContent = '❌ Error';
890
- status.className = 'error';
891
- }
892
- }
893
-
894
- async function runServerSide(lang, code) {
895
  runBtn.disabled = true;
896
- editor.disabled = true;
897
- output.textContent = '';
898
-
 
899
  try {
900
- const response = await fetch('/run', {
901
  method: 'POST',
902
- headers: { 'Content-Type': 'application/json' },
903
- body: JSON.stringify({ language: lang, code: code })
904
  });
905
-
906
- if (!response.ok) {
907
- const error = await response.json();
908
- output.textContent = 'Error: ' + (error.error || 'Unknown error');
909
- status.textContent = '❌ Error';
910
- status.className = 'error';
911
- runBtn.disabled = false;
912
- editor.disabled = false;
913
- return;
914
- }
915
-
916
- const reader = response.body.getReader();
917
- const decoder = new TextDecoder();
918
-
919
- while (true) {
920
- const { value, done } = await reader.read();
921
- if (done) break;
922
-
923
- const chunk = decoder.decode(value, { stream: true });
924
- output.textContent += chunk;
925
- output.scrollTop = output.scrollHeight;
926
 
927
- if (chunk.includes('done')) {
928
- status.textContent = '✓ Complete (Space restarting...)';
929
- status.className = 'success';
930
  }
931
  }
932
-
933
  } catch (error) {
934
- output.textContent += '\\n\\nConnection error: ' + error.message;
935
- status.textContent = '❌ Error';
936
- status.className = 'error';
937
  } finally {
938
  runBtn.disabled = false;
939
- editor.disabled = false;
940
  }
941
  }
942
-
943
- // Tab key support
944
- editor.addEventListener('keydown', (e) => {
945
- if (e.key === 'Tab') {
946
- e.preventDefault();
947
- const start = editor.selectionStart;
948
- const end = editor.selectionEnd;
949
- editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
950
- editor.selectionStart = editor.selectionEnd = start + 4;
951
- }
952
- });
953
-
954
- // Ctrl+Enter to run
955
- editor.addEventListener('keydown', (e) => {
956
- if (e.ctrlKey && e.key === 'Enter') {
957
- e.preventDefault();
958
- runCode();
959
- }
960
  });
961
-
962
- // Initialize
963
- switchLanguage();
964
  </script>
965
  </body>
966
  </html>
967
  """
968
- return HTMLResponse(content=html_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
 
971
  if __name__ == "__main__":
 
1
  """
2
+ FastAPI Universal Code Execution Sandbox with LANDRUN Security
3
+ Kernel-level sandboxing using Linux Landlock for maximum isolation
4
  """
5
 
6
  from fastapi import FastAPI, Request
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
  from fastapi.middleware.cors import CORSMiddleware
9
+ import subprocess
10
+ import tempfile
11
  import os
12
+ import base64
13
+ import shlex
14
 
15
  app = FastAPI()
16
 
17
+ # Enable CORS
18
  app.add_middleware(
19
  CORSMiddleware,
20
  allow_origins=["*"],
 
23
  allow_headers=["*"],
24
  )
25
 
26
+ def execute_with_landrun(language: str, code: str) -> dict:
27
+ """Execute code using landrun kernel-level sandboxing"""
28
+
29
+ # Language configurations
30
+ configs = {
31
+ "python": {
32
+ "ext": ".py",
33
+ "cmd": ["python3"],
34
+ "allowed_paths": ["/usr/lib/python3*", "/usr/local/lib/python3*"],
35
+ },
36
+ "javascript": {
37
+ "ext": ".js",
38
+ "cmd": ["node"],
39
+ "allowed_paths": ["/usr/lib/node_modules", "/usr/local/lib/node_modules"],
40
+ },
41
+ "html": {
42
+ "ext": ".html",
43
+ "cmd": None, # Static file
44
+ "allowed_paths": [],
45
+ },
46
+ "react": {
47
+ "ext": ".jsx",
48
+ "cmd": ["node"],
49
+ "allowed_paths": ["/usr/lib/node_modules", "/usr/local/lib/node_modules"],
50
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ config = configs.get(language.lower())
54
  if not config:
55
+ return {"error": f"Unsupported language: {language}"}
 
 
56
 
57
+ # Create temporary file
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
+ with tempfile.NamedTemporaryFile(mode='w', suffix=config['ext'], delete=False, dir='/tmp/sandbox') as f:
60
  f.write(code)
61
+ temp_file = f.name
62
+
63
+ # For HTML/static files, return directly
64
+ if language.lower() == "html":
65
+ with open(temp_file, 'r') as f:
66
+ html_content = f.read()
67
+ os.unlink(temp_file)
68
+ return {
69
+ "output": "HTML rendered successfully",
70
+ "preview": base64.b64encode(html_content.encode()).decode()
71
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ # Build landrun command with security restrictions
74
+ landrun_cmd = [
75
+ "/usr/local/bin/landrun",
76
+ "--ldd", # Auto-detect library dependencies
77
+ "--add-exec", # Auto-add executable
78
+ "--ro", "/usr", # Read-only access to system files
79
+ "--ro", "/lib", # Read-only access to libraries
80
+ "--ro", "/lib64", # Read-only 64-bit libraries
81
+ "--ro", "/etc", # Read-only config (for DNS, etc.)
82
+ "--rw", "/tmp/sandbox", # Write access to sandbox only
83
+ "--ro", temp_file, # Read-only access to code file
84
+ "--connect-tcp", "80,443", # Allow HTTP/HTTPS
85
+ "--log-level", "error",
86
+ ]
87
+
88
+ # Add language-specific paths
89
+ for path in config['allowed_paths']:
90
+ landrun_cmd.extend(["--ro", path])
91
+
92
+ # Add execution command
93
+ landrun_cmd.extend(config['cmd'] + [temp_file])
94
+
95
+ # Execute with timeout
96
+ result = subprocess.run(
97
+ landrun_cmd,
98
+ capture_output=True,
99
+ text=True,
100
+ timeout=10,
101
+ cwd="/tmp/sandbox"
102
  )
103
 
104
+ # Clean up
105
+ os.unlink(temp_file)
 
 
 
 
106
 
107
+ # Prepare output
108
+ output = result.stdout
109
+ if result.stderr:
110
+ output += f"\n--- STDERR ---\n{result.stderr}"
111
 
112
+ # For React/JS with output, create preview
113
+ preview = None
114
+ if language.lower() in ["react", "javascript"] and "<" in code:
115
+ preview = base64.b64encode(code.encode()).decode()
116
+
117
+ return {
118
+ "output": output or "Execution completed successfully",
119
+ "exit_code": result.returncode,
120
+ "preview": preview,
121
+ "security": "🔒 Landrun kernel-level isolation active"
122
+ }
123
+
124
+ except subprocess.TimeoutExpired:
125
+ return {"error": "⏱️ Execution timeout (10s limit)"}
126
  except Exception as e:
127
+ return {"error": f"Execution error: {str(e)}"}
 
 
128
  finally:
129
+ # Cleanup temp file if exists
130
+ if 'temp_file' in locals() and os.path.exists(temp_file):
131
+ try:
132
+ os.unlink(temp_file)
133
+ except:
134
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
 
137
  @app.get("/", response_class=HTMLResponse)
138
  async def root():
139
+ """Serve the main UI"""
140
+ return """
141
  <!DOCTYPE html>
142
  <html lang="en">
143
  <head>
144
  <meta charset="UTF-8">
145
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
146
+ <title>🔒 Landrun Sandbox - Kernel-Level Security</title>
 
 
 
 
 
 
 
147
  <style>
148
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
 
149
  body {
150
+ font-family: 'Segoe UI', system-ui, sans-serif;
151
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
152
+ min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
153
  padding: 20px;
 
154
  }
155
+ .container {
156
+ max-width: 1400px;
157
+ margin: 0 auto;
158
+ background: white;
159
+ border-radius: 20px;
160
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
161
+ overflow: hidden;
162
  }
163
+ .header {
164
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
165
+ color: white;
166
+ padding: 30px;
167
+ text-align: center;
 
 
168
  }
169
+ .header h1 { font-size: 2.5em; margin-bottom: 10px; }
170
+ .header p { opacity: 0.9; font-size: 1.1em; }
171
+ .security-badge {
172
+ display: inline-block;
173
+ background: rgba(255,255,255,0.2);
174
+ padding: 8px 16px;
175
+ border-radius: 20px;
176
+ margin-top: 10px;
177
  font-weight: bold;
178
  }
179
+ .content {
180
+ display: grid;
181
+ grid-template-columns: 1fr 1fr;
182
+ gap: 20px;
183
+ padding: 30px;
 
 
 
 
 
 
184
  }
185
+ .panel {
186
+ background: #f8f9fa;
187
+ border-radius: 12px;
188
+ padding: 20px;
189
  }
190
+ .panel h2 {
191
+ color: #667eea;
192
+ margin-bottom: 15px;
193
+ font-size: 1.3em;
194
+ }
195
+ textarea {
196
+ width: 100%;
197
+ height: 300px;
198
+ font-family: 'Monaco', 'Courier New', monospace;
199
+ font-size: 14px;
200
+ padding: 15px;
201
+ border: 2px solid #ddd;
202
+ border-radius: 8px;
203
+ resize: vertical;
204
+ background: white;
205
+ }
206
+ select {
207
+ width: 100%;
208
+ padding: 12px;
209
+ margin-bottom: 15px;
210
+ border: 2px solid #ddd;
211
+ border-radius: 8px;
212
+ font-size: 16px;
213
+ background: white;
214
  }
 
215
  button {
216
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
217
  color: white;
218
  border: none;
219
+ padding: 15px 30px;
220
+ font-size: 16px;
221
  font-weight: bold;
222
+ border-radius: 8px;
223
+ cursor: pointer;
224
+ width: 100%;
225
+ margin-top: 10px;
226
+ transition: transform 0.2s;
 
 
 
 
 
227
  }
228
+ button:hover { transform: scale(1.05); }
229
  button:disabled {
230
+ background: #ccc;
231
  cursor: not-allowed;
232
+ transform: none;
233
  }
234
+ .output {
235
+ background: #1e1e1e;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  color: #d4d4d4;
237
+ padding: 20px;
238
+ border-radius: 8px;
239
+ font-family: 'Monaco', 'Courier New', monospace;
 
240
  font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  white-space: pre-wrap;
242
+ min-height: 300px;
243
+ max-height: 500px;
244
+ overflow-y: auto;
245
  }
246
+ .preview {
247
+ width: 100%;
248
+ height: 400px;
249
+ border: 2px solid #ddd;
250
+ border-radius: 8px;
251
  background: white;
 
 
 
 
 
 
 
 
252
  }
253
+ .status {
254
+ padding: 10px;
255
+ border-radius: 8px;
256
+ margin-bottom: 15px;
 
 
 
 
 
 
 
257
  font-weight: bold;
258
  }
259
+ .status.success {
260
+ background: #d4edda;
261
+ color: #155724;
262
+ border: 1px solid #c3e6cb;
 
 
 
 
263
  }
264
+ .status.error {
265
+ background: #f8d7da;
266
+ color: #721c24;
267
+ border: 1px solid #f5c6cb;
268
  }
269
+ .examples {
270
+ display: grid;
271
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
272
+ gap: 10px;
273
+ margin-bottom: 15px;
274
  }
275
+ .example-btn {
276
+ padding: 10px;
277
+ background: white;
278
+ border: 2px solid #667eea;
279
+ color: #667eea;
280
+ border-radius: 8px;
281
+ cursor: pointer;
282
+ font-size: 14px;
283
+ transition: all 0.2s;
284
  }
285
+ .example-btn:hover {
286
+ background: #667eea;
287
+ color: white;
 
 
 
 
 
 
288
  }
289
  </style>
290
  </head>
291
  <body>
292
+ <div class="container">
293
+ <div class="header">
294
+ <h1>🔒 Landrun Sandbox</h1>
295
+ <p>Kernel-Level Security with Linux Landlock</p>
296
+ <div class="security-badge">
297
+ 🛡️ Maximum Isolation • Zero Trust • Kernel Enforced
298
+ </div>
299
+ </div>
300
 
301
+ <div class="content">
302
+ <div class="panel">
303
+ <h2>📝 Code Editor</h2>
304
+ <select id="language">
 
 
 
 
305
  <option value="python">Python</option>
306
+ <option value="javascript">JavaScript (Node.js)</option>
307
+ <option value="react">React (JSX)</option>
308
+ <option value="html">HTML</option>
309
+ </select>
310
+
311
+ <div class="examples">
312
+ <button class="example-btn" onclick="loadExample('hello')">Hello World</button>
313
+ <button class="example-btn" onclick="loadExample('math')">Math Demo</button>
314
+ <button class="example-btn" onclick="loadExample('html')">HTML Page</button>
315
+ <button class="example-btn" onclick="loadExample('react')">React App</button>
316
+ </div>
317
+
318
+ <textarea id="code" placeholder="Write your code here...">print("Hello from Landrun Sandbox!")
319
+ print("🔒 Running with kernel-level security!")
320
+ import sys
321
+ print(f"Python version: {sys.version}")</textarea>
322
+
323
+ <button id="runBtn" onclick="executeCode()">▶️ Run Code (Landrun Secured)</button>
324
+ </div>
 
325
 
326
+ <div class="panel">
327
+ <h2>📺 Output</h2>
328
+ <div id="status"></div>
329
+ <div id="output" class="output">Ready to execute code...</div>
330
+ </div>
331
  </div>
332
 
333
+ <div style="padding: 0 30px 30px 30px;">
334
+ <div class="panel">
335
+ <h2>🖼️ Preview</h2>
336
+ <iframe id="preview" class="preview"></iframe>
 
 
 
 
 
 
 
337
  </div>
338
  </div>
339
  </div>
340
 
341
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  const examples = {
343
+ hello: {
344
+ python: 'print("Hello from Landrun Sandbox!")\\nprint("🔒 Running with kernel-level security!")',
345
+ javascript: 'console.log("Hello from Landrun Sandbox!");\\nconsole.log("🔒 Running with kernel-level security!");',
346
+ react: 'export default function App() {\\n return <div><h1>Hello from React!</h1><p>🔒 Landrun secured</p></div>;\\n}',
347
+ html: '<!DOCTYPE html>\\n<html>\\n<head><title>Hello</title></head>\\n<body><h1>Hello from HTML!</h1></body>\\n</html>'
348
+ },
349
+ math: {
350
+ python: 'import math\\nprint(f"π = {math.pi}")\\nprint(f"e = {math.e}")\\nprint(f"sqrt(16) = {math.sqrt(16)}")',
351
+ javascript: 'console.log( = ${Math.PI}`);\\nconsole.log(`e = ${Math.E}`);\\nconsole.log(`sqrt(16) = ${Math.sqrt(16)}`);'
352
+ },
353
+ html: {
354
+ html: '<!DOCTYPE html>\\n<html>\\n<head><style>body{font-family:Arial;text-align:center;padding:50px}</style></head>\\n<body><h1>🔒 Landrun Sandbox</h1><p>Kernel-level security active!</p></body>\\n</html>'
355
+ },
356
+ react: {
357
+ react: 'export default function App() {\\n return (\\n <div style={{textAlign:"center",padding:"50px"}}>\\n <h1>🔒 Landrun Sandbox</h1>\\n <p>React app with kernel-level security!</p>\\n </div>\\n );\\n}'
358
+ }
359
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ function loadExample(type) {
362
+ const lang = document.getElementById('language').value;
363
+ const code = examples[type]?.[lang] || examples[type]?.python || examples.hello[lang];
364
+ document.getElementById('code').value = code;
 
365
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ async function executeCode() {
368
+ const code = document.getElementById('code').value;
369
+ const language = document.getElementById('language').value;
370
+ const output = document.getElementById('output');
371
+ const status = document.getElementById('status');
372
+ const runBtn = document.getElementById('runBtn');
373
+ const preview = document.getElementById('preview');
374
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  runBtn.disabled = true;
376
+ runBtn.textContent = '⏳ Executing with Landrun...';
377
+ status.innerHTML = '<div class="status">⚙️ Executing in kernel-secured sandbox...</div>';
378
+ output.textContent = 'Executing...';
379
+
380
  try {
381
+ const response = await fetch('/execute', {
382
  method: 'POST',
383
+ headers: {'Content-Type': 'application/json'},
384
+ body: JSON.stringify({language, code})
385
  });
386
+
387
+ const result = await response.json();
388
+
389
+ if (result.error) {
390
+ status.innerHTML = `<div class="status error">❌ Error: ${result.error}</div>`;
391
+ output.textContent = result.error;
392
+ } else {
393
+ status.innerHTML = `<div class="status success">✅ Success! ${result.security || ''}</div>`;
394
+ output.textContent = result.output || 'Execution completed successfully';
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
+ if (result.preview) {
397
+ const decoded = atob(result.preview);
398
+ preview.srcdoc = decoded;
399
  }
400
  }
 
401
  } catch (error) {
402
+ status.innerHTML = `<div class="status error">❌ Network Error</div>`;
403
+ output.textContent = error.message;
 
404
  } finally {
405
  runBtn.disabled = false;
406
+ runBtn.textContent = '▶️ Run Code (Landrun Secured)';
407
  }
408
  }
409
+
410
+ document.getElementById('language').addEventListener('change', () => {
411
+ loadExample('hello');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  });
 
 
 
413
  </script>
414
  </body>
415
  </html>
416
  """
417
+
418
+
419
+ @app.post("/execute")
420
+ async def execute(request: Request):
421
+ """Execute code with landrun sandboxing"""
422
+ data = await request.json()
423
+ language = data.get("language", "python")
424
+ code = data.get("code", "")
425
+
426
+ if not code:
427
+ return JSONResponse({"error": "No code provided"})
428
+
429
+ result = execute_with_landrun(language, code)
430
+ return JSONResponse(result)
431
+
432
+
433
+ @app.get("/health")
434
+ async def health():
435
+ """Health check endpoint"""
436
+ return {"status": "healthy", "sandbox": "landrun", "security": "kernel-level"}
437
 
438
 
439
  if __name__ == "__main__":
landrun-main/.github/workflows/build.yml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Build
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Set up Go
16
+ uses: actions/setup-go@v5
17
+ with:
18
+ go-version: "1.24.1"
19
+ check-latest: true
20
+
21
+ - name: Install dependencies
22
+ run: go mod download
23
+
24
+ - name: Build
25
+ run: go build -v -o landrun ./cmd/landrun/main.go
26
+
27
+ - name: Upload binary
28
+ uses: actions/upload-artifact@v4
29
+ with:
30
+ name: landrun-linux-amd64
31
+ path: ./landrun
landrun-main/.github/workflows/go-compatibility.yml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Go version compatibility
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ go: ["1.18", "1.20", "1.22", "1.24"]
11
+ name: Go ${{ matrix.go }} build
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+
15
+ - name: Set up Go ${{ matrix.go }}
16
+ uses: actions/setup-go@v4
17
+ with:
18
+ go-version: ${{ matrix.go }}
19
+
20
+ - name: Download dependencies
21
+ run: go mod tidy
22
+
23
+ - name: Build landrun
24
+ run: go build ./cmd/landrun
landrun-main/.gitignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # If you prefer the allow list template instead of the deny list, see community template:
2
+ # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3
+ #
4
+ # Binaries for programs and plugins
5
+ *.exe
6
+ *.exe~
7
+ *.dll
8
+ *.so
9
+ *.dylib
10
+
11
+ # Test binary, built with `go test -c`
12
+ *.test
13
+
14
+ # Output of the go coverage tool, specifically when used with LiteIDE
15
+ *.out
16
+
17
+ # Dependency directories (remove the comment below to include it)
18
+ # vendor/
19
+
20
+ # Go workspace file
21
+ go.work
22
+ go.work.sum
23
+
24
+ # env file
25
+ .env
26
+ main
27
+ tmp
28
+ internal/sandbox/test_rw
29
+ internal/sandbox/test_ro
30
+ test_env
31
+ landrun
landrun-main/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Armin ranjbar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
landrun-main/README.md ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Landrun <img src="https://avatars.githubusercontent.com/u/21111839?s=48&v=4" align="right"/>
2
+
3
+ A lightweight, secure sandbox for running Linux processes using Landlock. Think firejail, but with kernel-level security and minimal overhead.
4
+
5
+ Linux Landlock is a kernel-native security module that lets unprivileged processes sandbox themselves.
6
+
7
+ Landrun is designed to make it practical to sandbox any command with fine-grained filesystem and network access controls. No root. No containers. No SELinux/AppArmor configs.
8
+
9
+ It's lightweight, auditable, and wraps Landlock v5 features (file access + TCP restrictions).
10
+
11
+ ## Features
12
+
13
+ - 🔒 Kernel-level security using Landlock
14
+ - 🚀 Lightweight and fast execution
15
+ - 🛡️ Fine-grained access control for directories and files
16
+ - 🔄 Support for read and write paths
17
+ - ⚡ Path-specific execution permissions
18
+ - 🌐 TCP network access control (binding and connecting)
19
+
20
+ ## Demo
21
+
22
+ <p align="center">
23
+ <img src="demo.gif" alt="landrun demo" width="700"/>
24
+ </p>
25
+
26
+ ## Requirements
27
+
28
+ - Linux kernel 5.13 or later with Landlock enabled
29
+ - Linux kernel 6.7 or later for network restrictions (TCP bind/connect)
30
+ - Go 1.18 or later (for building from source)
31
+
32
+ ## Installation
33
+
34
+ ### Quick Install
35
+
36
+ ```bash
37
+ go install github.com/zouuup/landrun/cmd/landrun@latest
38
+ ```
39
+
40
+ ### From Source
41
+
42
+ ```bash
43
+ git clone https://github.com/zouuup/landrun.git
44
+ cd landrun
45
+ go build -o landrun cmd/landrun/main.go
46
+ sudo cp landrun /usr/local/bin/
47
+ ```
48
+
49
+ ### Distros
50
+
51
+ #### Arch (AUR)
52
+
53
+ - [stable](https://aur.archlinux.org/packages/landrun) maintained by [Vcalv](https://github.com/vcalv)
54
+ - [latest commit](https://aur.archlinux.org/packages/landrun-git) maintained by [juxuanu](https://github.com/juxuanu/)
55
+
56
+ #### Slackware
57
+
58
+ maintained by [r1w1s1](https://github.com/r1w1s1)
59
+
60
+ [Slackbuild](https://slackbuilds.org/repository/15.0/network/landrun/?search=landrun)
61
+ ```bash
62
+ sudo sbopkg -i packagename
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ Basic syntax:
68
+
69
+ ```bash
70
+ landrun [options] <command> [args...]
71
+ ```
72
+
73
+ ### Options
74
+
75
+ - `--ro <path>`: Allow read-only access to specified path (can be specified multiple times or as comma-separated values)
76
+ - `--rox <path>`: Allow read-only access with execution to specified path (can be specified multiple times or as comma-separated values)
77
+ - `--rw <path>`: Allow read-write access to specified path (can be specified multiple times or as comma-separated values)
78
+ - `--rwx <path>`: Allow read-write access with execution to specified path (can be specified multiple times or as comma-separated values)
79
+ - `--bind-tcp <port>`: Allow binding to specified TCP port (can be specified multiple times or as comma-separated values)
80
+ - `--connect-tcp <port>`: Allow connecting to specified TCP port (can be specified multiple times or as comma-separated values)
81
+ - `--env <var>`: Environment variable to pass to the sandboxed command (format: KEY=VALUE or just KEY to pass current value)
82
+ - `--best-effort`: Use best effort mode, falling back to less restrictive sandbox if necessary [default: disabled]
83
+ - `--log-level <level>`: Set logging level (error, info, debug) [default: "error"]
84
+ - `--unrestricted-network`: Allows unrestricted network access (disables all network restrictions)
85
+ - `--unrestricted-filesystem`: Allows unrestricted filesystem access (disables all filesystem restrictions)
86
+ - `--add-exec`: Automatically adds the executing binary to --rox
87
+ - `--ldd`: Automatically adds required libraries to --rox
88
+
89
+ ### Important Notes
90
+
91
+ - You must explicitly add the directory or files to the command you want to run with `--rox` flag
92
+ - For system commands, you typically need to include `/usr/bin`, `/usr/lib`, and other system directories
93
+ - Use `--rwx` for directories or files where you need both write access and the ability to execute files
94
+ - Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
95
+ - By default, no environment variables are passed to the sandboxed command. Use `--env` to explicitly pass environment variables
96
+ - The `--best-effort` flag allows graceful degradation on older kernels that don't support all requested restrictions
97
+ - Paths can be specified either using multiple flags or as comma-separated values (e.g., `--ro /usr,/lib,/home`)
98
+ - If no paths or network rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions (denying all access)
99
+
100
+ ### Environment Variables
101
+
102
+ - `LANDRUN_LOG_LEVEL`: Set logging level (error, info, debug)
103
+
104
+ ### Examples
105
+
106
+ 1. Run a command that allows exec access to a specific file
107
+
108
+ ```bash
109
+ landrun --rox /usr/bin/ls --rox /usr/lib --ro /home ls /home
110
+ ```
111
+
112
+ 2. Run a command with read-only access to a directory:
113
+
114
+ ```bash
115
+ landrun --rox /usr/ --ro /path/to/dir ls /path/to/dir
116
+ ```
117
+
118
+ 3. Run a command with write access to a directory:
119
+
120
+ ```bash
121
+ landrun --rox /usr/bin --ro /lib --rw /path/to/dir touch /path/to/dir/newfile
122
+ ```
123
+
124
+ 4. Run a command with write access to a file:
125
+
126
+ ```bash
127
+ landrun --rox /usr/bin --ro /lib --rw /path/to/dir/newfile touch /path/to/dir/newfile
128
+ ```
129
+
130
+ 5. Run a command with execution permissions:
131
+
132
+ ```bash
133
+ landrun --rox /usr/ --ro /lib,/lib64 /usr/bin/bash
134
+ ```
135
+
136
+ 6. Run with debug logging:
137
+
138
+ ```bash
139
+ landrun --log-level debug --rox /usr/ --ro /lib,/lib64,/path/to/dir ls /path/to/dir
140
+ ```
141
+
142
+ 7. Run with network restrictions:
143
+
144
+ ```bash
145
+ landrun --rox /usr/ --ro /lib,/lib64 --bind-tcp 8080 --connect-tcp 80 /usr/bin/my-server
146
+ ```
147
+
148
+ This will allow the program to only bind to TCP port 8080 and connect to TCP port 80.
149
+
150
+ 8. Run a DNS client with appropriate permissions:
151
+
152
+ ```bash
153
+ landrun --log-level debug --ro /etc,/usr --rox /usr/ --connect-tcp 443 nc kernel.org 443
154
+ ```
155
+
156
+ This allows connections to port 443, requires access to /etc/resolv.conf for resolving DNS.
157
+
158
+ 9. Run a web server with selective network permissions:
159
+
160
+ ```bash
161
+ landrun --rox /usr/bin --ro /lib,/lib64,/var/www --rwx /var/log --bind-tcp 80,443 /usr/bin/nginx
162
+ ```
163
+
164
+ 10. Running anything without providing parameters is... maximum security jail!
165
+
166
+ ```bash
167
+ landrun ls
168
+ ```
169
+
170
+ 11. If you keep getting permission denied without knowing what exactly going on, best to use strace with it.
171
+
172
+ ```bash
173
+ landrun --rox /usr strace -f -e trace=all ls
174
+ ```
175
+
176
+ 12. Run with specific environment variables:
177
+
178
+ ```bash
179
+ landrun --rox /usr --ro /etc --env HOME --env PATH --env CUSTOM_VAR=my_value -- env
180
+ ```
181
+
182
+ This example passes the current HOME and PATH variables, plus a custom variable named CUSTOM_VAR.
183
+
184
+ 13. Run command with explicity access to files instead of directories:
185
+ ```bash
186
+ landrun --rox /usr/lib/libc.so.6 --rox /usr/lib64/ld-linux-x86-64.so.2 --rox /usr/bin/true /usr/bin/true
187
+ ```
188
+
189
+ 14. Run a command with --add-exec which automatically adds target binary to --rox
190
+
191
+ ```bash
192
+ landrun --rox /usr/lib/ --add-exec /usr/bin/true
193
+ ```
194
+
195
+ 15. Run a command with --ldd and --add-exec which automatically adds required libraries and target binary to --rox
196
+
197
+ ```bash
198
+ landrun --ldd --add-exec /usr/bin/true
199
+ ```
200
+
201
+ Note that shared libs always need exec permission due to how they are loaded, PROT_EXEC on mmap() etc.
202
+
203
+ ## Systemd Integration
204
+
205
+ landrun can be integrated with systemd to run services with enhanced security. Here's an example of running nginx with landrun:
206
+
207
+ 1. Create a systemd service file (e.g., `/etc/systemd/system/nginx-landrun.service`):
208
+
209
+ ```ini
210
+ [Unit]
211
+ Description=nginx with landrun sandbox
212
+ After=network.target
213
+
214
+ [Service]
215
+ Type=simple
216
+ ExecStart=/usr/bin/landrun \
217
+ --rox /usr/bin,/usr/lib \
218
+ --ro /etc/nginx,/etc/ssl,/etc/passwd,/etc/group,/etc/nsswitch.conf \
219
+ --rwx /var/log/nginx \
220
+ --rwx /var/cache/nginx \
221
+ --bind-tcp 80,443 \
222
+ /usr/bin/nginx -g 'daemon off;'
223
+ Restart=always
224
+ User=nginx
225
+ Group=nginx
226
+
227
+ [Install]
228
+ WantedBy=multi-user.target
229
+ ```
230
+
231
+ 2. Enable and start the service:
232
+
233
+ ```bash
234
+ sudo systemctl daemon-reload
235
+ sudo systemctl enable nginx-landrun
236
+ sudo systemctl start nginx-landrun
237
+ ```
238
+
239
+ 3. Check the service status:
240
+
241
+ ```bash
242
+ sudo systemctl status nginx-landrun
243
+ ```
244
+
245
+ This configuration:
246
+ - Runs nginx with minimal required permissions
247
+ - Allows binding to ports 80 and 443
248
+ - Provides read-only access to configuration files
249
+ - Allows write access only to log and cache directories
250
+ - Runs as the nginx user and group
251
+ - Automatically restarts on failure
252
+
253
+ You can adjust the permissions based on your specific needs. For example, if you need to serve static files from `/var/www`, add `--ro /var/www` to the ExecStart line.
254
+
255
+ ## Security
256
+
257
+ landrun uses Linux's Landlock to create a secure sandbox environment. It provides:
258
+
259
+ - File system access control
260
+ - Directory access restrictions
261
+ - Execution control
262
+ - TCP network restrictions
263
+ - Process isolation
264
+ - Default restrictive mode when no rules are specified
265
+
266
+ Landlock is an access-control system that enables processes to securely restrict themselves and their future children. As a stackable Linux Security Module (LSM), it creates additional security layers on top of existing system-wide access controls, helping to mitigate security impacts from bugs or malicious behavior in applications.
267
+
268
+ ### Landlock Access Control Rights
269
+
270
+ landrun leverages Landlock's fine-grained access control mechanisms, which include:
271
+
272
+ **File-specific rights:**
273
+
274
+ - Execute files (`LANDLOCK_ACCESS_FS_EXECUTE`)
275
+ - Write to files (`LANDLOCK_ACCESS_FS_WRITE_FILE`)
276
+ - Read files (`LANDLOCK_ACCESS_FS_READ_FILE`)
277
+ - Truncate files (`LANDLOCK_ACCESS_FS_TRUNCATE`) - Available since Landlock ABI v3
278
+ - IOCTL operations on devices (`LANDLOCK_ACCESS_FS_IOCTL_DEV`) - Available since Landlock ABI v5
279
+
280
+ **Directory-specific rights:**
281
+
282
+ - Read directory contents (`LANDLOCK_ACCESS_FS_READ_DIR`)
283
+ - Remove directories (`LANDLOCK_ACCESS_FS_REMOVE_DIR`)
284
+ - Remove files (`LANDLOCK_ACCESS_FS_REMOVE_FILE`)
285
+ - Create various filesystem objects (char devices, directories, regular files, sockets, etc.)
286
+ - Refer/reparent files across directories (`LANDLOCK_ACCESS_FS_REFER`) - Available since Landlock ABI v2
287
+
288
+ **Network-specific rights** (requires Linux 6.7+ with Landlock ABI v4):
289
+
290
+ - Bind to specific TCP ports (`LANDLOCK_ACCESS_NET_BIND_TCP`)
291
+ - Connect to specific TCP ports (`LANDLOCK_ACCESS_NET_CONNECT_TCP`)
292
+
293
+ ### Limitations
294
+
295
+ - Landlock must be supported by your kernel
296
+ - Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
297
+ - Some operations may require additional permissions
298
+ - Files or directories opened before sandboxing are not subject to Landlock restrictions
299
+
300
+ ## Kernel Compatibility Table
301
+
302
+ | Feature | Minimum Kernel Version | Landlock ABI Version |
303
+ | ---------------------------------- | ---------------------- | -------------------- |
304
+ | Basic filesystem sandboxing | 5.13 | 1 |
305
+ | File referring/reparenting control | 5.19 | 2 |
306
+ | File truncation control | 6.2 | 3 |
307
+ | Network TCP restrictions | 6.7 | 4 |
308
+ | IOCTL on special files | 6.10 | 5 |
309
+
310
+ ## Troubleshooting
311
+
312
+ If you receive "permission denied" or similar errors:
313
+
314
+ 1. Ensure you've added all necessary paths with `--ro` or `--rw`
315
+ 2. Try running with `--log-level debug` to see detailed permission information
316
+ 3. Check that Landlock is supported and enabled on your system:
317
+ ```bash
318
+ grep -E 'landlock|lsm=' /boot/config-$(uname -r)
319
+ # alternatively, if there are no /boot/config-* files
320
+ zgrep -iE 'landlock|lsm=' /proc/config.gz
321
+ # another alternate method
322
+ grep -iE 'landlock|lsm=' /lib/modules/$(uname -r)/config
323
+ ```
324
+ You should see `CONFIG_SECURITY_LANDLOCK=y` and `lsm=landlock,...` in the output
325
+ 4. For network restrictions, verify your kernel version is 6.7+ with Landlock ABI v4:
326
+ ```bash
327
+ uname -r
328
+ ```
329
+
330
+ ## Technical Details
331
+
332
+ ### Implementation
333
+
334
+ This project uses the [landlock-lsm/go-landlock](https://github.com/landlock-lsm/go-landlock) package for sandboxing, which provides both filesystem and network restrictions. The current implementation supports:
335
+
336
+ - Read/write/execute restrictions for files and directories
337
+ - TCP port binding restrictions
338
+ - TCP port connection restrictions
339
+ - Best-effort mode for graceful degradation on older kernels
340
+
341
+ ### Best-Effort Mode
342
+
343
+ When using `--best-effort` (disabled by default), landrun will gracefully degrade to using the best available Landlock version on the current kernel. This means:
344
+
345
+ - On Linux 6.7+: Full filesystem and network restrictions
346
+ - On Linux 6.2-6.6: Filesystem restrictions including truncation, but no network restrictions
347
+ - On Linux 5.19-6.1: Basic filesystem restrictions including file reparenting, but no truncation control or network restrictions
348
+ - On Linux 5.13-5.18: Basic filesystem restrictions without file reparenting, truncation control, or network restrictions
349
+ - On older Linux: No restrictions (sandbox disabled)
350
+
351
+ When no rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions available for the current kernel version.
352
+
353
+ ### Tests
354
+
355
+ The project includes a comprehensive test suite that verifies:
356
+
357
+ - Basic filesystem access controls (read-only, read-write, execute)
358
+ - Directory traversal and path handling
359
+ - Network restrictions (TCP bind/connect)
360
+ - Environment variable isolation
361
+ - System command execution
362
+ - Edge cases and regression tests
363
+
364
+ Run the tests with:
365
+
366
+ ```bash
367
+ ./test.sh
368
+ ```
369
+
370
+ Use `--keep-binary` to preserve the test binary after completion:
371
+
372
+ ```bash
373
+ ./test.sh --keep-binary
374
+ ```
375
+
376
+ Use `--use-system` to test against the system-installed landrun binary:
377
+
378
+ ```bash
379
+ ./test.sh --use-system
380
+ ```
381
+
382
+ ## Future Features
383
+
384
+ Based on the Linux Landlock API capabilities, we plan to add:
385
+
386
+ - 🔒 Enhanced filesystem controls with more fine-grained permissions
387
+ - 🌐 Support for UDP and other network protocol restrictions (when supported by Linux kernel)
388
+ - 🔄 Process scoping and resource controls
389
+ - 🛡️ Additional security features as they become available in the Landlock API
390
+
391
+ ## Acknowledgements
392
+
393
+ This project wouldn't exist without:
394
+
395
+ - [Landlock](https://landlock.io), the kernel security module enabling unprivileged sandboxing - maintained by [@l0kod](https://github.com/l0kod)
396
+ - [go-landlock](https://github.com/landlock-lsm/go-landlock), the Go bindings powering this tool - developed by [@gnoack](https://github.com/gnoack)
397
+
398
+ ## Contributing
399
+
400
+ Contributions are welcome! Please feel free to submit a Pull Request.
landrun-main/demo.gif ADDED
landrun-main/go.mod ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module github.com/zouuup/landrun
2
+
3
+ go 1.18
4
+
5
+ require (
6
+ github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3
7
+ github.com/urfave/cli/v2 v2.27.6
8
+ )
9
+
10
+ require (
11
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
12
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
13
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
14
+ golang.org/x/sys v0.26.0 // indirect
15
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
16
+ )
landrun-main/go.sum ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
2
+ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3
+ github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3 h1:zcMi8R8vP0WrrXlFMNUBpDy/ydo3sTnCcUPowq1XmSc=
4
+ github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3/go.mod h1:RSub3ourNF8Hf+swvw49Catm3s7HVf4hzdFxDUnEzdA=
5
+ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
6
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
7
+ github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
8
+ github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
9
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
10
+ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
11
+ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
12
+ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
14
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
landrun-main/internal/elfdeps/elfdeps.go ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package elfdeps
2
+
3
+ import (
4
+ "debug/elf"
5
+ "io"
6
+ "os"
7
+ osexec "os/exec"
8
+ "path/filepath"
9
+ "strings"
10
+ )
11
+
12
+ // ldconfigRunner runs `ldconfig -p` and returns its output. Tests may override
13
+ // this variable to inject fake output. It is unexported on purpose to allow
14
+ // test injection within the package.
15
+ var ldconfigRunner = func() ([]byte, error) {
16
+ return osexec.Command("ldconfig", "-p").Output()
17
+ }
18
+
19
+ // getLdmap runs `ldconfig -p` and returns a map of soname -> path.
20
+ func getLdmap() map[string]string {
21
+ m := map[string]string{}
22
+ out, err := ldconfigRunner()
23
+ if err != nil {
24
+ return m
25
+ }
26
+ lines := strings.Split(string(out), "\n")
27
+ for _, line := range lines {
28
+ if !strings.Contains(line, "=>") {
29
+ continue
30
+ }
31
+ parts := strings.Split(line, "=>")
32
+ if len(parts) < 2 {
33
+ continue
34
+ }
35
+ path := strings.TrimSpace(parts[len(parts)-1])
36
+ left := strings.TrimSpace(parts[0])
37
+ toks := strings.Fields(left)
38
+ if len(toks) == 0 {
39
+ continue
40
+ }
41
+ soname := toks[0]
42
+ if path == "" || soname == "" {
43
+ continue
44
+ }
45
+ if _, err := os.Stat(path); err == nil {
46
+ if _, exists := m[soname]; !exists {
47
+ m[soname] = path
48
+ }
49
+ }
50
+ }
51
+ return m
52
+ }
53
+
54
+ // parseInterp extracts the PT_INTERP interpreter path from an ELF file.
55
+ func parseInterp(f *elf.File) string {
56
+ for _, prog := range f.Progs {
57
+ if prog.Type == elf.PT_INTERP {
58
+ r := prog.Open()
59
+ if r == nil {
60
+ // Can't read interpreter
61
+ return ""
62
+ }
63
+ if data, err := io.ReadAll(r); err == nil {
64
+ return strings.TrimRight(string(data), "\x00")
65
+ }
66
+ }
67
+ }
68
+ return ""
69
+ }
70
+
71
+ // parseDynamic extracts DT_NEEDED and RPATH/RUNPATH entries from the .dynamic section.
72
+ func parseDynamic(f *elf.File) (needed []string, rpaths []string) {
73
+ needed = []string{}
74
+ rpaths = []string{}
75
+
76
+ if libs, err := f.DynString(elf.DT_NEEDED); err == nil {
77
+ needed = append(needed, libs...)
78
+ }
79
+
80
+ // DT_RPATH and DT_RUNPATH may both be present; split on ':' and append
81
+ if rp, err := f.DynString(elf.DT_RPATH); err == nil {
82
+ for _, v := range rp {
83
+ if v == "" {
84
+ continue
85
+ }
86
+ rpaths = append(rpaths, strings.Split(v, ":")...)
87
+ }
88
+ }
89
+ if rp, err := f.DynString(elf.DT_RUNPATH); err == nil {
90
+ for _, v := range rp {
91
+ if v == "" {
92
+ continue
93
+ }
94
+ rpaths = append(rpaths, strings.Split(v, ":")...)
95
+ }
96
+ }
97
+ return
98
+ }
99
+
100
+ // normalizeRpaths expands common tokens like $ORIGIN and makes relative
101
+ // rpath entries absolute using the provided origin directory.
102
+ func normalizeRpaths(rpaths []string, origin string) []string {
103
+ out := []string{}
104
+ for _, rp := range rpaths {
105
+ if rp == "" {
106
+ continue
107
+ }
108
+ // expand $ORIGIN (common token in RPATH/RUNPATH)
109
+ rp = strings.ReplaceAll(rp, "$ORIGIN", origin)
110
+ rp = strings.ReplaceAll(rp, "${ORIGIN}", origin)
111
+ // make relative rpath entries absolute using origin
112
+ if !filepath.IsAbs(rp) {
113
+ rp = filepath.Join(origin, rp)
114
+ }
115
+ out = append(out, rp)
116
+ }
117
+ return out
118
+ }
119
+
120
+ // resolveSingleSoname attempts to resolve a single soname using rpaths,
121
+ // standard dirs and ldconfig fallback. It takes a pointer to ldmap so the
122
+ // caller can lazily populate and reuse it.
123
+ func resolveSingleSoname(soname string, rpaths []string, stdDirs []string, ldmap *map[string]string) string {
124
+ // check rpaths first
125
+ for _, rp := range rpaths {
126
+ candidate := filepath.Join(rp, soname)
127
+ if _, err := os.Stat(candidate); err == nil {
128
+ return candidate
129
+ }
130
+ }
131
+
132
+ // then check standard dirs
133
+ for _, d := range stdDirs {
134
+ candidate := filepath.Join(d, soname)
135
+ if _, err := os.Stat(candidate); err == nil {
136
+ return candidate
137
+ }
138
+ }
139
+
140
+ // fallback: consult parsed ldconfig map (populate lazily)
141
+ if *ldmap == nil {
142
+ *ldmap = getLdmap()
143
+ }
144
+ if p, ok := (*ldmap)[soname]; ok {
145
+ return p
146
+ }
147
+
148
+ return ""
149
+ }
150
+
151
+ // resolveSonames attempts to resolve sonames to absolute paths using rpaths,
152
+ // standard library directories and falling back to parsing `ldconfig -p` output.
153
+ func resolveSonames(needed []string, rpaths []string) []string {
154
+ resolved := map[string]string{}
155
+ stdDirs := []string{"/lib", "/lib64", "/usr/lib", "/usr/lib64", "/usr/local/lib"}
156
+ var ldmap map[string]string
157
+
158
+ for _, soname := range needed {
159
+ if _, ok := resolved[soname]; ok {
160
+ continue
161
+ }
162
+ resolved[soname] = resolveSingleSoname(soname, rpaths, stdDirs, &ldmap)
163
+ }
164
+
165
+ out := []string{}
166
+ for _, r := range resolved {
167
+ if r != "" {
168
+ out = append(out, r)
169
+ }
170
+ }
171
+ return out
172
+ }
173
+
174
+ // GetLibraryDependencies returns a list of library paths that the given binary depends on
175
+ func GetLibraryDependencies(binary string) ([]string, error) {
176
+ queue := []string{binary}
177
+ processed := map[string]struct{}{}
178
+ finalMap := map[string]struct{}{}
179
+
180
+ // Add /etc/ld.so.cache if present
181
+ if _, err := os.Stat("/etc/ld.so.cache"); err == nil {
182
+ finalMap["/etc/ld.so.cache"] = struct{}{}
183
+ }
184
+
185
+ for len(queue) > 0 {
186
+ // Dequeue
187
+ curr := queue[0]
188
+ queue = queue[1:]
189
+
190
+ if _, ok := processed[curr]; ok {
191
+ continue
192
+ }
193
+ processed[curr] = struct{}{}
194
+
195
+ f, err := elf.Open(curr)
196
+ if err != nil {
197
+ // This can happen with non-ELF files in the dependency chain
198
+ // (e.g. ld.so.cache). Ignore them.
199
+ continue
200
+ }
201
+ defer f.Close()
202
+
203
+ // The first binary in the queue is the main one; grab its interpreter
204
+ if curr == binary {
205
+ if interpPath := parseInterp(f); interpPath != "" {
206
+ finalMap[interpPath] = struct{}{}
207
+ queue = append(queue, interpPath)
208
+ }
209
+ }
210
+
211
+ needed, rpaths := parseDynamic(f)
212
+ origin := filepath.Dir(curr)
213
+ rpaths = normalizeRpaths(rpaths, origin)
214
+ libPaths := resolveSonames(needed, rpaths)
215
+
216
+ for _, p := range libPaths {
217
+ if _, ok := finalMap[p]; !ok {
218
+ finalMap[p] = struct{}{}
219
+ queue = append(queue, p)
220
+ }
221
+ }
222
+ }
223
+
224
+ out := make([]string, 0, len(finalMap))
225
+ for p := range finalMap {
226
+ out = append(out, p)
227
+ }
228
+
229
+ return out, nil
230
+ }
landrun-main/internal/elfdeps/elfdeps_test.go ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package elfdeps
2
+
3
+ import (
4
+ "debug/elf"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "testing"
9
+ )
10
+
11
+ // Test helpers against a known binary in the system: find `true` via LookPath
12
+ func TestParseAndResolveTrue(t *testing.T) {
13
+ bin, err := exec.LookPath("true")
14
+ if err != nil {
15
+ t.Fatalf("failed to find 'true' binary: %v", err)
16
+ }
17
+
18
+ f, err := elf.Open(bin)
19
+ if err != nil {
20
+ t.Fatalf("failed to open %s: %v", bin, err)
21
+ }
22
+ defer f.Close()
23
+
24
+ interp := parseInterp(f)
25
+ if interp == "" {
26
+ t.Fatalf("expected interpreter for %s, got empty", bin)
27
+ }
28
+
29
+ needed, rpaths := parseDynamic(f)
30
+ if needed == nil {
31
+ needed = []string{}
32
+ }
33
+
34
+ origin := filepath.Dir(bin)
35
+ rpaths = normalizeRpaths(rpaths, origin)
36
+ paths := resolveSonames(needed, rpaths)
37
+ if paths == nil {
38
+ paths = []string{}
39
+ }
40
+
41
+ // Ensure interpreter path exists on filesystem
42
+ if _, err := os.Stat(interp); err != nil {
43
+ t.Fatalf("interp path %s does not exist: %v", interp, err)
44
+ }
45
+
46
+ // If there are resolved library paths, they must exist
47
+ for _, p := range paths {
48
+ if _, err := os.Stat(p); err != nil {
49
+ t.Fatalf("resolved library path %s does not exist: %v", p, err)
50
+ }
51
+ }
52
+ }
53
+
54
+ func TestRecursiveDependencies(t *testing.T) {
55
+ if _, err := exec.LookPath("gcc"); err != nil {
56
+ t.Skip("gcc not found, skipping test")
57
+ }
58
+ // Create a temporary directory for compiled artifacts
59
+ tempDir := t.TempDir()
60
+
61
+ // Compile liba.so
62
+ libaSrc := "testdata/liba.c"
63
+ libaSo := filepath.Join(tempDir, "liba.so")
64
+ cmd := exec.Command("gcc", "-fPIC", "-shared", "-o", libaSo, libaSrc)
65
+ if out, err := cmd.CombinedOutput(); err != nil {
66
+ t.Fatalf("failed to compile liba.so: %v\n%s", err, string(out))
67
+ }
68
+
69
+ // Compile libb.so
70
+ libbSrc := "testdata/libb.c"
71
+ libbSo := filepath.Join(tempDir, "libb.so")
72
+ cmd = exec.Command("gcc", "-fPIC", "-shared", "-o", libbSo, libbSrc, "-L"+tempDir, "-la", "-Wl,-rpath,$ORIGIN")
73
+ if out, err := cmd.CombinedOutput(); err != nil {
74
+ t.Fatalf("failed to compile libb.so: %v\n%s", err, string(out))
75
+ }
76
+
77
+ // Compile test_binary
78
+ mainSrc := "testdata/main.c"
79
+ testBin := filepath.Join(tempDir, "test_binary")
80
+ cmd = exec.Command("gcc", "-o", testBin, mainSrc, "-L"+tempDir, "-lb", "-Wl,-rpath,$ORIGIN")
81
+ if out, err := cmd.CombinedOutput(); err != nil {
82
+ t.Fatalf("failed to compile test_binary: %v\n%s", err, string(out))
83
+ }
84
+
85
+ // Run the actual test logic
86
+ deps, err := GetLibraryDependencies(testBin)
87
+ if err != nil {
88
+ t.Fatalf("GetLibraryDependencies failed: %v", err)
89
+ }
90
+
91
+ foundA := false
92
+ foundB := false
93
+ for _, dep := range deps {
94
+ if dep == libaSo {
95
+ foundA = true
96
+ }
97
+ if dep == libbSo {
98
+ foundB = true
99
+ }
100
+ }
101
+
102
+ if !foundA {
103
+ t.Errorf("expected to find %s in dependency list, but didn't. Found: %v", libaSo, deps)
104
+ }
105
+ if !foundB {
106
+ t.Errorf("expected to find %s in dependency list, but didn't. Found: %v", libbSo, deps)
107
+ }
108
+ }
109
+
110
+ func TestGetLibraryDependencies(t *testing.T) {
111
+ bin, err := exec.LookPath("true")
112
+ if err != nil {
113
+ t.Fatalf("failed to find 'true' binary: %v", err)
114
+ }
115
+ paths, err := GetLibraryDependencies(bin)
116
+ if err != nil {
117
+ t.Fatalf("GetLibraryDependencies failed: %v", err)
118
+ }
119
+ if len(paths) == 0 {
120
+ t.Fatalf("expected non-empty dependency list for %s", bin)
121
+ }
122
+ // ensure returned paths are absolute and exist
123
+ for _, p := range paths {
124
+ if !filepath.IsAbs(p) {
125
+ t.Fatalf("expected absolute path, got %s", p)
126
+ }
127
+ if _, err := os.Stat(p); err != nil {
128
+ t.Fatalf("path %s does not exist: %v", p, err)
129
+ }
130
+ }
131
+ }
132
+
133
+ func TestGetLdmapWithFakeOutput(t *testing.T) {
134
+ // fake ldconfig output with a single mapping
135
+ original := ldconfigRunner
136
+ defer func() { ldconfigRunner = original }()
137
+
138
+ // create a fake file on disk to satisfy os.Stat checks in getLdmap
139
+ tmpDir := t.TempDir()
140
+ tmp := filepath.Join(tmpDir, "libfake.so")
141
+ f, err := os.Create(tmp)
142
+ if err != nil {
143
+ t.Fatalf("failed to create tmp file: %v", err)
144
+ }
145
+ f.Close()
146
+
147
+ // Because getLdmap checks the path exists, return tmp in the fake output
148
+ ldconfigRunner = func() ([]byte, error) {
149
+ return []byte("libfake.so (libc6,x86-64) => " + tmp + "\n"), nil
150
+ }
151
+
152
+ m := getLdmap()
153
+ if got, ok := m["libfake.so"]; !ok {
154
+ t.Fatalf("expected libfake.so in map")
155
+ } else if got != tmp {
156
+ t.Fatalf("expected path %s, got %s", tmp, got)
157
+ }
158
+ }
159
+
160
+ func TestResolveSonamesUsesLdmapFallback(t *testing.T) {
161
+ original := ldconfigRunner
162
+ defer func() { ldconfigRunner = original }()
163
+
164
+ tmpDir := t.TempDir()
165
+ tmp := filepath.Join(tmpDir, "libfake2.so")
166
+ f, err := os.Create(tmp)
167
+ if err != nil {
168
+ t.Fatalf("failed to create tmp file: %v", err)
169
+ }
170
+ f.Close()
171
+
172
+ ldconfigRunner = func() ([]byte, error) {
173
+ return []byte("libfake2.so (libc6,x86-64) => " + tmp + "\n"), nil
174
+ }
175
+
176
+ // needed contains a soname that won't be found in rpaths or std dirs
177
+ rpaths := normalizeRpaths([]string{}, tmpDir)
178
+ out := resolveSonames([]string{"libfake2.so"}, rpaths)
179
+ if len(out) != 1 {
180
+ t.Fatalf("expected 1 resolved path, got %d", len(out))
181
+ }
182
+ if out[0] != tmp {
183
+ t.Fatalf("expected %s, got %s", tmp, out[0])
184
+ }
185
+ }
186
+
187
+ func TestResolveSonamesOriginExpansion(t *testing.T) {
188
+ // Create a temp dir and a lib subdir to simulate $ORIGIN/lib
189
+ tmpDir := t.TempDir()
190
+ libDir := filepath.Join(tmpDir, "lib")
191
+ if err := os.Mkdir(libDir, 0755); err != nil {
192
+ t.Fatalf("failed create lib dir: %v", err)
193
+ }
194
+
195
+ libName := "liborigin.so"
196
+ libPath := filepath.Join(libDir, libName)
197
+ f, err := os.Create(libPath)
198
+ if err != nil {
199
+ t.Fatalf("failed to create lib file: %v", err)
200
+ }
201
+ f.Close()
202
+
203
+ // rpath using $ORIGIN should resolve to tmpDir/lib
204
+ rpaths1 := normalizeRpaths([]string{"$ORIGIN/lib"}, tmpDir)
205
+ out := resolveSonames([]string{libName}, rpaths1)
206
+ if len(out) != 1 {
207
+ t.Fatalf("expected 1 resolved path for $ORIGIN, got %d", len(out))
208
+ }
209
+ if out[0] != libPath {
210
+ t.Fatalf("expected %s, got %s", libPath, out[0])
211
+ }
212
+
213
+ // relative rpath should also resolve against origin
214
+ rpaths2 := normalizeRpaths([]string{"lib"}, tmpDir)
215
+ out2 := resolveSonames([]string{libName}, rpaths2)
216
+ if len(out2) != 1 {
217
+ t.Fatalf("expected 1 resolved path for relative rpath, got %d", len(out2))
218
+ }
219
+ if out2[0] != libPath {
220
+ t.Fatalf("expected %s, got %s", libPath, out2[0])
221
+ }
222
+ }
landrun-main/internal/elfdeps/testdata/liba.c ADDED
@@ -0,0 +1 @@
 
 
1
+ int a() { return 1; }
landrun-main/internal/elfdeps/testdata/libb.c ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ int a();
2
+ int b() { return a(); }
landrun-main/internal/elfdeps/testdata/main.c ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ int b();
2
+ int main() { b(); return 0; }
landrun-main/internal/exec/runner.go ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package exec
2
+
3
+ import (
4
+ "os/exec"
5
+ "syscall"
6
+
7
+ "github.com/zouuup/landrun/internal/log"
8
+ )
9
+
10
+ func Run(args []string, env []string) error {
11
+ binary, err := exec.LookPath(args[0])
12
+ if err != nil {
13
+ return err
14
+ }
15
+
16
+ log.Info("Executing: %v", args)
17
+
18
+ // Only pass the explicitly specified environment variables
19
+ // If env is empty, no environment variables will be passed
20
+ return syscall.Exec(binary, args, env)
21
+ }
landrun-main/internal/log/log.go ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package log
2
+
3
+ import (
4
+ "log"
5
+ "os"
6
+ "strings"
7
+ )
8
+
9
+ type Level int
10
+
11
+ const (
12
+ LevelError Level = iota
13
+ LevelInfo
14
+ LevelDebug
15
+ )
16
+
17
+ var (
18
+ debug = log.New(os.Stderr, "[landrun:debug] ", log.LstdFlags)
19
+ info = log.New(os.Stderr, "[landrun] ", log.LstdFlags)
20
+ error = log.New(os.Stderr, "[landrun:error] ", log.LstdFlags)
21
+
22
+ currentLevel = LevelInfo // default level
23
+ )
24
+
25
+ // SetLevel sets the logging level
26
+ func SetLevel(level string) {
27
+ switch strings.ToLower(level) {
28
+ case "error":
29
+ currentLevel = LevelError
30
+ case "info":
31
+ currentLevel = LevelInfo
32
+ case "debug":
33
+ currentLevel = LevelDebug
34
+ default:
35
+ currentLevel = LevelError
36
+ }
37
+ }
38
+
39
+ // Debug logs a debug message
40
+ func Debug(format string, v ...interface{}) {
41
+ if currentLevel >= LevelDebug {
42
+ debug.Printf(format, v...)
43
+ }
44
+ }
45
+
46
+ // Info logs an info message
47
+ func Info(format string, v ...interface{}) {
48
+ if currentLevel >= LevelInfo {
49
+ info.Printf(format, v...)
50
+ }
51
+ }
52
+
53
+ // Error logs an error message
54
+ func Error(format string, v ...interface{}) {
55
+ if currentLevel >= LevelError {
56
+ error.Printf(format, v...)
57
+ }
58
+ }
59
+
60
+ // Fatal logs an error message and exits
61
+ func Fatal(format string, v ...interface{}) {
62
+ error.Printf(format, v...)
63
+ os.Exit(1)
64
+ }
landrun-main/internal/sandbox/sandbox.go ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package sandbox
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/landlock-lsm/go-landlock/landlock"
8
+ "github.com/landlock-lsm/go-landlock/landlock/syscall"
9
+ "github.com/zouuup/landrun/internal/log"
10
+ )
11
+
12
+ type Config struct {
13
+ ReadOnlyPaths []string
14
+ ReadWritePaths []string
15
+ ReadOnlyExecutablePaths []string
16
+ ReadWriteExecutablePaths []string
17
+ BindTCPPorts []int
18
+ ConnectTCPPorts []int
19
+ BestEffort bool
20
+ UnrestrictedFilesystem bool
21
+ UnrestrictedNetwork bool
22
+ }
23
+
24
+ // getReadWriteExecutableRights returns a full set of permissions including execution
25
+ func getReadWriteExecutableRights(dir bool) landlock.AccessFSSet {
26
+ accessRights := landlock.AccessFSSet(0)
27
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
28
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
29
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
30
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
31
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
32
+
33
+ if dir {
34
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
35
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
36
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
37
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
38
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
39
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
40
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
41
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
42
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
43
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
44
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
45
+ }
46
+
47
+ return accessRights
48
+ }
49
+
50
+ func getReadOnlyExecutableRights(dir bool) landlock.AccessFSSet {
51
+ accessRights := landlock.AccessFSSet(0)
52
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
53
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
54
+ if dir {
55
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
56
+ }
57
+ return accessRights
58
+ }
59
+
60
+ // getReadOnlyRights returns permissions for read-only access
61
+ func getReadOnlyRights(dir bool) landlock.AccessFSSet {
62
+ accessRights := landlock.AccessFSSet(0)
63
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
64
+ if dir {
65
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
66
+ }
67
+ return accessRights
68
+ }
69
+
70
+ // getReadWriteRights returns permissions for read-write access
71
+ func getReadWriteRights(dir bool) landlock.AccessFSSet {
72
+ accessRights := landlock.AccessFSSet(0)
73
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
74
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
75
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
76
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
77
+ if dir {
78
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
79
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
80
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
81
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
82
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
83
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
84
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
85
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
86
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
87
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
88
+ accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
89
+ }
90
+
91
+ return accessRights
92
+ }
93
+
94
+ // isDirectory checks if the given path is a directory
95
+ func isDirectory(path string) bool {
96
+ fileInfo, err := os.Stat(path)
97
+ if err != nil {
98
+ return false
99
+ }
100
+ return fileInfo.IsDir()
101
+ }
102
+
103
+ func Apply(cfg Config) error {
104
+ log.Info("Sandbox config: %+v", cfg)
105
+
106
+ // Get the most advanced Landlock version available
107
+ llCfg := landlock.V5
108
+ if cfg.BestEffort {
109
+ llCfg = llCfg.BestEffort()
110
+ }
111
+
112
+ // Collect our rules
113
+ var file_rules []landlock.Rule
114
+ var net_rules []landlock.Rule
115
+
116
+ // Process executable paths
117
+ for _, path := range cfg.ReadOnlyExecutablePaths {
118
+ log.Debug("Adding read-only executable path: %s", path)
119
+ file_rules = append(file_rules, landlock.PathAccess(getReadOnlyExecutableRights(isDirectory(path)), path))
120
+ }
121
+
122
+ for _, path := range cfg.ReadWriteExecutablePaths {
123
+ log.Debug("Adding read-write executable path: %s", path)
124
+ file_rules = append(file_rules, landlock.PathAccess(getReadWriteExecutableRights(isDirectory(path)), path))
125
+ }
126
+
127
+ // Process read-only paths
128
+ for _, path := range cfg.ReadOnlyPaths {
129
+ log.Debug("Adding read-only path: %s", path)
130
+ file_rules = append(file_rules, landlock.PathAccess(getReadOnlyRights(isDirectory(path)), path))
131
+ }
132
+
133
+ // Process read-write paths
134
+ for _, path := range cfg.ReadWritePaths {
135
+ log.Debug("Adding read-write path: %s", path)
136
+ file_rules = append(file_rules, landlock.PathAccess(getReadWriteRights(isDirectory(path)), path))
137
+ }
138
+
139
+ // Add rules for TCP port binding
140
+ for _, port := range cfg.BindTCPPorts {
141
+ log.Debug("Adding TCP bind port: %d", port)
142
+ net_rules = append(net_rules, landlock.BindTCP(uint16(port)))
143
+ }
144
+
145
+ // Add rules for TCP connections
146
+ for _, port := range cfg.ConnectTCPPorts {
147
+ log.Debug("Adding TCP connect port: %d", port)
148
+ net_rules = append(net_rules, landlock.ConnectTCP(uint16(port)))
149
+ }
150
+
151
+ if cfg.UnrestrictedFilesystem && cfg.UnrestrictedNetwork {
152
+ log.Info("Unrestricted filesystem and network access enabled; no rules applied.")
153
+ return nil
154
+ }
155
+
156
+ if cfg.UnrestrictedFilesystem {
157
+ log.Info("Unrestricted filesystem access enabled.")
158
+ }
159
+
160
+ if cfg.UnrestrictedNetwork {
161
+ log.Info("Unrestricted network access enabled")
162
+ }
163
+
164
+ // If we have no rules, just return
165
+ if len(file_rules) == 0 && len(net_rules) == 0 && !cfg.UnrestrictedFilesystem && !cfg.UnrestrictedNetwork {
166
+ log.Error("No rules provided, applying default restrictive rules, this will restrict anything landlock can do.")
167
+ err := llCfg.Restrict()
168
+ if err != nil {
169
+ return fmt.Errorf("failed to apply default Landlock restrictions: %w", err)
170
+ }
171
+ log.Info("Default restrictive Landlock rules applied successfully")
172
+ return nil
173
+ }
174
+
175
+ // Apply all rules at once
176
+ log.Debug("Applying Landlock restrictions")
177
+ if !cfg.UnrestrictedFilesystem {
178
+ err := llCfg.RestrictPaths(file_rules...)
179
+ if err != nil {
180
+ return fmt.Errorf("failed to apply Landlock filesystem restrictions: %w", err)
181
+ }
182
+ }
183
+ if !cfg.UnrestrictedNetwork {
184
+ err := llCfg.RestrictNet(net_rules...)
185
+ if err != nil {
186
+ return fmt.Errorf("failed to apply Landlock network restrictions: %w", err)
187
+ }
188
+ }
189
+
190
+ log.Info("Landlock restrictions applied successfully")
191
+ return nil
192
+ }
landrun-main/test.sh ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Check if we should keep the binary
4
+ KEEP_BINARY=false
5
+ USE_SYSTEM_BINARY=false
6
+ NO_BUILD=false
7
+ # Whether we should try to do network calls during testing.
8
+ INTERNET_ACCESS=true
9
+
10
+ while [ "$#" -gt 0 ]; do
11
+ case "$1" in
12
+ "--keep-binary")
13
+ KEEP_BINARY=true
14
+ shift
15
+ ;;
16
+ "--use-system")
17
+ USE_SYSTEM_BINARY=true
18
+ shift
19
+ ;;
20
+ "--no-build")
21
+ NO_BUILD=true
22
+ shift
23
+ ;;
24
+ "--offline")
25
+ INTERNET_ACCESS=false
26
+ shift
27
+ ;;
28
+ *)
29
+ echo "Unknown parameter: $1"
30
+ exit 1
31
+ ;;
32
+ esac
33
+ done
34
+
35
+ # Don't exit on error, we'll handle errors in the run_test function
36
+ set +e
37
+
38
+ # Colors for output
39
+ RED='\033[0;31m'
40
+ GREEN='\033[0;32m'
41
+ YELLOW='\033[1;33m'
42
+ NC='\033[0m' # No Color
43
+
44
+ # Function to print colored output
45
+ print_status() {
46
+ echo -e "${YELLOW}[TEST]${NC} $1"
47
+ }
48
+
49
+ print_success() {
50
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
51
+ }
52
+
53
+ print_error() {
54
+ echo -e "${RED}[ERROR]${NC} $1"
55
+ }
56
+
57
+ # Build the binary if not using system binary
58
+ if [ "$USE_SYSTEM_BINARY" = false ]; then
59
+ if [ "$NO_BUILD" = false ]; then
60
+ print_status "Building landrun binary..."
61
+ go build -o landrun cmd/landrun/main.go
62
+ if [ $? -ne 0 ]; then
63
+ print_error "Failed to build landrun binary"
64
+ exit 1
65
+ fi
66
+ print_success "Binary built successfully"
67
+ else
68
+ print_success "Using already built landrun binary"
69
+ fi
70
+ fi
71
+
72
+ # Create test directories
73
+ TEST_DIR="test_env"
74
+ RO_DIR="$TEST_DIR/ro"
75
+ RO_DIR_NESTED_RO="$RO_DIR/ro_nested_ro_1"
76
+ RO_DIR_NESTED_RW="$RO_DIR/ro_nested_rw_1"
77
+ RO_DIR_NESTED_EXEC="$RO_DIR/ro_nested_exec"
78
+
79
+ RW_DIR="$TEST_DIR/rw"
80
+ RW_DIR_NESTED_RO="$RW_DIR/rw_nested_ro_1"
81
+ RW_DIR_NESTED_RW="$RW_DIR/rw_nested_rw_1"
82
+ RW_DIR_NESTED_EXEC="$RW_DIR/rw_nested_exec"
83
+
84
+ EXEC_DIR="$TEST_DIR/exec"
85
+ NESTED_DIR="$TEST_DIR/nested/path/deep"
86
+
87
+ print_status "Setting up test environment..."
88
+ rm -rf "$TEST_DIR"
89
+ mkdir -p "$RO_DIR" "$RW_DIR" "$EXEC_DIR" "$NESTED_DIR" "$RO_DIR_NESTED_RO" "$RO_DIR_NESTED_RW" "$RO_DIR_NESTED_EXEC" "$RW_DIR_NESTED_RO" "$RW_DIR_NESTED_RW" "$RW_DIR_NESTED_EXEC"
90
+
91
+ # Create test files
92
+ echo "readonly content" > "$RO_DIR/test.txt"
93
+ echo "readwrite content" > "$RW_DIR/test.txt"
94
+ echo "nested content" > "$NESTED_DIR/test.txt"
95
+ echo "#!/bin/bash" > "$EXEC_DIR/test.sh"
96
+ echo "echo 'executable content'" >> "$EXEC_DIR/test.sh"
97
+ chmod +x "$EXEC_DIR/test.sh"
98
+ cp $EXEC_DIR/test.sh $EXEC_DIR/test2.sh
99
+
100
+ cp "$RO_DIR/test.txt" "$RO_DIR_NESTED_RO/test.txt"
101
+ cp "$RO_DIR/test.txt" "$RW_DIR_NESTED_RO/test.txt"
102
+
103
+ cp "$RW_DIR/test.txt" "$RO_DIR_NESTED_RW/test.txt"
104
+ cp "$RW_DIR/test.txt" "$RW_DIR_NESTED_RW/test.txt"
105
+
106
+ cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_EXEC/test.sh"
107
+ cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_EXEC/test.sh"
108
+ cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RO/test.sh"
109
+ cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RO/test.sh"
110
+ cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RW/test.sh"
111
+ cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RW/test.sh"
112
+
113
+ # Create a script in RW dir to test execution in RW dirs
114
+ echo "#!/bin/bash" > "$RW_DIR/rw_script.sh"
115
+ echo "echo 'this script is in a read-write directory'" >> "$RW_DIR/rw_script.sh"
116
+ chmod +x "$RW_DIR/rw_script.sh"
117
+
118
+ # Function to run a test case
119
+ run_test() {
120
+ local name="$1"
121
+ local cmd="$2"
122
+ local expected_exit="$3"
123
+
124
+ # Replace ./landrun with landrun if using system binary
125
+ if [ "$USE_SYSTEM_BINARY" = true ]; then
126
+ cmd="${cmd//.\/landrun/landrun}"
127
+ fi
128
+
129
+ print_status "Running test: $name"
130
+ eval "$cmd"
131
+ local exit_code=$?
132
+
133
+ if [ $exit_code -eq $expected_exit ]; then
134
+ print_success "Test passed: $name"
135
+ return 0
136
+ else
137
+ print_error "Test failed: $name (expected exit $expected_exit, got $exit_code)"
138
+ exit 1
139
+ fi
140
+ }
141
+
142
+ # Test cases
143
+ print_status "Starting test cases..."
144
+
145
+ # Basic access tests
146
+ run_test "Read-only access to file" \
147
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR/test.txt" \
148
+ 0
149
+
150
+ run_test "Read-only access to nested file" \
151
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR_NESTED_RO/test.txt" \
152
+ 0
153
+
154
+ run_test "Write access to nested directory writable nested in read-only directory" \
155
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW -- touch $RO_DIR_NESTED_RW/created_file" \
156
+ 0
157
+
158
+ run_test "Write access to nested file writable nested in read-only directory" \
159
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW/created_file -- touch $RO_DIR_NESTED_RW/created_file" \
160
+ 0
161
+
162
+ run_test "Read-write access to file" \
163
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RW_DIR/new.txt" \
164
+ 0
165
+
166
+ run_test "No write access to read-only directory" \
167
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RO_DIR/new.txt" \
168
+ 1
169
+
170
+ # Executable permission tests
171
+ run_test "Execute access with rox flag" \
172
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR -- $EXEC_DIR/test.sh" \
173
+ 0
174
+
175
+ run_test "Execute access with rox flag on file" \
176
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test.sh" \
177
+ 0
178
+
179
+ run_test "Execute access with rox flag on a file that is executable in same directory that one is allowed" \
180
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
181
+ 1
182
+
183
+ run_test "Execute a file with --add-exec flag" \
184
+ "./landrun --log-level debug --add-exec --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
185
+ 0
186
+
187
+ run_test "Execute a file with --add-exec and --ldd flag" \
188
+ "./landrun --log-level debug --add-exec --ldd -- $(which true)" \
189
+ 0
190
+
191
+
192
+ run_test "No execute access with just ro flag" \
193
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $EXEC_DIR -- $EXEC_DIR/test.sh" \
194
+ 1
195
+
196
+ run_test "Execute access in read-write directory" \
197
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rwx $RW_DIR -- $RW_DIR/rw_script.sh" \
198
+ 0
199
+
200
+ run_test "No execute access in read-write directory without rwx" \
201
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rw $RW_DIR -- $RW_DIR/rw_script.sh" \
202
+ 1
203
+
204
+ # Directory traversal tests
205
+ run_test "Directory traversal with root access" \
206
+ "./landrun --log-level debug --rox / -- ls /usr" \
207
+ 0
208
+
209
+ run_test "Deep directory traversal" \
210
+ "./landrun --log-level debug --rox / -- ls $NESTED_DIR" \
211
+ 0
212
+
213
+ # Multiple paths and complex specifications
214
+ run_test "Multiple read paths" \
215
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --ro $NESTED_DIR -- cat $NESTED_DIR/test.txt" \
216
+ 0
217
+
218
+ run_test "Comma-separated paths" \
219
+ "./landrun --log-level debug --rox /usr --ro /lib,/lib64,$RO_DIR -- cat $RO_DIR/test.txt" \
220
+ 0
221
+
222
+ # System command tests
223
+ run_test "Simple system command" \
224
+ "./landrun --log-level debug --rox /usr --ro /etc -- whoami" \
225
+ 0
226
+
227
+ run_test "System command with arguments" \
228
+ "./landrun --log-level debug --rox / -- ls -la /usr/bin" \
229
+ 0
230
+
231
+ # Edge cases
232
+ run_test "Non-existent read-only path" \
233
+ "./landrun --log-level debug --ro /usr --ro /lib --ro /lib64 --ro /nonexistent/path -- ls" \
234
+ 1
235
+
236
+ run_test "No configuration" \
237
+ "./landrun --log-level debug -- ls /" \
238
+ 1
239
+
240
+ # Process creation and redirection tests
241
+ run_test "Process creation with pipe" \
242
+ "./landrun --log-level debug --rox / --env PATH -- bash -c 'ls /usr | grep bin'" \
243
+ 0
244
+
245
+ run_test "File redirection" \
246
+ "./landrun --log-level debug --rox / --rw $RW_DIR --env PATH -- bash -c 'ls /usr > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
247
+ 0
248
+
249
+ # Network restrictions tests (if kernel supports it)
250
+ $INTERNET_ACCESS && run_test "TCP connection without permission" \
251
+ "./landrun --log-level debug --rox /usr --ro / -- curl -s --connect-timeout 2 https://example.com" \
252
+ 7
253
+
254
+ $INTERNET_ACCESS && run_test "TCP connection with permission" \
255
+ "./landrun --log-level debug --rox /usr --ro / --connect-tcp 443 -- curl -s --connect-timeout 2 https://example.com" \
256
+ 0
257
+
258
+ # Environment isolation tests
259
+ export TEST_ENV_VAR="test_value_123"
260
+ run_test "Environment isolation" \
261
+ "./landrun --log-level debug --rox /usr --ro / -- bash -c 'echo \$TEST_ENV_VAR'" \
262
+ 0
263
+
264
+ run_test "Environment isolation (no variables should be passed)" \
265
+ "./landrun --log-level debug --rox /usr --ro / -- bash -c '[[ -z \$TEST_ENV_VAR ]] && echo \"No env var\" || echo \$TEST_ENV_VAR'" \
266
+ 0
267
+
268
+ run_test "Passing specific environment variable" \
269
+ "./landrun --log-level debug --rox /usr --ro / --env TEST_ENV_VAR --env PATH -- bash -c 'echo \$TEST_ENV_VAR | grep \"test_value_123\"'" \
270
+ 0
271
+
272
+ run_test "Passing custom environment variable" \
273
+ "./landrun --log-level debug --rox /usr --ro / --env CUSTOM_VAR=custom_value --env PATH -- bash -c 'echo \$CUSTOM_VAR | grep \"custom_value\"'" \
274
+ 0
275
+
276
+ # Combining different permission types
277
+ run_test "Mixed permissions" \
278
+ "./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR --rwx $RW_DIR --env PATH -- bash -c '$EXEC_DIR/test.sh > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
279
+ 0
280
+
281
+ # Specific regression tests for bugs we fixed
282
+ run_test "Root path traversal regression test" \
283
+ "./landrun --log-level debug --rox /usr -- $(which ls) /usr" \
284
+ 0
285
+
286
+ run_test "Execute from read-only paths regression test" \
287
+ "./landrun --log-level debug --rox /usr --ro /usr/bin -- $(which id)" \
288
+ 0
289
+
290
+ run_test "Unrestricted filesystem access" \
291
+ "./landrun --log-level debug --unrestricted-filesystem ls /usr" \
292
+ 0
293
+
294
+ $INTERNET_ACCESS && run_test "Unrestricted network access" \
295
+ "./landrun --log-level debug --unrestricted-network --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
296
+ 0
297
+
298
+ run_test "Restricted filesystem access" \
299
+ "./landrun --log-level debug ls /usr" \
300
+ 1
301
+
302
+ $INTERNET_ACCESS && run_test "Restricted network access" \
303
+ "./landrun --log-level debug --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
304
+ 7
305
+
306
+
307
+ # Cleanup
308
+ print_status "Cleaning up..."
309
+ rm -rf "$TEST_DIR"
310
+ if [ "$KEEP_BINARY" = false ] && [ "$USE_SYSTEM_BINARY" = false ]; then
311
+ rm -f landrun
312
+ fi
313
+
314
+ print_success "All tests completed!"