From 6fa2822d6275f210ca2a730013cde715e467b970 Mon Sep 17 00:00:00 2001 From: Fabian Gundlach Date: Sun, 18 Oct 2026 10:30:56 +0200 Subject: [PATCH 1/4] Add ability to override time/memory limits on submissions Add nullable override_time_limit and override_mem_limit columns to submissions. When set, EvaluationJob.from_submission runs the submission with these (usually larger) limits instead of the dataset's ones. --- cms/db/submission.py | 17 ++++++++++++++++- cms/grading/Job.py | 12 ++++++++++-- cmscontrib/updaters/update_from_1.5.sql | 7 +++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cms/db/submission.py b/cms/db/submission.py index f2997cb5c9..236e883adb 100644 --- a/cms/db/submission.py +++ b/cms/db/submission.py @@ -33,7 +33,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.schema import Column, ForeignKey, ForeignKeyConstraint, \ - UniqueConstraint + UniqueConstraint, CheckConstraint from sqlalchemy.types import Integer, Float, String, Unicode, DateTime, Enum, \ BigInteger @@ -108,6 +108,21 @@ class Submission(Base): default=True, ) + # Time and memory limits (in seconds and bytes) overriding the dataset's + # ones for this submission's evaluation; None to use the dataset's + # limits. + override_time_limit: float | None = Column( + Float, + CheckConstraint("override_time_limit > 0"), + nullable=True) + override_mem_limit: int | None = Column( + BigInteger, + CheckConstraint("override_mem_limit > 0", + name='submissions_override_mem_limit_check'), + CheckConstraint("MOD(override_mem_limit, 1048576) = 0", + name='submissions_override_mem_limit_check1'), + nullable=True) + @property def short_comment(self) -> str: """The first line of the comment.""" diff --git a/cms/grading/Job.py b/cms/grading/Job.py index 986bd88bf1..63386a967c 100644 --- a/cms/grading/Job.py +++ b/cms/grading/Job.py @@ -630,6 +630,14 @@ def from_submission( info = "evaluate submission %d on testcase %s" % \ (submission.id, testcase.codename) + # A submission may carry its own (usually larger) limits. + time_limit = submission.override_time_limit \ + if submission.override_time_limit is not None \ + else dataset.time_limit + memory_limit = submission.override_mem_limit \ + if submission.override_mem_limit is not None \ + else dataset.memory_limit + # dict() is required to detach the dictionary that gets added # to the Job from the control of SQLAlchemy return EvaluationJob( @@ -644,8 +652,8 @@ def from_submission( executables=dict(submission_result.executables), input=testcase.input, output=testcase.output, - time_limit=dataset.time_limit, - memory_limit=dataset.memory_limit, + time_limit=time_limit, + memory_limit=memory_limit, info=info ) diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index 57931e7718..406303bde1 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -133,4 +133,11 @@ WHERE user_test_managers.user_test_id IN ( ) AND user_test_managers.filename LIKE 'stub.%'; +-- Relaxed time and memory limits for submissions +ALTER TABLE submissions ADD COLUMN override_time_limit double precision; +ALTER TABLE submissions ADD COLUMN override_mem_limit bigint; +ALTER TABLE submissions ADD CONSTRAINT submissions_override_time_limit_check CHECK (override_time_limit > 0); +ALTER TABLE submissions ADD CONSTRAINT submissions_override_mem_limit_check CHECK (override_mem_limit > 0); +ALTER TABLE submissions ADD CONSTRAINT submissions_override_mem_limit_check1 CHECK (MOD(override_mem_limit, 1048576) = 0); + COMMIT; From e7720d4fda2d88b444e0d5d38505106c6d4b73e5 Mon Sep 17 00:00:00 2001 From: Fabian Gundlach Date: Sun, 18 Oct 2026 09:44:17 +0200 Subject: [PATCH 2/4] Admins can make submissions with special time / mem limits via the api --- cms/server/contest/handlers/api.py | 10 +++ cms/server/contest/submission/workflow.py | 24 ++++++- .../unit_tests/server/contest/api_test.py | 66 ++++++++++++++++++- docs/API.rst | 26 +++++--- 4 files changed, 114 insertions(+), 12 deletions(-) diff --git a/cms/server/contest/handlers/api.py b/cms/server/contest/handlers/api.py index 92059c86db..3eb6ff0311 100644 --- a/cms/server/contest/handlers/api.py +++ b/cms/server/contest/handlers/api.py @@ -150,6 +150,8 @@ def post(self, task_name: str): # If the submission is performed by the administrator acting on behalf # of a contestant, allow overriding. + override_time_limit = None + override_mem_limit = None if self.impersonated_by_admin: try: official = self.get_boolean_argument("override_official", official) @@ -159,6 +161,12 @@ def post(self, task_name: str): override_min_interval = self.get_boolean_argument( "override_min_interval", False ) + # Special (usually larger) limits (seconds / bytes) to evaluate + # the submission with. + override_time_str = self.get_argument("override_time_limit", "") + override_mem_str = self.get_argument("override_mem_limit", "") + override_time_limit = float(override_time_str) if override_time_str else None + override_mem_limit = int(override_mem_str) if override_mem_str else None except ValueError as err: self.json({"error": str(err)}, 400) return @@ -178,6 +186,8 @@ def post(self, task_name: str): official, override_max_number=override_max_number, override_min_interval=override_min_interval, + override_time_limit=override_time_limit, + override_mem_limit=override_mem_limit, ) self.sql_session.commit() except UnacceptableSubmission as e: diff --git a/cms/server/contest/submission/workflow.py b/cms/server/contest/submission/workflow.py index a421427eb8..70821f6b23 100644 --- a/cms/server/contest/submission/workflow.py +++ b/cms/server/contest/submission/workflow.py @@ -89,6 +89,8 @@ def accept_submission( official: bool, override_max_number: bool = False, override_min_interval: bool = False, + override_time_limit: float | None = None, + override_mem_limit: int | None = None, ) -> Submission: """Process a contestant's request to submit a submission. @@ -110,6 +112,10 @@ def accept_submission( during the analysis mode. override_max_number: skip checks for the maximum number of submissions override_min_interval: skip checks for the minimum interval between submissions + override_time_limit: if set, a time limit (in seconds) overriding the + dataset's one to evaluate this submission with + override_mem_limit: if set, a memory limit (in bytes, a multiple of 1 MiB) + overriding the dataset's one to evaluate this submission with return: the resulting submission, if all went well. @@ -157,6 +163,20 @@ def accept_submission( "after %d seconds from last submission."), task.min_submission_interval.total_seconds()) + # Check whether special time / memory limits conform to the constraints + # enforced by the database. + + if override_time_limit is not None and override_time_limit <= 0: + raise UnacceptableSubmission( + N_("Invalid relaxed time limit!"), + N_("The relaxed time limit must be positive.")) + if override_mem_limit is not None and ( + override_mem_limit <= 0 or override_mem_limit % (1024 * 1024) != 0): + raise UnacceptableSubmission( + N_("Invalid relaxed memory limit!"), + N_("The relaxed memory limit must be a positive multiple of " + "1 MiB.")) + # Process the data we received and ensure it's valid. required_codenames = set(task.submission_format) @@ -275,7 +295,9 @@ def accept_submission( task=task, participation=participation, comment=received_filenames_joined, - official=official) + official=official, + override_time_limit=override_time_limit, + override_mem_limit=override_mem_limit) sql_session.add(submission) for codename, digest in digests.items(): diff --git a/cmstestsuite/unit_tests/server/contest/api_test.py b/cmstestsuite/unit_tests/server/contest/api_test.py index f3759ef403..7e8358177e 100644 --- a/cmstestsuite/unit_tests/server/contest/api_test.py +++ b/cmstestsuite/unit_tests/server/contest/api_test.py @@ -19,10 +19,11 @@ """Unit tests for submission details API handlers.""" import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from cms import FEEDBACK_LEVEL_RESTRICTED, FEEDBACK_LEVEL_FULL from cms.server.contest.handlers.api import ( + ApiSubmitHandler, ApiSubmissionDetailsHandler, ApiSubmissionFullDetailsHandler, ) @@ -176,5 +177,68 @@ def test_impersonated_returns_full_details(self): ) +class TestApiSubmitHandler(BaseApiHandlerTest): + """Weak limits may only be set when submitting with the admin token.""" + + def make_submit_handler(self, impersonated, arguments): + handler = self.create_handler( + ApiSubmitHandler, impersonated=impersonated + ) + handler.sql_session = MagicMock() + handler.timestamp = MagicMock() + handler.request.files = {} + handler.request.arguments = arguments + return handler + + def test_weak_limits_set_when_impersonated(self): + handler = self.make_submit_handler( + True, + { + "override_time_limit": [b"3.5"], + "override_mem_limit": [b"524288000"], + }, + ) + with patch( + "cms.server.contest.handlers.api.accept_submission" + ) as accept: + accept.return_value = self.submission + handler.post("my_task") + + kwargs = accept.call_args.kwargs + self.assertEqual(kwargs["override_time_limit"], 3.5) + self.assertEqual(kwargs["override_mem_limit"], 524288000) + self.assertEqual(handler.status_code, 200) + + def test_weak_limits_ignored_for_contestants(self): + # A contestant sending the same arguments must not get them applied. + handler = self.make_submit_handler( + False, + { + "override_time_limit": [b"3.5"], + "override_mem_limit": [b"524288000"], + }, + ) + with patch( + "cms.server.contest.handlers.api.accept_submission" + ) as accept: + accept.return_value = self.submission + handler.post("my_task") + + kwargs = accept.call_args.kwargs + self.assertIsNone(kwargs["override_time_limit"]) + self.assertIsNone(kwargs["override_mem_limit"]) + self.assertEqual(handler.status_code, 200) + + def test_weak_limits_invalid_value_rejected_when_impersonated(self): + handler = self.make_submit_handler(True, {"override_time_limit": [b"abc"]}) + with patch( + "cms.server.contest.handlers.api.accept_submission" + ) as accept: + handler.post("my_task") + + self.assertEqual(handler.status_code, 400) + accept.assert_not_called() + + if __name__ == "__main__": unittest.main() diff --git a/docs/API.rst b/docs/API.rst index 85f7316a9b..da821156c5 100644 --- a/docs/API.rst +++ b/docs/API.rst @@ -186,17 +186,23 @@ restrictions: * Hidden participation is never blocked. * Requests can carry special parameters (either in the URL or in ``POST`` data) - that bypasses further restrictions when set to ``1``: + that bypasses further restrictions: - * ``override_phase_check`` lets the operation proceed regardless of contest - phase (for example, you can submit even though the contest has already - ended). + * ``override_phase_check`` set to ``1`` lets the operation proceed + regardless of contest phase (for example, you can submit even though the + contest has already ended). - * ``override_official`` (in the submit endpoint) makes the submission count - as official regardless of contest phase. + * ``override_official`` set to ``1`` (in the submit endpoint) makes the + submission count as official regardless of contest phase. - * ``override_max_number`` (in the submit endpoint) skips all checks for the - maximum number of submits. + * ``override_max_number`` set to ``1`` (in the submit endpoint) skips all + checks for the maximum number of submits. - * ``override_min_interval`` (in the submit endpoint) skips all checks for - the minimum time interval between submits. + * ``override_min_interval`` set to ``1`` (in the submit endpoint) skips all + checks for the minimum time interval between submits. + + * ``override_time_limit`` specifies an altered time limit for the submission + (floating point number, in seconds). + + * ``override_mem_limit`` specifies an altered memory limit for the submission + (int, in bytes, must be a multiple of 1MiB). From 4e77e005361148a9b0dc827200056f6c2bf4e2bd Mon Sep 17 00:00:00 2001 From: Fabian Gundlach Date: Sun, 18 Oct 2026 19:18:33 +0200 Subject: [PATCH 3/4] Link to pull request in updater --- cmscontrib/updaters/update_from_1.5.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/cmscontrib/updaters/update_from_1.5.sql b/cmscontrib/updaters/update_from_1.5.sql index 406303bde1..469efbf907 100644 --- a/cmscontrib/updaters/update_from_1.5.sql +++ b/cmscontrib/updaters/update_from_1.5.sql @@ -133,6 +133,7 @@ WHERE user_test_managers.user_test_id IN ( ) AND user_test_managers.filename LIKE 'stub.%'; +-- https://github.com/cms-dev/cms/pull/1731 -- Relaxed time and memory limits for submissions ALTER TABLE submissions ADD COLUMN override_time_limit double precision; ALTER TABLE submissions ADD COLUMN override_mem_limit bigint; From cfbba73dd38cb15688775f15a3ca7945ed95eecf Mon Sep 17 00:00:00 2001 From: Fabian Gundlach Date: Sun, 18 Oct 2026 19:22:38 +0200 Subject: [PATCH 4/4] Clarification in api docs about controller wall time limit --- docs/API.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/API.rst b/docs/API.rst index da821156c5..da55725e1d 100644 --- a/docs/API.rst +++ b/docs/API.rst @@ -202,7 +202,8 @@ restrictions: checks for the minimum time interval between submits. * ``override_time_limit`` specifies an altered time limit for the submission - (floating point number, in seconds). + (floating point number, in seconds). Note that manager / controller wall + time limits in interactive tasks are not adjusted. * ``override_mem_limit`` specifies an altered memory limit for the submission (int, in bytes, must be a multiple of 1MiB).