fix
This commit is contained in:
@@ -79,6 +79,10 @@ class DispatcherBase(object):
|
|||||||
|
|
||||||
|
|
||||||
class JudgeDispatcher(DispatcherBase):
|
class JudgeDispatcher(DispatcherBase):
|
||||||
|
# 是否占用 JudgeServer 槽位。判完后只有占过槽位的才需要唤醒等待队列,
|
||||||
|
# 否则会把队首任务 pop 出来又原地退回(SQLJudgeDispatcher 在 worker 内判题,不占槽位)
|
||||||
|
uses_judge_server = True
|
||||||
|
|
||||||
def __init__(self, submission_id, problem_id):
|
def __init__(self, submission_id, problem_id):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.submission = Submission.objects.get(id=submission_id)
|
self.submission = Submission.objects.get(id=submission_id)
|
||||||
@@ -216,7 +220,8 @@ class JudgeDispatcher(DispatcherBase):
|
|||||||
else:
|
else:
|
||||||
self.update_problem_status()
|
self.update_problem_status()
|
||||||
|
|
||||||
# 至此判题结束,尝试处理任务队列中剩余的任务
|
# 至此判题结束,释放了 JudgeServer 槽位,尝试处理任务队列中剩余的任务
|
||||||
|
if self.uses_judge_server:
|
||||||
process_pending_task()
|
process_pending_task()
|
||||||
|
|
||||||
def update_problem_status_rejudge(self):
|
def update_problem_status_rejudge(self):
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class SQLProblemConfigError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class SQLJudgeDispatcher(JudgeDispatcher):
|
class SQLJudgeDispatcher(JudgeDispatcher):
|
||||||
|
# 在 worker 内用 sqlite3 判题,不经过 ChooseJudgeServer,也就没有槽位可释放
|
||||||
|
uses_judge_server = False
|
||||||
|
|
||||||
def judge(self):
|
def judge(self):
|
||||||
Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING)
|
Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING)
|
||||||
self._push_status(JudgeStatus.JUDGING, "judging")
|
self._push_status(JudgeStatus.JUDGING, "judging")
|
||||||
@@ -56,6 +59,15 @@ class SQLJudgeDispatcher(JudgeDispatcher):
|
|||||||
return
|
return
|
||||||
cases.append(case)
|
cases.append(case)
|
||||||
|
|
||||||
|
# 判题给出的中文提示(授权拒绝/超时/内存/无结果集)只存在测试点的 error_message 里,
|
||||||
|
# 前端只读 statistic_info.err_info,这里把首个失败测试点的提示提上来,否则学生看不到原因
|
||||||
|
failed = next((c for c in cases if c["result"] != JudgeStatus.ACCEPTED), None)
|
||||||
|
if failed and failed["error_message"]:
|
||||||
|
self.submission.statistic_info["err_info"] = failed["error_message"]
|
||||||
|
else:
|
||||||
|
# rejudge 时清掉上一轮的残留提示
|
||||||
|
self.submission.statistic_info.pop("err_info", None)
|
||||||
|
|
||||||
self._process_judge_result({"err": None, "data": cases})
|
self._process_judge_result({"err": None, "data": cases})
|
||||||
|
|
||||||
def _system_error(self, message):
|
def _system_error(self, message):
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
查询题(mode="query")比对最后一条 SELECT 的结果集;
|
查询题(mode="query")比对最后一条 SELECT 的结果集;
|
||||||
增删改题(mode="modify")比对执行后所有用户表的最终状态。
|
增删改题(mode="modify")比对执行后所有用户表的最终状态。
|
||||||
学生 SQL 通过 authorizer(禁 ATTACH/PRAGMA,查询题只读)、
|
学生 SQL 通过 authorizer(禁 ATTACH/PRAGMA,查询题只读)、
|
||||||
progress_handler(墙钟超时)和 max_page_count(内存上限)三重防护。
|
progress_handler(墙钟超时)和 max_page_count + SQLITE_LIMIT_LENGTH(内存上限)三重防护;
|
||||||
|
受信脚本(初始化/标准答案)不受超时和只读限制,但同样禁止 ATTACH/DETACH。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -26,6 +27,16 @@ _SYNTAX_ERROR_MARKERS = ("syntax error", "unrecognized token", "incomplete input
|
|||||||
# 两种模式都禁止的授权码:挂载外部库 / 数据库参数
|
# 两种模式都禁止的授权码:挂载外部库 / 数据库参数
|
||||||
_DENIED_ALWAYS = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH, sqlite3.SQLITE_PRAGMA}
|
_DENIED_ALWAYS = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH, sqlite3.SQLITE_PRAGMA}
|
||||||
|
|
||||||
|
# 受信脚本(初始化/标准答案)也必须禁止挂载外部库:ATTACH 能在服务器上读写任意 SQLite 文件,
|
||||||
|
# 而这些脚本在保存/预览题目时跑在 Django 请求进程里,等于把出题权限提成任意文件读写。
|
||||||
|
_TRUSTED_DENIED = {sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH}
|
||||||
|
_ATTACH_DENIED_HINT = "脚本中禁止使用 ATTACH/DETACH 挂载外部数据库"
|
||||||
|
|
||||||
|
|
||||||
|
def _trusted_authorizer(action, arg1, arg2, db_name, trigger):
|
||||||
|
return sqlite3.SQLITE_DENY if action in _TRUSTED_DENIED else sqlite3.SQLITE_OK
|
||||||
|
|
||||||
|
|
||||||
# 查询题允许的授权码(白名单外一律拒绝,防先 INSERT 伪造数据再 SELECT)
|
# 查询题允许的授权码(白名单外一律拒绝,防先 INSERT 伪造数据再 SELECT)
|
||||||
_QUERY_MODE_ALLOWED = {getattr(sqlite3, name) for name in ("SQLITE_SELECT", "SQLITE_READ", "SQLITE_FUNCTION", "SQLITE_RECURSIVE", "SQLITE_TRANSACTION") if hasattr(sqlite3, name)}
|
_QUERY_MODE_ALLOWED = {getattr(sqlite3, name) for name in ("SQLITE_SELECT", "SQLITE_READ", "SQLITE_FUNCTION", "SQLITE_RECURSIVE", "SQLITE_TRANSACTION") if hasattr(sqlite3, name)}
|
||||||
|
|
||||||
@@ -84,10 +95,17 @@ def _canonical_row(row):
|
|||||||
|
|
||||||
|
|
||||||
def _new_db(memory_limit_mb):
|
def _new_db(memory_limit_mb):
|
||||||
|
limit_mb = max(int(memory_limit_mb), 1)
|
||||||
conn = sqlite3.connect(":memory:", isolation_level=None) # autocommit,脚本行为可预期
|
conn = sqlite3.connect(":memory:", isolation_level=None) # autocommit,脚本行为可预期
|
||||||
conn.execute("PRAGMA page_size=4096")
|
conn.execute("PRAGMA page_size=4096")
|
||||||
# 4096B/页 × 256 页/MB,超限报 "database or disk is full"
|
# 4096B/页 × 256 页/MB,超限报 "database or disk is full"
|
||||||
conn.execute(f"PRAGMA max_page_count={max(int(memory_limit_mb), 1) * 256}")
|
conn.execute(f"PRAGMA max_page_count={limit_mb * 256}")
|
||||||
|
# max_page_count 只约束数据库页,管不住查询期的单值分配:默认 1GB 上限下
|
||||||
|
# 一条 SELECT hex(zeroblob(5e8)) 就能在 worker 进程里吃掉 GB 级内存且秒级返回,
|
||||||
|
# progress_handler 的指令粒度也拦不到(分配发生在单条 opcode 内)。
|
||||||
|
conn.setlimit(sqlite3.SQLITE_LIMIT_LENGTH, limit_mb * 1024 * 1024)
|
||||||
|
# 整条连接生命周期内禁止 ATTACH/DETACH(学生 SQL 由 _run_student 换上更严的 authorizer)
|
||||||
|
conn.set_authorizer(_trusted_authorizer)
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +140,16 @@ def _dump_tables(conn):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def _trusted_error_text(e):
|
||||||
|
"""受信脚本的 sqlite 错误转出题人能看懂的提示。"""
|
||||||
|
msg = str(e)
|
||||||
|
if "not authorized" in msg or "prohibited" in msg:
|
||||||
|
return _ATTACH_DENIED_HINT
|
||||||
|
if "interrupted" in msg:
|
||||||
|
return "超时"
|
||||||
|
return _truncate(msg)
|
||||||
|
|
||||||
|
|
||||||
def _execute_trusted(conn, script, deadline, error_prefix):
|
def _execute_trusted(conn, script, deadline, error_prefix):
|
||||||
"""执行受信脚本,任何失败都是出题问题 → SYSTEM_ERROR。"""
|
"""执行受信脚本,任何失败都是出题问题 → SYSTEM_ERROR。"""
|
||||||
conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
|
conn.set_progress_handler(lambda: 1 if time.monotonic() > deadline else 0, PROGRESS_STEP)
|
||||||
@@ -130,7 +158,7 @@ def _execute_trusted(conn, script, deadline, error_prefix):
|
|||||||
except SQLCaseError as e:
|
except SQLCaseError as e:
|
||||||
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {e.message}")
|
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {e.message}")
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {_truncate(e)}")
|
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"{error_prefix}: {_trusted_error_text(e)}")
|
||||||
finally:
|
finally:
|
||||||
conn.set_progress_handler(None, PROGRESS_STEP)
|
conn.set_progress_handler(None, PROGRESS_STEP)
|
||||||
|
|
||||||
@@ -159,6 +187,9 @@ def _run_student(conn, script, mode, deadline):
|
|||||||
raise SQLCaseError(JudgeStatus.CPU_TIME_LIMIT_EXCEEDED, "SQL 执行超时")
|
raise SQLCaseError(JudgeStatus.CPU_TIME_LIMIT_EXCEEDED, "SQL 执行超时")
|
||||||
if "database or disk is full" in msg:
|
if "database or disk is full" in msg:
|
||||||
raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "数据量超出内存限制")
|
raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "数据量超出内存限制")
|
||||||
|
# SQLITE_LIMIT_LENGTH 触顶,如 zeroblob/group_concat 构造出的超大单值
|
||||||
|
if "too big" in msg:
|
||||||
|
raise SQLCaseError(JudgeStatus.MEMORY_LIMIT_EXCEEDED, "单个数据值超出内存限制")
|
||||||
if "not authorized" in msg or "prohibited" in msg:
|
if "not authorized" in msg or "prohibited" in msg:
|
||||||
hint = denied_hints[-1] if denied_hints else "本题禁止使用该语句"
|
hint = denied_hints[-1] if denied_hints else "本题禁止使用该语句"
|
||||||
raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, hint)
|
raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, hint)
|
||||||
@@ -167,7 +198,8 @@ def _run_student(conn, script, mode, deadline):
|
|||||||
raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, _truncate(msg))
|
raise SQLCaseError(JudgeStatus.RUNTIME_ERROR, _truncate(msg))
|
||||||
finally:
|
finally:
|
||||||
conn.set_progress_handler(None, PROGRESS_STEP)
|
conn.set_progress_handler(None, PROGRESS_STEP)
|
||||||
conn.set_authorizer(None)
|
# 还原连接级的 ATTACH/DETACH 防护,而不是彻底放开
|
||||||
|
conn.set_authorizer(_trusted_authorizer)
|
||||||
|
|
||||||
if mode == "query":
|
if mode == "query":
|
||||||
return last_result
|
return last_result
|
||||||
@@ -322,8 +354,7 @@ def build_display(init_sql, ref_sql, mode, *, memory_limit_mb=64):
|
|||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
msg = "超时" if "interrupted" in str(e) else _truncate(e)
|
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案执行失败: {_trusted_error_text(e)}")
|
||||||
raise SQLCaseError(JudgeStatus.SYSTEM_ERROR, f"标准答案执行失败: {msg}")
|
|
||||||
finally:
|
finally:
|
||||||
conn.set_progress_handler(None, PROGRESS_STEP)
|
conn.set_progress_handler(None, PROGRESS_STEP)
|
||||||
if expected is None:
|
if expected is None:
|
||||||
|
|||||||
@@ -676,8 +676,8 @@ class TopACTrendAPI(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class SQLTestCasePreviewAPI(APIView):
|
class SQLTestCasePreviewAPI(APIView):
|
||||||
@validate_serializer(SQLTestCasePreviewSerializer)
|
|
||||||
@problem_permission_required
|
@problem_permission_required
|
||||||
|
@validate_serializer(SQLTestCasePreviewSerializer)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user