Files
IOT_SIM/runtime/Sim.Runner/ApiPolicy.cs
T
xuyueandClaude Opus 5.5 697f31e9ce 部署: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>
2026-10-22 15:40:09 +08:00

97 lines
4.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}",
};
}