部署:bubblewrap 沙箱、编译检查、访问口令、Docker Compose
- 学生程序每次运行放进 bubblewrap:断网,只读挂载系统文件、运行时和自己的工程目录, 看不到其他进程和服务器环境变量;限制堆内存 128 MB、CPU 时间 120 秒、运行 60 分钟, nice 10,全站同时运行上限 150;启动时自检沙箱,不可用时拒绝运行 - 编译检查(SIM001):网络、反射、启动进程、调用本机代码、加载程序集、dynamic 给出中文报错 - 访问口令:cookie 保存口令哈希,按真实 IP 限制输错次数;前端口令页 - 服务端配置集中到 config.ts,可用环境变量覆盖;Bun 直接提供打包好的网页 - Dockerfile(多阶段,服务端打包成单文件)、compose.yaml(Caddy 只收 HTTP,接入 NPM 的 npm_proxy 网络)、deploy/deploy.sh(本机构建后经 ssh 传到服务器)和部署说明 - 测试:沙箱隔离、内存上限、CPU 时间上限、编译检查 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
081ac6f924
commit
697f31e9ce
25 files changed
+869
-52
No files matched your search
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Sim.Runner;
|
||||
|
||||
/// <summary>
|
||||
/// 编译时检查学生代码用到的 API:网络、启动外部程序、反射、调用本机代码等课程用不到、只会带来风险的功能,
|
||||
/// 直接给出中文报错。真正的隔离靠运行时沙箱,这里是第二道防线,也让学生看到明确的原因。
|
||||
/// </summary>
|
||||
internal static class ApiPolicy
|
||||
{
|
||||
#pragma warning disable RS2008 // 不是分析器,不需要发布跟踪
|
||||
private static readonly DiagnosticDescriptor NotAllowed = new(
|
||||
"SIM001", "平台不允许使用的功能", "平台不允许使用 {0}({1})", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true);
|
||||
#pragma warning restore RS2008
|
||||
|
||||
/// <summary>整个命名空间(含子命名空间)都不允许</summary>
|
||||
private static readonly (string Namespace, string Reason)[] DeniedNamespaces =
|
||||
{
|
||||
("System.Net", "网络访问"),
|
||||
("System.Reflection", "反射"),
|
||||
("System.Runtime.Loader", "加载程序集"),
|
||||
("System.IO.Pipes", "进程间通信"),
|
||||
("System.IO.MemoryMappedFiles", "进程间通信"),
|
||||
};
|
||||
|
||||
/// <summary>单个类型不允许(所在命名空间的其他类型可以用)</summary>
|
||||
private static readonly Dictionary<string, string> DeniedTypes = new()
|
||||
{
|
||||
["System.Diagnostics.Process"] = "启动其他程序",
|
||||
["System.Diagnostics.ProcessStartInfo"] = "启动其他程序",
|
||||
["System.AppDomain"] = "加载程序集",
|
||||
["System.Runtime.InteropServices.Marshal"] = "调用本机代码",
|
||||
["System.Runtime.InteropServices.DllImportAttribute"] = "调用本机代码",
|
||||
["System.Runtime.InteropServices.LibraryImportAttribute"] = "调用本机代码",
|
||||
["System.Runtime.InteropServices.NativeLibrary"] = "调用本机代码",
|
||||
["System.Runtime.InteropServices.NativeMemory"] = "调用本机代码",
|
||||
};
|
||||
|
||||
public static IEnumerable<Diagnostic> Check(CSharpCompilation compilation)
|
||||
{
|
||||
foreach (var tree in compilation.SyntaxTrees)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
foreach (var name in tree.GetRoot().DescendantNodes().OfType<SimpleNameSyntax>())
|
||||
{
|
||||
// A.B.C 只检查最右边的名字,避免同一处报好几次
|
||||
if (name.Parent is QualifiedNameSyntax q && q.Left == name) continue;
|
||||
// var 推断出的类型在初始化表达式里已经检查过
|
||||
if (name is IdentifierNameSyntax { IsVar: true }) continue;
|
||||
if (name.Parent is MemberAccessExpressionSyntax m && m.Expression == name && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue;
|
||||
|
||||
var symbol = model.GetSymbolInfo(name).Symbol;
|
||||
if (symbol == null) continue;
|
||||
if (Reason(symbol) is { } reason)
|
||||
{
|
||||
yield return Diagnostic.Create(NotAllowed, name.GetLocation(), Display(symbol), reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? Reason(ISymbol symbol)
|
||||
{
|
||||
if (symbol is IDynamicTypeSymbol) return "dynamic 动态调用";
|
||||
// using 指令里的命名空间不算:VS 生成的 AssemblyInfo.cs 会 using System.Runtime.InteropServices
|
||||
if (symbol is INamespaceSymbol) return null;
|
||||
var type = symbol as INamedTypeSymbol ?? symbol.ContainingType;
|
||||
// 方法的返回值、参数类型由调用处检查;这里看符号本身属于哪个类型
|
||||
for (var t = type; t != null; t = t.ContainingType)
|
||||
{
|
||||
var fullName = t.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
|
||||
if (DeniedTypes.TryGetValue(fullName, out var reason)) return reason;
|
||||
}
|
||||
if (type == null) return null;
|
||||
// AssemblyInfo.cs 里的 [assembly: AssemblyTitle(...)] 等特性可以用
|
||||
if (type.Name.EndsWith("Attribute") && type.ContainingNamespace.ToDisplayString() == "System.Reflection") return null;
|
||||
var ns = type.ContainingNamespace.ToDisplayString();
|
||||
foreach (var (denied, reason) in DeniedNamespaces)
|
||||
{
|
||||
if (ns == denied || ns.StartsWith(denied + ".")) return reason;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string Display(ISymbol symbol) => symbol switch
|
||||
{
|
||||
IDynamicTypeSymbol => "dynamic",
|
||||
INamedTypeSymbol type => type.ToDisplayString(),
|
||||
_ => $"{symbol.ContainingType.ToDisplayString()}.{symbol.Name}",
|
||||
};
|
||||
}
|
||||
@@ -55,15 +55,16 @@ internal static class StudentCompiler
|
||||
using var pdbStream = new MemoryStream();
|
||||
var emit = compilation.Emit(peStream, pdbStream,
|
||||
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||
var denied = ApiPolicy.Check(compilation).ToList();
|
||||
|
||||
var diagnostics = new JsonArray();
|
||||
foreach (var diagnostic in emit.Diagnostics
|
||||
foreach (var diagnostic in emit.Diagnostics.Concat(denied)
|
||||
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
|
||||
.OrderBy(d => d.Severity == DiagnosticSeverity.Error ? 0 : 1))
|
||||
{
|
||||
diagnostics.Add(ToJson(diagnostic));
|
||||
}
|
||||
return new Result(emit.Success, peStream.ToArray(), pdbStream.ToArray(), diagnostics);
|
||||
return new Result(emit.Success && denied.Count == 0, peStream.ToArray(), pdbStream.ToArray(), diagnostics);
|
||||
}
|
||||
|
||||
private static JsonObject ToJson(Diagnostic diagnostic)
|
||||
|
||||
Reference in new issue
Block a user