diff options
author | Eric Wong <e@80x24.org> | 2017-03-23 00:06:08 +0000 |
---|---|---|
committer | Eric Wong <e@80x24.org> | 2017-03-23 00:06:08 +0000 |
commit | 3acb0d5d2592415e8f415aea37c929a4392142fc (patch) | |
tree | df201e152873f9fb6e4435a3aca590de593ccde6 | |
parent | 079da35629a507719a7c15324cfba4e9c5a7be4d (diff) | |
parent | d0afe48696a643a5d7400b3db1d68cfd81e8cd38 (diff) | |
download | unicorn-3acb0d5d2592415e8f415aea37c929a4392142fc.tar.gz |
* origin/worker_exec: Don't pass a block for fork when forking workers Add worker_exec configuration option
-rw-r--r-- | lib/unicorn/configurator.rb | 10 | ||||
-rw-r--r-- | lib/unicorn/http_server.rb | 77 | ||||
-rw-r--r-- | lib/unicorn/worker.rb | 5 |
3 files changed, 73 insertions, 19 deletions
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index 9f7f56f..f404aea 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -53,6 +53,7 @@ class Unicorn::Configurator server.logger.info("worker=#{worker.nr} ready") }, :pid => nil, + :worker_exec => false, :preload_app => false, :check_client_connection => false, :rewindable_input => true, @@ -239,6 +240,15 @@ class Unicorn::Configurator set[:timeout] = seconds > max ? max : seconds end + # Whether to exec in each worker process after forking. This changes the + # memory layout of each worker process, which is a security feature designed + # to defeat possible address space discovery attacks. Note that using + # worker_exec only makes sense if you are not preloading the application, + # and will result in higher memory usage. + def worker_exec(bool) + set_bool(:worker_exec, bool) + end + # sets the current number of worker_processes to +nr+. Each worker # process will serve exactly one client at a time. You can # increment or decrement this value at runtime by sending SIGTTIN diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb index 4a2ccce..3827f2e 100644 --- a/lib/unicorn/http_server.rb +++ b/lib/unicorn/http_server.rb @@ -15,7 +15,7 @@ class Unicorn::HttpServer :before_fork, :after_fork, :before_exec, :listener_opts, :preload_app, :orig_app, :config, :ready_pipe, :user - attr_writer :after_worker_exit, :after_worker_ready + attr_writer :after_worker_exit, :after_worker_ready, :worker_exec attr_reader :pid, :logger include Unicorn::SocketHelper @@ -106,6 +106,14 @@ class Unicorn::HttpServer # list of signals we care about and trap in master. @queue_sigs = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ] + + @worker_data = if worker_data = ENV['UNICORN_WORKER'] + worker_data = worker_data.split(',').map!(&:to_i) + worker_data[1] = worker_data.slice!(1..2).map do |i| + Kgio::Pipe.for_fd(i) + end + worker_data + end end # Runs the thing. Returns self so you can run join on it @@ -114,7 +122,7 @@ class Unicorn::HttpServer # this pipe is used to wake us up from select(2) in #join when signals # are trapped. See trap_deferred. @self_pipe.replace(Unicorn.pipe) - @master_pid = $$ + @master_pid = @worker_data ? Process.ppid : $$ # setup signal handlers before writing pid file in case people get # trigger happy and send signals as soon as the pid file exists. @@ -431,11 +439,7 @@ class Unicorn::HttpServer end @reexec_pid = fork do - listener_fds = {} - LISTENERS.each do |sock| - sock.close_on_exec = false - listener_fds[sock.fileno] = sock - end + listener_fds = listener_sockets ENV['UNICORN_FD'] = listener_fds.keys.join(',') Dir.chdir(START_CTX[:cwd]) cmd = [ START_CTX[0] ].concat(START_CTX[:argv]) @@ -443,12 +447,7 @@ class Unicorn::HttpServer # avoid leaking FDs we don't know about, but let before_exec # unset FD_CLOEXEC, if anything else in the app eventually # relies on FD inheritence. - (3..1024).each do |io| - next if listener_fds.include?(io) - io = IO.for_fd(io) rescue next - io.autoclose = false - io.close_on_exec = true - end + close_sockets_on_exec(listener_fds) # exec(command, hash) works in at least 1.9.1+, but will only be # required in 1.9.4/2.0.0 at earliest. @@ -460,6 +459,40 @@ class Unicorn::HttpServer proc_name 'master (old)' end + def worker_spawn(worker) + listener_fds = listener_sockets + env = {} + env['UNICORN_FD'] = listener_fds.keys.join(',') + + listener_fds[worker.to_io.fileno] = worker.to_io + listener_fds[worker.master.fileno] = worker.master + + worker_info = [worker.nr, worker.to_io.fileno, worker.master.fileno] + env['UNICORN_WORKER'] = worker_info.join(',') + + close_sockets_on_exec(listener_fds) + + Process.spawn(env, START_CTX[0], *START_CTX[:argv], listener_fds) + end + + def listener_sockets + listener_fds = {} + LISTENERS.each do |sock| + sock.close_on_exec = false + listener_fds[sock.fileno] = sock + end + listener_fds + end + + def close_sockets_on_exec(sockets) + (3..1024).each do |io| + next if sockets.include?(io) + io = IO.for_fd(io) rescue next + io.autoclose = false + io.close_on_exec = true + end + end + # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File def murder_lazy_workers next_sleep = @timeout - 1 @@ -496,19 +529,29 @@ class Unicorn::HttpServer end def spawn_missing_workers + if @worker_data + worker = Unicorn::Worker.new(*@worker_data) + after_fork_internal + worker_loop(worker) + exit + end + worker_nr = -1 until (worker_nr += 1) == @worker_processes @workers.value?(worker_nr) and next worker = Unicorn::Worker.new(worker_nr) before_fork.call(self, worker) - if pid = fork - @workers[pid] = worker - worker.atfork_parent - else + + pid = @worker_exec ? worker_spawn(worker) : fork + + unless pid after_fork_internal worker_loop(worker) exit end + + @workers[pid] = worker + worker.atfork_parent end rescue => e @logger.error(e) rescue nil diff --git a/lib/unicorn/worker.rb b/lib/unicorn/worker.rb index e22c1bf..8bbac5e 100644 --- a/lib/unicorn/worker.rb +++ b/lib/unicorn/worker.rb @@ -12,18 +12,19 @@ class Unicorn::Worker # :stopdoc: attr_accessor :nr, :switched attr_reader :to_io # IO.select-compatible + attr_reader :master PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE DROPS = [] - def initialize(nr) + def initialize(nr, pipe=nil) drop_index = nr / PER_DROP @raindrop = DROPS[drop_index] ||= Raindrops.new(PER_DROP) @offset = nr % PER_DROP @raindrop[@offset] = 0 @nr = nr @switched = false - @to_io, @master = Unicorn.pipe + @to_io, @master = pipe || Unicorn.pipe end def atfork_child # :nodoc: |