Skip to content

Commit

Permalink
enforce memory limit (via rlimit) for submissions
Browse files Browse the repository at this point in the history
  • Loading branch information
austrin committed Feb 6, 2017
1 parent e64048a commit c671fef
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 17 deletions.
7 changes: 6 additions & 1 deletion problemtools/run/buildrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def compile(self):
return True


def get_runcmd(self, cwd=None):
def get_runcmd(self, cwd=None, memlim=None):
"""Run command for the program.
Args:
Expand All @@ -86,3 +86,8 @@ def get_runcmd(self, cwd=None):
"""
path = self.path if cwd is None else os.path.relpath(self.path, cwd)
return [os.path.join(path, 'run')]


def should_skip_memory_rlimit(self):
"""Ugly hack (see program.py for details)."""
return True
6 changes: 5 additions & 1 deletion problemtools/run/executable.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ def compile(self):
"""
return True

def get_runcmd(self):
def get_runcmd(self, cwd=None, memlim=None):
"""Command to run the program.
"""
return [self.path] + self.args

def should_skip_memory_rlimit(self):
"""Ugly hack (see program.py for details)."""
return True
14 changes: 10 additions & 4 deletions problemtools/run/limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ def check_limit_capabilities(logger):
FIXME: if running as root with a hard stack or cpu rlimit set,
this will still issue warnings.
"""
(_, cpu_hard) = resource.getrlimit(resource.RLIMIT_CPU)
if cpu_hard != resource.RLIM_INFINITY:
logger.warning("Hard CPU rlimit of %d, runs involving higher CPU limits than this may behave incorrectly."
% cpu_hard)

(_, stack_hard) = resource.getrlimit(resource.RLIMIT_STACK)
if stack_hard != resource.RLIM_INFINITY:
logger.warning("Hard stack rlimit of %d so I can't set it to unlimited. I will keep it at %d. If you experience unexpected issues (in particular run-time errors) this may be the cause."
% (stack_hard, stack_hard))

(_, cpu_hard) = resource.getrlimit(resource.RLIMIT_CPU)
if cpu_hard != resource.RLIM_INFINITY:
logger.warning("Hard CPU rlimit of %d, runs involving higher CPU limits than this may behave incorrectly."
% cpu_hard)
(_, mem_hard) = resource.getrlimit(resource.RLIMIT_DATA)
if mem_hard != resource.RLIM_INFINITY:
logger.warning("Hard memory rlimit of %d, runs involving a higher memory limit may behave incorrectly. If you experience unexpected issues (in particular run-time errors) this may be the cause."
% (stack_hard, stack_hard))



def try_limit(limit, soft, hard):
Expand Down
34 changes: 27 additions & 7 deletions problemtools/run/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Program(object):
runtime = 0

def run(self, infile='/dev/null', outfile='/dev/null', errfile='/dev/null',
args=None, timelim=1000):
args=None, timelim=1000, memlim=1024):
"""Run the program.
Args:
Expand All @@ -24,37 +24,57 @@ def run(self, infile='/dev/null', outfile='/dev/null', errfile='/dev/null',
args (list of str): additional command-line arguments to
pass to the program
timelim (int): CPU time limit in seconds
memlim (int): memory limit in MB
Returns:
pair (status, runtime):
status (int): exit status of the process
runtime (float): user+sys runtime of the process, in seconds
"""
runcmd = self.get_runcmd()
runcmd = self.get_runcmd(memlim=memlim)
if runcmd == []:
raise ProgramError('Could not figure out how to run %s' % self)
if args is None:
args = []
if self.should_skip_memory_rlimit():
memlim = None

status, runtime = self.__run_wait(runcmd + args,
infile, outfile, errfile, timelim)
infile, outfile, errfile,
timelim, memlim)

self.runtime = max(self.runtime, runtime)

return status, runtime


def should_skip_memory_rlimit(self):
"""Ugly workaround to accommodate Java -- the JVM will crash and burn
if there is a memory rlimit applied and this will probably not
change anytime soon [time of writing this: 2017-02-05], see
e.g.: https://bugs.openjdk.java.net/browse/JDK-8071445
Subclasses of Program where the associated program is (or may
be) a Java program need to override this method and return
True (which will cause the memory rlimit to not be applied).
"""
return False


@staticmethod
def __run_wait(argv, infile='/dev/null', outfile='/dev/null',
errfile='/dev/null', timelim=1000):
def __run_wait(argv, infile, outfile, errfile, timelim, memlim):
logging.debug('run "%s < %s > %s 2> %s"',
' '.join(argv), infile, outfile, errfile)
pid = os.fork()
if pid == 0: # child
try:
if timelim is not None:
limit.try_limit(resource.RLIMIT_CPU, timelim, timelim + 1)
if memlim is not None:
limit.try_limit(resource.RLIMIT_AS, memlim * (1024**2), resource.RLIM_INFINITY)
limit.try_limit(resource.RLIMIT_STACK,
resource.RLIM_INFINITY, resource.RLIM_INFINITY)
limit.try_limit(resource.RLIMIT_CPU, timelim, timelim + 1)
Program.__setfd(0, infile, os.O_RDONLY)
Program.__setfd(1, outfile,
os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
Expand All @@ -65,7 +85,7 @@ def __run_wait(argv, infile='/dev/null', outfile='/dev/null',
print "Oops. Fatal error in child process:"
print exc
os.kill(os.getpid(), signal.SIGTERM)
#Unreachable
# Unreachable
logging.error("Unreachable part of run_wait reached")
os.kill(os.getpid(), signal.SIGTERM)
(pid, status, rusage) = os.wait4(pid, 0)
Expand Down
12 changes: 10 additions & 2 deletions problemtools/run/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,30 @@ def compile(self):
return self._compile_result


def get_runcmd(self, cwd=None):
def get_runcmd(self, cwd=None, memlim=1024):
"""Run command for the program.
Args:
cwd (str): if not None, the run command is provided
relative to cwd (otherwise absolute paths are given).
memlim (int): if not None, memory limit in MB (only
relevant for languages where memory limit is passed on
command line)
"""
self.compile()
subs = self.__get_substitution()
subs = self.__get_substitution(memlim)
if cwd is not None:
subs['path'] = os.path.relpath(subs['path'], cwd)
subs['binary'] = os.path.relpath(subs['binary'], cwd)
subs['mainfile'] = os.path.relpath(subs['mainfile'], cwd)
return shlex.split(self.language.run.format(**subs))


def should_skip_memory_rlimit(self):
"""Ugly hack (see program.py for details)."""
return self.language.name == 'Java'


def __str__(self):
"""String representation"""
return '%s (%s)' % (self.name, self.language.name)
Expand Down
6 changes: 4 additions & 2 deletions problemtools/verifyproblem.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ def run_submission(self, sub, args, timelim_low=1000, timelim_high=1000):
if self._problem.is_interactive:
res2 = self._problem.output_validators.validate_interactive(self, sub, timelim_high, self._problem.submissions)
else:
status, runtime = sub.run(self.infile, outfile, timelim=timelim_high+1)
status, runtime = sub.run(self.infile, outfile,
timelim=timelim_high+1,
memlim=self._problem.config.get('limits')['memory'])
if is_TLE(status) or runtime > timelim_high:
res2 = SubmissionResult('TLE', score=self._problem.config.get('grading')['reject_score'])
elif is_RTE(status):
Expand Down Expand Up @@ -909,7 +911,7 @@ def validate_interactive(self, testcase, submission, timelim, errorhandler):
# file descriptor, wall time lim
initargs = ['1', str(2 * timelim)]
validator_args = [testcase.infile, testcase.ansfile, '<feedbackdir>']
submission_args = submission.get_runcmd()
submission_args = submission.get_runcmd(memlim=self._problem.config.get('limits')['memory'])
for val in self._actual_validators():
if val is not None and val.compile():
feedbackdir = tempfile.mkdtemp(prefix='feedback', dir=self._problem.tmpdir)
Expand Down

0 comments on commit c671fef

Please sign in to comment.