update isolate integrity
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
/// <summary>
|
||||
/// C++ compilation service using Isolate sandbox for security
|
||||
/// </summary>
|
||||
public class CppCompilationServiceIsolate : ICompilationService
|
||||
{
|
||||
private readonly ILogger<CppCompilationServiceIsolate> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IsolateService _isolateService;
|
||||
private readonly IsolateBoxPool _boxPool;
|
||||
|
||||
// Compilation limits (more generous than execution limits)
|
||||
private const int CompilationTimeLimitSeconds = 30;
|
||||
private const int CompilationMemoryLimitMb = 512;
|
||||
|
||||
public CppCompilationServiceIsolate(
|
||||
ILogger<CppCompilationServiceIsolate> logger,
|
||||
IConfiguration configuration,
|
||||
IsolateService isolateService,
|
||||
IsolateBoxPool boxPool)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_isolateService = isolateService;
|
||||
_boxPool = boxPool;
|
||||
}
|
||||
|
||||
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||
{
|
||||
var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp");
|
||||
var executablePath = Path.Combine(workingDirectory, "solution");
|
||||
|
||||
_logger.LogInformation("Compiling C++ code with Isolate in {WorkingDirectory} with version {Version}",
|
||||
workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
return await CompileFileInIsolateAsync(sourceFilePath, executablePath, version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Isolate compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CompilationResult> CompileFileInIsolateAsync(
|
||||
string sourceFilePath,
|
||||
string outputExecutablePath,
|
||||
string? version = null)
|
||||
{
|
||||
int boxId = -1;
|
||||
|
||||
try
|
||||
{
|
||||
// Acquire box from pool
|
||||
boxId = await _boxPool.AcquireBoxAsync();
|
||||
_logger.LogDebug("Acquired isolate box {BoxId} for compilation", boxId);
|
||||
|
||||
// Initialize box
|
||||
await _isolateService.InitBoxAsync(boxId);
|
||||
|
||||
// Copy source file to box
|
||||
var boxDir = $"/var/local/lib/isolate/{boxId}/box";
|
||||
var sourceFileName = Path.GetFileName(sourceFilePath);
|
||||
var boxSourcePath = Path.Combine(boxDir, sourceFileName);
|
||||
var boxOutputName = "solution";
|
||||
var boxOutputPath = Path.Combine(boxDir, boxOutputName);
|
||||
|
||||
File.Copy(sourceFilePath, boxSourcePath, overwrite: true);
|
||||
|
||||
// Resolve compiler and flags
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags));
|
||||
|
||||
// Build compiler arguments
|
||||
var arguments = new List<string>(compilerFlags);
|
||||
arguments.Add($"/box/{sourceFileName}");
|
||||
arguments.Add("-o");
|
||||
arguments.Add($"/box/{boxOutputName}");
|
||||
|
||||
// Prepare stderr output file for compiler messages
|
||||
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
||||
|
||||
// Run compiler in Isolate
|
||||
// Note: Isolate by default provides access to /usr, /lib, etc. via --share-net=no
|
||||
// For compilation, we need access to system headers and libraries
|
||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||
{
|
||||
BoxId = boxId,
|
||||
Executable = $"/usr/bin/{compiler}",
|
||||
Arguments = arguments.ToArray(),
|
||||
TimeLimitSeconds = CompilationTimeLimitSeconds,
|
||||
WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2,
|
||||
MemoryLimitKb = CompilationMemoryLimitMb * 1024,
|
||||
StackLimitKb = 256 * 1024,
|
||||
ProcessLimit = 10, // g++ spawns multiple processes
|
||||
EnableNetwork = false,
|
||||
StderrFile = stderrFilePath,
|
||||
WorkingDirectory = "/box",
|
||||
DirectoryBindings = new List<DirectoryBinding>
|
||||
{
|
||||
new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true },
|
||||
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
||||
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }
|
||||
}
|
||||
});
|
||||
|
||||
// Read compiler output
|
||||
var compilerOutput = string.Empty;
|
||||
if (File.Exists(stderrFilePath))
|
||||
{
|
||||
compilerOutput = await File.ReadAllTextAsync(stderrFilePath);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(isolateResult.ErrorOutput))
|
||||
{
|
||||
compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim();
|
||||
}
|
||||
|
||||
// Check for time/memory limits during compilation
|
||||
if (isolateResult.TimeLimitExceeded)
|
||||
{
|
||||
_logger.LogWarning("Compilation time limit exceeded for box {BoxId}", boxId);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
|
||||
if (isolateResult.MemoryLimitExceeded)
|
||||
{
|
||||
_logger.LogWarning("Compilation memory limit exceeded for box {BoxId}", boxId);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
|
||||
// Copy compiled executable back if successful
|
||||
if (isolateResult.ExitCode == 0 && File.Exists(boxOutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputExecutablePath)!);
|
||||
File.Copy(boxOutputPath, outputExecutablePath, overwrite: true);
|
||||
|
||||
_logger.LogInformation("Compilation successful in Isolate box {BoxId}", boxId);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = outputExecutablePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
|
||||
// Compilation failed
|
||||
_logger.LogWarning("Compilation failed with exit code {ExitCode} in box {BoxId}",
|
||||
isolateResult.ExitCode, boxId);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Isolate compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup box
|
||||
if (boxId >= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _isolateService.CleanupBoxAsync(boxId);
|
||||
_logger.LogDebug("Cleaned up isolate box {BoxId}", boxId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId);
|
||||
}
|
||||
|
||||
// Release box back to pool
|
||||
_boxPool.ReleaseBox(boxId);
|
||||
_logger.LogDebug("Released compilation box {BoxId} back to pool", boxId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, List<string> compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var defaultFlags = SplitFlags(_configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall");
|
||||
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (defaultCompiler, defaultFlags);
|
||||
}
|
||||
|
||||
var versionKey = $"Cpp:Versions:{version}";
|
||||
var versionCompiler = _configuration[$"{versionKey}:Compiler"];
|
||||
var versionFlagsValue = _configuration[$"{versionKey}:CompilerFlags"];
|
||||
|
||||
if (!string.IsNullOrEmpty(versionCompiler) || !string.IsNullOrEmpty(versionFlagsValue))
|
||||
{
|
||||
var resolvedFlags = !string.IsNullOrEmpty(versionFlagsValue)
|
||||
? SplitFlags(versionFlagsValue)
|
||||
: defaultFlags;
|
||||
|
||||
_logger.LogInformation("Using C++ version {Version} configuration", version);
|
||||
return (versionCompiler ?? defaultCompiler, resolvedFlags);
|
||||
}
|
||||
|
||||
var normalized = NormalizeCppVersion(version);
|
||||
if (normalized != null)
|
||||
{
|
||||
var flagsWithoutStd = defaultFlags
|
||||
.Where(flag => !flag.StartsWith("-std=", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
flagsWithoutStd.Add($"-std=c++{normalized}");
|
||||
_logger.LogInformation("Using inferred C++ standard c++{Standard}", normalized);
|
||||
return (defaultCompiler, flagsWithoutStd);
|
||||
}
|
||||
|
||||
_logger.LogWarning("C++ version {Version} not found in configuration, using default", version);
|
||||
return (defaultCompiler, defaultFlags);
|
||||
}
|
||||
|
||||
private static List<string> SplitFlags(string flags) =>
|
||||
flags.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
private static string? NormalizeCppVersion(string version)
|
||||
{
|
||||
var cleaned = version.Trim().ToLowerInvariant();
|
||||
cleaned = cleaned.Replace("c++", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("gnu++", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Trim('+', ' ');
|
||||
|
||||
cleaned = cleaned switch
|
||||
{
|
||||
"2b" => "23",
|
||||
"2a" => "20",
|
||||
"1z" => "17",
|
||||
"0x" => "11",
|
||||
_ => cleaned
|
||||
};
|
||||
|
||||
return cleaned switch
|
||||
{
|
||||
"26" => "26",
|
||||
"23" => "23",
|
||||
"20" => "20",
|
||||
"17" => "17",
|
||||
"14" => "14",
|
||||
"11" => "11",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user