From 881863d8a23c9223e93681cd3215bb0937468e51 Mon Sep 17 00:00:00 2001 From: Christian Beer Date: Tue, 3 Nov 2015 15:36:21 +0100 Subject: [PATCH 1/3] Server: fix behaviour of start script If the command of a task or daemon wants to use shell features like |, > or < the start script uses a shell encapsulation (sh -c) to start the process. This had two problems: 1. It also started a shell if the command contained ' or " and didn't check if |, > or < where escaped or used within quotes (e.g. as part of a regular expression). The new mechanism uses the python module shlex to prepare the arguments for the execvp() call. It also detects if a shell encapsulation is needed and informs the user about it. 2. The actual daemon or task is a subprocess of the shell and was not terminated with the parent. The new signal propagation mechanism properly kills the daemon or task if the shell receives a signal to do so (e.g. by stop). --- sched/start | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/sched/start b/sched/start index 03d9eeedab..c0178cab10 100755 --- a/sched/start +++ b/sched/start @@ -79,7 +79,7 @@ Both: import boinc_path_config from Boinc import boinc_project_path, configxml -import sys, os, getopt, time, glob, fcntl, signal, socket, getpass +import sys, os, getopt, time, glob, fcntl, signal, socket, getpass, shlex right_now = int(time.time()) verbose = os.isatty(sys.stdout.fileno()) @@ -318,17 +318,39 @@ def is_lock_file_locked(filename): else: os.unlink(filename) +# if a command contains a pipe or a redirection, exec won't work +# this detects those cases and a shell encapsulation can be used def contains_shell_characters(command): - return ('"' in command or "'" in command or - '\\' in command or '|' in command or - '>' in command) + for item in shlex.split(command): + if item == "|": + return True + if item == ">" or item == ">>" or item == "<": + return True + if item.startswith("1>") or item.startswith("2>") or item.startswith("&>"): + return True + return False + +# if a line ends with a \ it escapes the newline witch then +# is in front of the first argument of the next line where it needs to be cleaned +# this enables the use of multiline shell commands within +def strip_leading_escapes(string): + if string.startswith("\n"): + return string[1:] + return string + +def command_string_to_list(command): + l = shlex.split(command) + return map(strip_leading_escapes, l) def exec_command_string(command): - args = command.strip().split() + args = command_string_to_list(command) os.chdir(tmp_dir) try: if contains_shell_characters(command): - os.execl('/bin/sh', 'sh', '-c', ' '.join(args)) + # sends a TERM signal to the child processes + # if either of INT, QUIT, HUP or TERM is received by the parent + command = "trap \"kill 0\" INT QUIT HUP TERM; "+command+"& wait" + os.execl('/bin/sh', 'sh', '-c', command) else: os.execvp( args[0], args ) # on success we don't reach here @@ -400,6 +422,8 @@ def run_task(task): if verbose: print >>sys.stderr, " Task currently running! (%s)"%task.cmd sys.exit(0) + if contains_shell_characters(task.cmd): + print >>sys.stderr, " Using shell encapsulation for: ",task.cmd redirect(get_task_output_name(task)) exec_command_string(task.cmd) @@ -408,11 +432,13 @@ def run_daemon(task): if double_fork() > 0: return if lock_file(get_task_lock_name(task)): if verbose: - print >>sys.stderr, " Daemon already running:",task.cmd + print >>sys.stderr, " Daemon already running: ",task.cmd sys.exit(0) if verbose or ( verbose_daemon_run and not get_daemon_silent_start(task) ): print " Starting daemon:", task.cmd sys.stdout.flush() + if contains_shell_characters(task.cmd): + print >>sys.stderr, " Using shell encapsulation for: ",task.cmd redirect(get_daemon_output_name(task)) write_pid_file(get_daemon_pid_name(task)) print "[%s] Executing command:"%timestamp(), task.cmd From ff892afa23f3b59bb6de53cb72bf6f91219a20c5 Mon Sep 17 00:00:00 2001 From: Christian Beer Date: Tue, 3 Nov 2015 18:06:46 +0100 Subject: [PATCH 2/3] Server: fix comment from last commit --- sched/start | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sched/start b/sched/start index c0178cab10..d32c0a3176 100755 --- a/sched/start +++ b/sched/start @@ -330,7 +330,7 @@ def contains_shell_characters(command): return True return False -# if a line ends with a \ it escapes the newline witch then +# if a line ends with a \ it escapes the newline which then # is in front of the first argument of the next line where it needs to be cleaned # this enables the use of multiline shell commands within def strip_leading_escapes(string): From 69c04cca8ced17b0d2a3f6d27beee3af7b7e4bd9 Mon Sep 17 00:00:00 2001 From: Christian Beer Date: Tue, 10 Nov 2015 10:16:30 +0100 Subject: [PATCH 3/3] Server: enhance shell encapsulation of daemons and tasks - if a daemon or task should run in a shell, add 1 to the task entry in config.xml this will spawn a "sh -c cmd" process that propagates signals to the child process (see 881863d) - if a daemon or task has to use a shell (pipe or redirection present in cmd) and is not enabled: don't execute the cmd and print an error message (other daemons and tasks are still started) --- sched/start | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/sched/start b/sched/start index d32c0a3176..aeefc15d46 100755 --- a/sched/start +++ b/sched/start @@ -160,6 +160,13 @@ def get_task_output_name(task): return os.path.join(log_dir, task.__dict__.get('output') or get_task_command_basename(task) + '.out') +def get_task_use_shell(task): + use_shell = task.__dict__.get('use_shell') + if use_shell and use_shell != "0": + return 1 + else: + return 0 + def get_daemon_output_name(task): return os.path.join(log_dir, task.__dict__.get('output') or get_task_command_basename(task) + '.log') @@ -342,11 +349,11 @@ def command_string_to_list(command): l = shlex.split(command) return map(strip_leading_escapes, l) -def exec_command_string(command): +def exec_command_string(command, use_shell): args = command_string_to_list(command) os.chdir(tmp_dir) try: - if contains_shell_characters(command): + if use_shell: # sends a TERM signal to the child processes # if either of INT, QUIT, HUP or TERM is received by the parent command = "trap \"kill 0\" INT QUIT HUP TERM; "+command+"& wait" @@ -422,10 +429,13 @@ def run_task(task): if verbose: print >>sys.stderr, " Task currently running! (%s)"%task.cmd sys.exit(0) - if contains_shell_characters(task.cmd): + if get_task_use_shell(task): print >>sys.stderr, " Using shell encapsulation for: ",task.cmd + elif contains_shell_characters(task.cmd): + print >>sys.stderr, " Couldn't start: ",task.cmd, " is required but was not specified" + sys.exit(1) redirect(get_task_output_name(task)) - exec_command_string(task.cmd) + exec_command_string(task.cmd, get_task_use_shell(task)) def run_daemon(task): '''Double-fork and exec command with stdout/err redirection and pid writing''' @@ -437,13 +447,16 @@ def run_daemon(task): if verbose or ( verbose_daemon_run and not get_daemon_silent_start(task) ): print " Starting daemon:", task.cmd sys.stdout.flush() - if contains_shell_characters(task.cmd): + if get_task_use_shell(task): print >>sys.stderr, " Using shell encapsulation for: ",task.cmd + elif contains_shell_characters(task.cmd): + print >>sys.stderr, " Couldn't start: ",task.cmd, " is required but was not specified" + sys.exit(1) redirect(get_daemon_output_name(task)) write_pid_file(get_daemon_pid_name(task)) print "[%s] Executing command:"%timestamp(), task.cmd sys.stdout.flush() - exec_command_string(task.cmd) + exec_command_string(task.cmd, get_task_use_shell(task)) def run_daemons(): found_any = False