hotcell-core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b90b3c31e988d71fe6f9b1da6c1e727d8c25b934c5085757367fe6e38ffd5aef
4
- data.tar.gz: d8eed61e4eeb2d52fe65571ff75b34ff0f73d37ef96c615ec5ab88b7e4d64696
3
+ metadata.gz: 8891b08ef8f6bea20b5d0046b865792da18374918fd310d1c521eda6c20b5cac
4
+ data.tar.gz: fe246492b615e044c9762b629de4082f4ac88167a1cedc1b07e13cf235611aae
5
5
  SHA512:
6
- metadata.gz: 5b8e87d2713d7d8aae5091a469f3a16be1a1dc860e37c4a0b40aa2b3819a1ee3336f7807e88cbb4ae35892f4a6ad5ad892a3c239a6f4928108ecd584946d1d19
7
- data.tar.gz: 16ea6b1a37ab997e25d4553f697a6fd390b3b4dbe6869a6b703e0bccf1a1a8467fb14e95921e19c0125e48462aecc4ac578214729573106d4f00e5c0a8ac509c
6
+ metadata.gz: 0d8e8e358a731b951f94716eec4793aca4f7595213c12b7ebbff89079eef01a205812fe87a4ddac00ed428e8a99412ee81c2bc4812efffbaeab0854da43573e3
7
+ data.tar.gz: 7b349aff2ee4ccf6e48a3d6545923285337c8b59c00016dd1416ea55f18f5a3b5b7e9786197febc5865bdc07702cf45574461af0717449984074d77e6a2dc368
@@ -47,9 +47,9 @@ module HotCell
47
47
  # updated by a deploy, so an application that ships a client for a new operation before anybody reboots
48
48
  # the cell gets `unsupported` at one hundred percent for as long as that takes.
49
49
  #
50
- # The two mistakes are not symmetrical. Retrying a caller's typo costs some work and shows up in the
51
- # `unsupported` rate and in the client's boot-time warning. Recording a deploy window as permanent
52
- # condemns every blob uploaded during it, and needs a hand-written backfill to undo.
50
+ # The two mistakes are not symmetrical. Retrying a caller's typo costs some work, and it says which
51
+ # operation in the refusal `worker.rb` writes and in the `unsupported` rate. Recording a deploy window
52
+ # as permanent condemns every blob uploaded during it, and needs a hand-written backfill to undo.
53
53
 
54
54
  # `killed` splits on what the worker hit, because a caller cannot otherwise tell a decompression
55
55
  # bomb from a slow afternoon. Size and memory are properties of the input, so the same bytes will
@@ -79,6 +79,18 @@ module HotCell
79
79
  # from somewhere it cannot see: a cgroup OOM kill chosen on aggregate pressure across concurrent workers,
80
80
  # or one worker signalling another — they share a uid, and nothing stops that. Attributing either to the
81
81
  # input this worker happened to be holding condemns a file for something it did not do.
82
+ #
83
+ # So the supervisor never infers either of them from a signal. They are decided in the worker, which is
84
+ # the process that holds the request: `memory` when it catches NoMemoryError itself, and `fsize` when a
85
+ # write of its own returns EFBIG. See Worker#disarm_file_size_signal for why the file-size verdict has
86
+ # to be earned that way rather than read off a wait status.
87
+ #
88
+ # That is a narrower guarantee than "a permanent verdict cannot be forged", and the difference matters.
89
+ # It removes the supervisor as an instrument: a sibling's signal no longer travels through a wait status
90
+ # into someone else's blob. It does nothing about a cell that has been compromised outright and answers
91
+ # a connection itself — `from_wire` believes a `permanent` boolean off the wire, and a worker that stole
92
+ # `work.sock` writes whatever it likes. That is the socket-theft residual `docs/DESIGN.md` records, and
93
+ # it is not closed here.
82
94
  PERMANENT_BY_CAUSE = {
83
95
  FSIZE => true,
84
96
  MEMORY => true,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HotCell
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -15,19 +15,24 @@ module HotCell
15
15
  class Failure
16
16
  MAX_MESSAGE_BYTES = 512
17
17
 
18
- attr_reader :code, :message, :error_class, :cause, :signal
18
+ attr_reader :code, :message, :error_class, :cause, :signal, :stderr
19
19
 
20
- # Every field is sanitized, not only the message. All five arrive from the wire on the client side, so
21
- # all five carry whatever the peer put there — and they travel further than the message does, into
20
+ # Every field is sanitized, not only the message. All of them arrive from the wire on the client side,
21
+ # so all of them carry whatever the peer put there — and they travel further than the message does, into
22
22
  # `to_s`, into the `perform.hot_cell` event, and into whatever a subscriber writes down. `code` in
23
- # particular is the field applications store. Scrubbing one and not the other four left the same
24
- # poisoned row the scrub exists to prevent, reachable through a different key.
25
- def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil)
23
+ # particular is the field applications store. Scrubbing one and not the rest left the same poisoned row
24
+ # the scrub exists to prevent, reachable through a different key.
25
+ def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil,
26
+ stderr: nil)
26
27
  @code = self.class.sanitize(code).to_s
27
28
  @cause = self.class.sanitize(cause)
28
29
  @signal = self.class.sanitize(signal)
29
30
  @error_class = self.class.sanitize(error_class)
30
31
  @message = self.class.sanitize(message)
32
+
33
+ # The tail, because this is a transcript and its last line is the one that ended the request. Every
34
+ # other field is one message, where the head is what matters.
35
+ @stderr = self.class.sanitize(stderr, keep: :tail)
31
36
  @permanent = permanent.nil? ? Codes.permanent?(@code, cause: @cause) : permanent
32
37
  end
33
38
 
@@ -40,11 +45,15 @@ module HotCell
40
45
  # `permanent` is the exception and survives, because compact drops only nil.
41
46
  def to_h
42
47
  { code: code, permanent: permanent? }
43
- .merge(cause: cause, signal: signal, class: error_class, message: message).compact
48
+ .merge(cause: cause, signal: signal, class: error_class, message: message, stderr: stderr).compact
44
49
  end
45
50
 
51
+ # `one_line` rather than raw interpolation: `to_s` becomes the exception message an application logs, and
52
+ # a transcript ends in a newline — so a peer that put newlines in one writes extra lines into that log.
53
+ # The attribute keeps the raw text.
46
54
  def to_s
47
- [ code, cause, error_class, message ].compact.join(": ")
55
+ text = [ code, cause, error_class, message ].compact.join(": ")
56
+ stderr ? "#{text} (#{self.class.one_line(stderr)})" : text
48
57
  end
49
58
 
50
59
  class << self
@@ -73,14 +82,31 @@ module HotCell
73
82
  end
74
83
 
75
84
  new code: wire[:code], permanent: permanent, cause: wire[:cause], signal: wire[:signal],
76
- error_class: wire[:class], message: wire[:message]
85
+ error_class: wire[:class], message: wire[:message], stderr: wire[:stderr]
86
+ end
87
+
88
+ # For a message that is about to be written as one line of a log. `sanitize` leaves CR and LF alone,
89
+ # which is right for a message an application stores or re-raises, but a peer that puts a newline in
90
+ # one writes a second log line of its own — formatted and indented like the real ones. Escape them,
91
+ # and the rest of the control characters with them.
92
+ def one_line(text)
93
+ sanitize(text)&.gsub(/[[:cntrl:]]/) { |character| character.dump[1..-2] }
77
94
  end
78
95
 
79
- def sanitize(message)
96
+ # `keep: :tail` is for a captured stream rather than a message, and it is load-bearing on `from_wire`:
97
+ # a cell this client does not trust can fill that field to the response limit, and head-truncating
98
+ # there hands the caller the noise a decoder printed first instead of the fatal that ended it.
99
+ def sanitize(message, keep: :head)
80
100
  return nil if message.nil?
81
101
 
82
- String(message).dup.force_encoding(Encoding::UTF_8)
83
- .scrub("").byteslice(0, MAX_MESSAGE_BYTES).scrub("")
102
+ text = String(message).dup.force_encoding(Encoding::UTF_8).scrub("")
103
+ text = if keep == :tail && text.bytesize > MAX_MESSAGE_BYTES
104
+ text.byteslice(text.bytesize - MAX_MESSAGE_BYTES, MAX_MESSAGE_BYTES)
105
+ else
106
+ text.byteslice(0, MAX_MESSAGE_BYTES)
107
+ end
108
+
109
+ text.scrub("")
84
110
  end
85
111
  end
86
112
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotcell-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -10,12 +10,9 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
- HotCell runs untrusted media conversion in an unprivileged sibling container with no network,
14
- reached over a Unix socket that carries file descriptors rather than paths or bytes.
15
-
16
- This gem holds what both sides must agree on: the request and response format, SCM_RIGHTS
17
- marshalling, descriptor access-mode verification, payload validation, and the error taxonomy.
18
- It performs no I/O of its own and loads no media library.
13
+ The wire protocol between an application and a HotCell container: the request and response format,
14
+ file descriptor passing over SCM_RIGHTS, payload validation, and the error taxonomy. Both
15
+ hotcell-client and hotcell-server depend on it. Install one of those rather than this.
19
16
  email:
20
17
  - mike@37signals.com
21
18
  executables: []
@@ -46,8 +43,8 @@ licenses:
46
43
  - MIT
47
44
  metadata:
48
45
  homepage_uri: https://github.com/basecamp/hotcell
49
- source_code_uri: https://github.com/basecamp/hotcell/tree/v0.1.0/hotcell-core
50
- changelog_uri: https://github.com/basecamp/hotcell/blob/v0.1.0/CHANGELOG.md
46
+ source_code_uri: https://github.com/basecamp/hotcell/tree/v0.3.0/hotcell-core
47
+ changelog_uri: https://github.com/basecamp/hotcell/blob/v0.3.0/CHANGELOG.md
51
48
  bug_tracker_uri: https://github.com/basecamp/hotcell/issues
52
49
  rubygems_mfa_required: 'true'
53
50
  rdoc_options: []