bootsnap 1.18.6 → 1.26.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.
@@ -11,7 +11,6 @@
11
11
  * here.
12
12
  */
13
13
 
14
- #include "bootsnap.h"
15
14
  #include "ruby.h"
16
15
  #include <stdint.h>
17
16
  #include <stdbool.h>
@@ -20,10 +19,15 @@
20
19
  #include <fcntl.h>
21
20
  #include <unistd.h>
22
21
  #include <sys/stat.h>
22
+ #include <dirent.h>
23
+
24
+ #ifndef RBIMPL_ATTR_NORETURN
25
+ #define RBIMPL_ATTR_NORETURN()
26
+ #endif
23
27
 
24
28
  #ifdef __APPLE__
25
29
  // The symbol is present, however not in the headers
26
- // See: https://github.com/Shopify/bootsnap/issues/470
30
+ // See: https://github.com/rails/bootsnap/issues/470
27
31
  extern int fdatasync(int);
28
32
  #endif
29
33
 
@@ -36,7 +40,7 @@
36
40
  #define MAX_CACHEPATH_SIZE 1000
37
41
  #define MAX_CACHEDIR_SIZE 981
38
42
 
39
- #define KEY_SIZE 64
43
+ #define KEY_SIZE 32
40
44
 
41
45
  #define MAX_CREATE_TEMPFILE_ATTEMPT 3
42
46
 
@@ -45,34 +49,27 @@
45
49
  #endif
46
50
 
47
51
  /*
48
- * An instance of this key is written as the first 64 bytes of each cache file.
52
+ * An instance of this key is written as the first `KEY_SIZE` bytes of each cache file.
49
53
  * The mtime and size members track whether the file contents have changed, and
50
- * the version, ruby_platform, compile_option, and ruby_revision members track
51
- * changes to the environment that could invalidate compile results without
54
+ * the ruby_version_digest (bootsnap_cache_version + RUBY_DESCRIPTION) and compile_option
55
+ * members track changes to the environment that could invalidate compile results without
52
56
  * file contents having changed. The data_size member is not truly part of the
53
57
  * "key". Really, this could be called a "header" with the first six members
54
58
  * being an embedded "key" struct and an additional data_size member.
55
59
  *
56
60
  * The data_size indicates the remaining number of bytes in the cache file
57
61
  * after the header (the size of the cached artifact).
58
- *
59
- * After data_size, the struct is padded to 64 bytes.
60
62
  */
61
63
  struct bs_cache_key {
62
- uint32_t version;
63
- uint32_t ruby_platform;
64
- uint32_t compile_option;
65
- uint32_t ruby_revision;
66
- uint64_t size;
64
+ uint64_t ruby_version_digest;
67
65
  uint64_t mtime;
68
- uint64_t data_size; //
69
66
  uint64_t digest;
70
- uint8_t digest_set;
71
- uint8_t pad[15];
67
+ uint32_t size;
68
+ uint32_t data_size;
72
69
  } __attribute__((packed));
73
70
 
74
71
  /*
75
- * If the struct padding isn't correct to pad the key to 64 bytes, refuse to
72
+ * If the struct padding isn't correct to pad the key to `KEY_SIZE` bytes, refuse to
76
73
  * compile.
77
74
  */
78
75
  #define STATIC_ASSERT(X) STATIC_ASSERT2(X,__LINE__)
@@ -82,21 +79,20 @@ struct bs_cache_key {
82
79
  STATIC_ASSERT(sizeof(struct bs_cache_key) == KEY_SIZE);
83
80
 
84
81
  /* Effectively a schema version. Bumping invalidates all previous caches */
85
- static const uint32_t current_version = 6;
86
-
87
- /* hash of e.g. "x86_64-darwin17", invalidating when ruby is recompiled on a
88
- * new OS ABI, etc. */
89
- static uint32_t current_ruby_platform;
90
- /* Invalidates cache when switching ruby versions */
91
- static uint32_t current_ruby_revision;
92
- /* Invalidates cache when RubyVM::InstructionSequence.compile_option changes */
93
- static uint32_t current_compile_option_crc32 = 0;
82
+ static const uint32_t bootsnap_cache_version = 8;
83
+
84
+ /* Invalidates cache when switching ruby version, platform or ABI */
85
+ static uint64_t base_ruby_version_digest = 0;
86
+
87
+ // `base_ruby_version_digest` combined with RubyVM::InstructionSequence.compile_option
88
+ // Invalidates cache when RubyVM::InstructionSequence.compile_option changes
89
+ static uint64_t current_ruby_version_digest = 0;
90
+
94
91
  /* Current umask */
95
92
  static mode_t current_umask;
96
93
 
97
94
  /* Bootsnap::CompileCache::{Native, Uncompilable} */
98
95
  static VALUE rb_mBootsnap;
99
- static VALUE rb_mBootsnap_CompileCache;
100
96
  static VALUE rb_mBootsnap_CompileCache_Native;
101
97
  static VALUE rb_cBootsnap_CompileCache_UNCOMPILABLE;
102
98
  static ID instrumentation_method;
@@ -106,13 +102,15 @@ static bool readonly = false;
106
102
  static bool revalidation = false;
107
103
  static bool perm_issue = false;
108
104
 
105
+ static ID id_storage_to_output, id_input_to_output, id_input_to_storage;
106
+
109
107
  /* Functions exposed as module functions on Bootsnap::CompileCache::Native */
110
108
  static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
111
109
  static VALUE bs_readonly_set(VALUE self, VALUE enabled);
112
110
  static VALUE bs_revalidation_set(VALUE self, VALUE enabled);
113
111
  static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
114
- static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
115
- static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
112
+ static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args);
113
+ static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler);
116
114
 
117
115
  /* Helpers */
118
116
  enum cache_status {
@@ -120,7 +118,7 @@ enum cache_status {
120
118
  hit,
121
119
  stale,
122
120
  };
123
- static void bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE]);
121
+ static void bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE]);
124
122
  static int bs_read_key(int fd, struct bs_cache_key * key);
125
123
  static enum cache_status cache_key_equal_fast_path(struct bs_cache_key * k1, struct bs_cache_key * k2);
126
124
  static int cache_key_equal_slow_path(struct bs_cache_key * current_key, struct bs_cache_key * cached_key, const VALUE input_data);
@@ -131,8 +129,7 @@ static VALUE bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handle
131
129
  static VALUE bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler);
132
130
  static int open_current_file(const char * path, struct bs_cache_key * key, const char ** errno_provenance);
133
131
  static int fetch_cached_data(int fd, ssize_t data_size, VALUE handler, VALUE args, VALUE * output_data, int * exception_tag, const char ** errno_provenance);
134
- static uint32_t get_ruby_revision(void);
135
- static uint32_t get_ruby_platform(void);
132
+ static uint64_t get_ruby_version_digest(void);
136
133
 
137
134
  /*
138
135
  * Helper functions to call ruby methods on handler object without crashing on
@@ -140,7 +137,7 @@ static uint32_t get_ruby_platform(void);
140
137
  */
141
138
  static int bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * output_data);
142
139
  static VALUE prot_input_to_output(VALUE arg);
143
- static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag);
140
+ static void bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * output_data, int * exception_tag);
144
141
  static int bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data);
145
142
  struct s2o_data;
146
143
  struct i2o_data;
@@ -152,6 +149,114 @@ bs_rb_get_path(VALUE self, VALUE fname)
152
149
  return rb_get_path(fname);
153
150
  }
154
151
 
152
+ #ifdef HAVE_FSTATAT
153
+
154
+ RBIMPL_ATTR_NORETURN()
155
+ static void
156
+ bs_syserr_fail_path(const char *func_name, int n, VALUE path)
157
+ {
158
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s", func_name, RSTRING_PTR(path)));
159
+ }
160
+
161
+ RBIMPL_ATTR_NORETURN()
162
+ static void
163
+ bs_syserr_fail_dir_entry(const char *func_name, int n, VALUE dir, const char *d_name)
164
+ {
165
+ rb_syserr_fail_str(n, rb_sprintf("%s @ %s/%s", func_name, RSTRING_PTR(dir), d_name));
166
+ }
167
+
168
+ static VALUE
169
+ bs_rb_scan_dir(VALUE self, VALUE abspath)
170
+ {
171
+ Check_Type(abspath, T_STRING);
172
+
173
+ VALUE dirs = rb_ary_new();
174
+ VALUE requirables = rb_ary_new();
175
+ VALUE result = rb_ary_new_from_args(2, requirables, dirs);
176
+
177
+ DIR *dirp = opendir(RSTRING_PTR(abspath));
178
+ if (dirp == NULL) {
179
+ if (errno == ENOTDIR || errno == ENOENT) {
180
+ return result;
181
+ }
182
+
183
+ bs_syserr_fail_path("opendir", errno, abspath);
184
+ return Qundef;
185
+ }
186
+
187
+ struct dirent *entry;
188
+ struct stat st;
189
+ int dfd = -1;
190
+
191
+ while (1) {
192
+ errno = 0;
193
+
194
+ entry = readdir(dirp);
195
+ if (entry == NULL) break;
196
+
197
+ if (entry->d_name[0] == '.') continue;
198
+
199
+ if (RB_UNLIKELY(entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)) {
200
+ // Note: the original implementation of LoadPathCache did follow symlink.
201
+ // So this is replicated here, but I'm not sure it's a good idea.
202
+ if (dfd < 0) {
203
+ dfd = dirfd(dirp);
204
+ if (dfd < 0) {
205
+ int err = errno;
206
+ closedir(dirp);
207
+ bs_syserr_fail_path("dirfd", err, abspath);
208
+ return Qundef;
209
+ }
210
+ }
211
+
212
+ if (fstatat(dfd, entry->d_name, &st, 0)) {
213
+ if (errno == ENOENT) continue; // Broken symlink
214
+
215
+ int err = errno;
216
+ closedir(dirp);
217
+ bs_syserr_fail_dir_entry("fstatat", err, abspath, entry->d_name);
218
+ return Qundef;
219
+ }
220
+
221
+ if (S_ISREG(st.st_mode)) {
222
+ entry->d_type = DT_REG;
223
+ } else if (S_ISDIR(st.st_mode)) {
224
+ entry->d_type = DT_DIR;
225
+ }
226
+ }
227
+
228
+ if (entry->d_type == DT_DIR) {
229
+ rb_ary_push(dirs, rb_utf8_str_new_cstr(entry->d_name));
230
+ continue;
231
+ } else if (entry->d_type == DT_REG) {
232
+ size_t len = strlen(entry->d_name);
233
+ bool is_requirable = (
234
+ // Comparing 4B allows compiler to optimize this into a single 32b integer comparison.
235
+ (len > 3 && memcmp(entry->d_name + (len - 3), ".rb", 4) == 0)
236
+ || (len > DLEXT_MAXLEN && memcmp(entry->d_name + (len - DLEXT_MAXLEN), DLEXT, DLEXT_MAXLEN + 1) == 0)
237
+ #ifdef DLEXT2
238
+ || (len > DLEXT2_MAXLEN && memcmp(entry->d_name + (len - DLEXT2_MAXLEN), DLEXT2, DLEXT2_MAXLEN + 1) == 0)
239
+ #endif
240
+ );
241
+ if (is_requirable) {
242
+ rb_ary_push(requirables, rb_utf8_str_new(entry->d_name, len));
243
+ }
244
+ }
245
+ }
246
+
247
+ if (errno) {
248
+ int err = errno;
249
+ closedir(dirp);
250
+ bs_syserr_fail_path("readdir", err, abspath);
251
+ } else if (closedir(dirp)) {
252
+ bs_syserr_fail_path("closedir", errno, abspath);
253
+ return Qundef;
254
+ }
255
+
256
+ return result;
257
+ }
258
+ #endif
259
+
155
260
  /*
156
261
  * Ruby C extensions are initialized by calling Init_<extname>.
157
262
  *
@@ -164,15 +269,25 @@ Init_bootsnap(void)
164
269
  {
165
270
  rb_mBootsnap = rb_define_module("Bootsnap");
166
271
 
272
+ id_storage_to_output = rb_intern("storage_to_output");
273
+ id_input_to_output = rb_intern("input_to_output");
274
+ id_input_to_storage = rb_intern("input_to_storage");
275
+
167
276
  rb_define_singleton_method(rb_mBootsnap, "rb_get_path", bs_rb_get_path, 1);
168
277
 
169
- rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
278
+ #ifdef HAVE_FSTATAT
279
+ VALUE rb_mBootsnap_LoadPathCache = rb_define_module_under(rb_mBootsnap, "LoadPathCache");
280
+ VALUE rb_mBootsnap_LoadPathCache_Native = rb_define_module_under(rb_mBootsnap_LoadPathCache, "Native");
281
+
282
+ rb_define_singleton_method(rb_mBootsnap_LoadPathCache_Native, "scan_dir", bs_rb_scan_dir, 1);
283
+ #endif
284
+
285
+ VALUE rb_mBootsnap_CompileCache = rb_define_module_under(rb_mBootsnap, "CompileCache");
170
286
  rb_mBootsnap_CompileCache_Native = rb_define_module_under(rb_mBootsnap_CompileCache, "Native");
171
287
  rb_cBootsnap_CompileCache_UNCOMPILABLE = rb_const_get(rb_mBootsnap_CompileCache, rb_intern("UNCOMPILABLE"));
172
288
  rb_global_variable(&rb_cBootsnap_CompileCache_UNCOMPILABLE);
173
289
 
174
- current_ruby_revision = get_ruby_revision();
175
- current_ruby_platform = get_ruby_platform();
290
+ base_ruby_version_digest = get_ruby_version_digest();
176
291
 
177
292
  instrumentation_method = rb_intern("_instrument");
178
293
 
@@ -184,8 +299,8 @@ Init_bootsnap(void)
184
299
  rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
185
300
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
186
301
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "revalidation=", bs_revalidation_set, 1);
187
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
188
- rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
302
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 5);
303
+ rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 4);
189
304
  rb_define_module_function(rb_mBootsnap_CompileCache_Native, "compile_option_crc32=", bs_compile_option_crc32_set, 1);
190
305
 
191
306
  current_umask = umask(0777);
@@ -203,7 +318,13 @@ static inline void
203
318
  bs_instrumentation(VALUE event, VALUE path)
204
319
  {
205
320
  if (RB_UNLIKELY(instrumentation_enabled)) {
321
+ // If the funcall raises and rescues an error we will lose the errinfo
322
+ // that exists now so we save it before and restore it after.
323
+ VALUE saved_errinfo = rb_errinfo();
206
324
  rb_funcall(rb_mBootsnap, instrumentation_method, 2, event, path);
325
+ // If the ruby code in instrumentation_method raised we won't reach this point.
326
+ // If it didn't raise it _might_ have rescued so we should restore errinfo.
327
+ rb_set_errinfo(saved_errinfo);
207
328
  }
208
329
  }
209
330
 
@@ -221,28 +342,10 @@ bs_revalidation_set(VALUE self, VALUE enabled)
221
342
  return enabled;
222
343
  }
223
344
 
224
- /*
225
- * Bootsnap's ruby code registers a hook that notifies us via this function
226
- * when compile_option changes. These changes invalidate all existing caches.
227
- *
228
- * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but
229
- * can be represented by a uint.
230
- */
231
- static VALUE
232
- bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
233
- {
234
- if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) {
235
- Check_Type(crc32_v, T_FIXNUM);
236
- }
237
- current_compile_option_crc32 = NUM2UINT(crc32_v);
238
- return Qnil;
239
- }
240
-
241
345
  static uint64_t
242
- fnv1a_64_iter(uint64_t h, const VALUE str)
346
+ fnv1a_64_iter(uint64_t h, const unsigned char *s, size_t len)
243
347
  {
244
- unsigned char *s = (unsigned char *)RSTRING_PTR(str);
245
- unsigned char *str_end = (unsigned char *)RSTRING_PTR(str) + RSTRING_LEN(str);
348
+ const unsigned char *str_end = s + len;
246
349
 
247
350
  while (s < str_end) {
248
351
  h ^= (uint64_t)*s++;
@@ -253,45 +356,43 @@ fnv1a_64_iter(uint64_t h, const VALUE str)
253
356
  }
254
357
 
255
358
  static uint64_t
256
- fnv1a_64(const VALUE str)
359
+ fnv1a_64_iter_str(uint64_t h, const VALUE str)
360
+ {
361
+ Check_Type(str, T_STRING);
362
+ return fnv1a_64_iter(h, (unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str));
363
+ }
364
+
365
+ static uint64_t
366
+ fnv1a_64_str(const VALUE str)
257
367
  {
258
368
  uint64_t h = (uint64_t)0xcbf29ce484222325ULL;
259
- return fnv1a_64_iter(h, str);
369
+ return fnv1a_64_iter_str(h, str);
260
370
  }
261
371
 
262
372
  /*
263
- * Ruby's revision may be Integer or String. CRuby 2.7 or later uses
264
- * Git commit ID as revision. It's String.
373
+ * Bootsnap's ruby code registers a hook that notifies us via this function
374
+ * when compile_option changes. These changes invalidate all existing caches.
375
+ *
376
+ * Note that on 32-bit platforms, a CRC32 can't be represented in a Fixnum, but
377
+ * can be represented by a uint.
265
378
  */
266
- static uint32_t
267
- get_ruby_revision(void)
379
+ static VALUE
380
+ bs_compile_option_crc32_set(VALUE self, VALUE crc32_v)
268
381
  {
269
- VALUE ruby_revision;
270
-
271
- ruby_revision = rb_const_get(rb_cObject, rb_intern("RUBY_REVISION"));
272
- if (RB_TYPE_P(ruby_revision, RUBY_T_FIXNUM)) {
273
- return FIX2INT(ruby_revision);
274
- } else {
275
- uint64_t hash;
276
-
277
- hash = fnv1a_64(ruby_revision);
278
- return (uint32_t)(hash >> 32);
382
+ if (!RB_TYPE_P(crc32_v, T_BIGNUM) && !RB_TYPE_P(crc32_v, T_FIXNUM)) {
383
+ Check_Type(crc32_v, T_FIXNUM);
279
384
  }
385
+ uint32_t crc32 = (uint32_t)NUM2UINT(crc32_v);
386
+ current_ruby_version_digest = fnv1a_64_iter(base_ruby_version_digest, (unsigned char *)&crc32, sizeof(crc32));
387
+ return Qnil;
280
388
  }
281
389
 
282
- /*
283
- * When ruby's version doesn't change, but it's recompiled on a different OS
284
- * (or OS version), we need to invalidate the cache.
285
- */
286
- static uint32_t
287
- get_ruby_platform(void)
390
+ static uint64_t
391
+ get_ruby_version_digest(void)
288
392
  {
289
- uint64_t hash;
290
- VALUE ruby_platform;
291
-
292
- ruby_platform = rb_const_get(rb_cObject, rb_intern("RUBY_PLATFORM"));
293
- hash = fnv1a_64(ruby_platform);
294
- return (uint32_t)(hash >> 32);
393
+ uint64_t hash = fnv1a_64_str(rb_const_get(rb_mBootsnap, rb_intern("RUBY_CACHE_KEY")));
394
+ hash = fnv1a_64_iter(hash, (unsigned char *)&bootsnap_cache_version, sizeof(bootsnap_cache_version));
395
+ return hash;
295
396
  }
296
397
 
297
398
  /*
@@ -300,15 +401,35 @@ get_ruby_platform(void)
300
401
  * be stored.
301
402
  *
302
403
  * The path will look something like: <cachedir>/12/34567890abcdef
404
+ *
405
+ * `path_v` must already have been through FilePathValue(): the callers read it
406
+ * with RSTRING_PTR() too, so the conversion has to happen in the frame that
407
+ * owns the variable.
303
408
  */
304
409
  static void
305
- bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_CACHEPATH_SIZE])
410
+ bs_cache_path(VALUE cachedir_v, VALUE namespace_v, VALUE path_v, char (* cache_path)[MAX_CACHEPATH_SIZE])
306
411
  {
307
- uint64_t hash = fnv1a_64(path);
412
+ Check_Type(cachedir_v, T_STRING);
413
+ Check_Type(path_v, T_STRING);
414
+
415
+ long namespace_len = 0;
416
+ if (!NIL_P(namespace_v)) {
417
+ Check_Type(namespace_v, T_STRING);
418
+ namespace_len = RSTRING_LEN(namespace_v);
419
+ }
420
+
421
+ if (RSTRING_LEN(cachedir_v) + namespace_len > MAX_CACHEDIR_SIZE) {
422
+ rb_raise(rb_eArgError, "cachedir too long");
423
+ }
424
+
425
+ const char * cachedir = RSTRING_PTR(cachedir_v);
426
+ const char * namespace = NIL_P(namespace_v) ? "" : RSTRING_PTR(namespace_v);
427
+
428
+ uint64_t hash = fnv1a_64_str(path_v);
308
429
  uint8_t first_byte = (hash >> (64 - 8));
309
430
  uint64_t remainder = hash & 0x00ffffffffffffff;
310
431
 
311
- sprintf(*cache_path, "%s/%02"PRIx8"/%014"PRIx64, cachedir, first_byte, remainder);
432
+ sprintf(*cache_path, "%s%s/%02"PRIx8"/%014"PRIx64, cachedir, namespace, first_byte, remainder);
312
433
  }
313
434
 
314
435
  /*
@@ -321,10 +442,8 @@ bs_cache_path(const char * cachedir, const VALUE path, char (* cache_path)[MAX_C
321
442
  */
322
443
  static enum cache_status cache_key_equal_fast_path(struct bs_cache_key *k1,
323
444
  struct bs_cache_key *k2) {
324
- if (k1->version == k2->version &&
325
- k1->ruby_platform == k2->ruby_platform &&
326
- k1->compile_option == k2->compile_option &&
327
- k1->ruby_revision == k2->ruby_revision && k1->size == k2->size) {
445
+ if (k1->ruby_version_digest == k2->ruby_version_digest &&
446
+ k1->size == k2->size) {
328
447
  if (k1->mtime == k2->mtime) {
329
448
  return hit;
330
449
  }
@@ -343,11 +462,34 @@ static int cache_key_equal_slow_path(struct bs_cache_key *current_key,
343
462
  return current_key->digest == cached_key->digest;
344
463
  }
345
464
 
465
+ static ssize_t
466
+ resumable_write(int fd, const void *buf, size_t count)
467
+ {
468
+ size_t total_written = 0;
469
+ const char *ptr = buf;
470
+
471
+ while (count > 0) {
472
+ ssize_t res = write(fd, ptr, count);
473
+
474
+ if (res < 0) {
475
+ if (errno == EINTR) {
476
+ continue; // Interrupted by signal, try again
477
+ }
478
+ return -1; // Real error
479
+ }
480
+
481
+ ptr += res;
482
+ count -= res;
483
+ total_written += res;
484
+ }
485
+ return total_written;
486
+ }
487
+
346
488
  static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_key *old_key, int cache_fd, const char ** errno_provenance)
347
489
  {
348
490
  old_key->mtime = current_key->mtime;
349
491
  lseek(cache_fd, 0, SEEK_SET);
350
- ssize_t nwrite = write(cache_fd, old_key, KEY_SIZE);
492
+ ssize_t nwrite = resumable_write(cache_fd, old_key, KEY_SIZE);
351
493
  if (nwrite < 0) {
352
494
  *errno_provenance = "update_cache_key:write";
353
495
  return -1;
@@ -368,10 +510,9 @@ static int update_cache_key(struct bs_cache_key *current_key, struct bs_cache_ke
368
510
  */
369
511
  static void bs_cache_key_digest(struct bs_cache_key *key,
370
512
  const VALUE input_data) {
371
- if (key->digest_set)
513
+ if (key->digest)
372
514
  return;
373
- key->digest = fnv1a_64(input_data);
374
- key->digest_set = 1;
515
+ key->digest = fnv1a_64_str(input_data);
375
516
  }
376
517
 
377
518
  /*
@@ -380,25 +521,16 @@ static void bs_cache_key_digest(struct bs_cache_key *key,
380
521
  * conversions on the ruby VALUE arguments before passing them along.
381
522
  */
382
523
  static VALUE
383
- bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args)
524
+ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args)
384
525
  {
385
526
  FilePathValue(path_v);
386
527
 
387
- Check_Type(cachedir_v, T_STRING);
388
- Check_Type(path_v, T_STRING);
389
-
390
- if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
391
- rb_raise(rb_eArgError, "cachedir too long");
392
- }
393
-
394
- char * cachedir = RSTRING_PTR(cachedir_v);
395
- char * path = RSTRING_PTR(path_v);
396
528
  char cache_path[MAX_CACHEPATH_SIZE];
397
529
 
398
530
  /* generate cache path to cache_path */
399
- bs_cache_path(cachedir, path_v, &cache_path);
531
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
400
532
 
401
- return bs_fetch(path, path_v, cache_path, handler, args);
533
+ return bs_fetch(RSTRING_PTR(path_v), path_v, cache_path, handler, args);
402
534
  }
403
535
 
404
536
  /*
@@ -407,25 +539,15 @@ bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE arg
407
539
  * and doesn't return the content.
408
540
  */
409
541
  static VALUE
410
- bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler)
542
+ bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler)
411
543
  {
412
544
  FilePathValue(path_v);
413
545
 
414
- Check_Type(cachedir_v, T_STRING);
415
- Check_Type(path_v, T_STRING);
416
-
417
- if (RSTRING_LEN(cachedir_v) > MAX_CACHEDIR_SIZE) {
418
- rb_raise(rb_eArgError, "cachedir too long");
419
- }
420
-
421
- char * cachedir = RSTRING_PTR(cachedir_v);
422
- char * path = RSTRING_PTR(path_v);
423
546
  char cache_path[MAX_CACHEPATH_SIZE];
424
-
425
547
  /* generate cache path to cache_path */
426
- bs_cache_path(cachedir, path_v, &cache_path);
548
+ bs_cache_path(cachedir_v, namespace_v, path_v, &cache_path);
427
549
 
428
- return bs_precompile(path, path_v, cache_path, handler);
550
+ return bs_precompile(RSTRING_PTR(path_v), path_v, cache_path, handler);
429
551
  }
430
552
 
431
553
  static int bs_open_noatime(const char *path, int flags) {
@@ -471,13 +593,19 @@ open_current_file(const char * path, struct bs_cache_key * key, const char ** er
471
593
  return -1;
472
594
  }
473
595
 
474
- key->version = current_version;
475
- key->ruby_platform = current_ruby_platform;
476
- key->compile_option = current_compile_option_crc32;
477
- key->ruby_revision = current_ruby_revision;
478
- key->size = (uint64_t)statbuf.st_size;
479
- key->mtime = (uint64_t)statbuf.st_mtime;
480
- key->digest_set = false;
596
+ key->ruby_version_digest = current_ruby_version_digest;
597
+
598
+ // We're limited to file of 4GiB or less. Hopefully that's enough for everyone.
599
+ if (statbuf.st_size > (uint32_t)-1) {
600
+ *errno_provenance = "bs_fetch:open_current_file:file_too_big";
601
+ close(fd);
602
+ errno = EFBIG;
603
+ return -1;
604
+ }
605
+
606
+ key->size = (uint32_t)statbuf.st_size;
607
+ key->mtime = (uint64_t)statbuf.st_mtime;
608
+ key->digest = 0;
481
609
 
482
610
  return fd;
483
611
  }
@@ -547,10 +675,10 @@ open_cache_file(const char * path, struct bs_cache_key * key, const char ** errn
547
675
 
548
676
  /*
549
677
  * The cache file is laid out like:
550
- * 0...64 : bs_cache_key
551
- * 64..-1 : cached artifact
678
+ * 0...`KEY_SIZE` : bs_cache_key
679
+ * `KEY_SIZE`..-1 : cached artifact
552
680
  *
553
- * This function takes a file descriptor whose position is pre-set to 64, and
681
+ * This function takes a file descriptor whose position is pre-set to `KEY_SIZE`, and
554
682
  * the data_size (corresponding to the remaining number of bytes) listed in the
555
683
  * cache header.
556
684
  *
@@ -626,6 +754,17 @@ mkpath(char * file_path, mode_t mode)
626
754
  return 0;
627
755
  }
628
756
 
757
+ static int
758
+ bs_fchmod(int fd, const char *path, mode_t mode)
759
+ {
760
+ #ifdef _WIN32
761
+ setmode(fd, O_BINARY);
762
+ return chmod(path, mode);
763
+ #else
764
+ return fchmod(fd, mode);
765
+ #endif
766
+ }
767
+
629
768
  /*
630
769
  * Write a cache header/key and a compiled artifact to a given cache path by
631
770
  * writing to a tmpfile and then renaming the tmpfile over top of the final
@@ -639,13 +778,18 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
639
778
  int fd, ret, attempt;
640
779
  ssize_t nwrite;
641
780
 
781
+ uint64_t data_size = RSTRING_LEN(data);
782
+ if (data_size > (uint32_t)-1) {
783
+ return 0; // Don't cache.
784
+ }
785
+
642
786
  for (attempt = 0; attempt < MAX_CREATE_TEMPFILE_ATTEMPT; ++attempt) {
643
787
  tmp_path = strncpy(template, path, MAX_CACHEPATH_SIZE);
644
788
  strcat(tmp_path, ".tmp.XXXXXX");
645
789
 
646
790
  // mkstemp modifies the template to be the actual created path
647
791
  fd = mkstemp(tmp_path);
648
- if (fd > 0) break;
792
+ if (fd >= 0) break;
649
793
 
650
794
  if (attempt == 0 && mkpath(tmp_path, 0775) < 0) {
651
795
  *errno_provenance = "bs_fetch:atomic_write_cache_file:mkpath";
@@ -657,45 +801,42 @@ atomic_write_cache_file(char * path, struct bs_cache_key * key, VALUE data, cons
657
801
  return -1;
658
802
  }
659
803
 
660
- if (chmod(tmp_path, 0644) < 0) {
804
+ if (bs_fchmod(fd, tmp_path, 0644 & ~current_umask) < 0) {
805
+ close(fd);
661
806
  *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod";
662
807
  return -1;
663
808
  }
664
809
 
665
- #ifdef _WIN32
666
- setmode(fd, O_BINARY);
667
- #endif
668
-
669
- key->data_size = RSTRING_LEN(data);
670
- nwrite = write(fd, key, KEY_SIZE);
810
+ key->data_size = (uint32_t)data_size;
811
+ nwrite = resumable_write(fd, key, KEY_SIZE);
671
812
  if (nwrite < 0) {
813
+ close(fd);
672
814
  *errno_provenance = "bs_fetch:atomic_write_cache_file:write";
673
815
  return -1;
674
816
  }
675
817
  if (nwrite != KEY_SIZE) {
818
+ close(fd);
676
819
  *errno_provenance = "bs_fetch:atomic_write_cache_file:keysize";
677
820
  errno = EIO; /* Lies but whatever */
678
821
  return -1;
679
822
  }
680
823
 
681
- nwrite = write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
824
+ nwrite = resumable_write(fd, RSTRING_PTR(data), RSTRING_LEN(data));
682
825
  if (nwrite < 0) return -1;
683
826
  if (nwrite != RSTRING_LEN(data)) {
827
+ close(fd);
684
828
  *errno_provenance = "bs_fetch:atomic_write_cache_file:writelength";
685
829
  errno = EIO; /* Lies but whatever */
686
830
  return -1;
687
831
  }
688
832
 
689
833
  close(fd);
834
+
690
835
  ret = rename(tmp_path, path);
691
836
  if (ret < 0) {
692
837
  *errno_provenance = "bs_fetch:atomic_write_cache_file:rename";
693
838
  return -1;
694
839
  }
695
- ret = chmod(path, 0664 & ~current_umask);
696
- if (ret < 0) {
697
- *errno_provenance = "bs_fetch:atomic_write_cache_file:chmod";
698
- }
699
840
  return ret;
700
841
  }
701
842
 
@@ -845,7 +986,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
845
986
  exception_message = path_v;
846
987
  goto fail_errno;
847
988
  }
848
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
989
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
849
990
  if (exception_tag != 0) goto raise;
850
991
  goto succeed;
851
992
  } else if (res == CACHE_MISS || res == CACHE_STALE) valid_cache = 0;
@@ -871,7 +1012,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
871
1012
  /* If input_to_storage raised Bootsnap::CompileCache::Uncompilable, don't try
872
1013
  * to cache anything; just return input_to_output(input_data) */
873
1014
  if (storage_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
874
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1015
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
875
1016
  if (exception_tag != 0) goto raise;
876
1017
  goto succeed;
877
1018
  }
@@ -891,7 +1032,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
891
1032
 
892
1033
  if (output_data == rb_cBootsnap_CompileCache_UNCOMPILABLE) {
893
1034
  /* If storage_to_output returned `Uncompilable` we fallback to `input_to_output` */
894
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1035
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
895
1036
  if (exception_tag != 0) goto raise;
896
1037
  } else if (NIL_P(output_data)) {
897
1038
  /* If output_data is nil, delete the cache entry and generate the output
@@ -905,7 +1046,7 @@ bs_fetch(char * path, VALUE path_v, char * cache_path, VALUE handler, VALUE args
905
1046
  goto fail_errno;
906
1047
  }
907
1048
  }
908
- bs_input_to_output(handler, args, input_data, &output_data, &exception_tag);
1049
+ bs_input_to_output(handler, args, input_data, path_v, &output_data, &exception_tag);
909
1050
  if (exception_tag != 0) goto raise;
910
1051
  }
911
1052
 
@@ -1001,7 +1142,7 @@ bs_precompile(char * path, VALUE path_v, char * cache_path, VALUE handler)
1001
1142
  /* Cache is stale, invalid, or missing. Regenerate and write it out. */
1002
1143
 
1003
1144
  /* Read the contents of the source file into a buffer */
1004
- if ((input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail;
1145
+ if (input_data == Qfalse && (input_data = bs_read_contents(current_fd, current_key.size, &errno_provenance)) == Qfalse) goto fail;
1005
1146
 
1006
1147
  /* Try to compile the input_data using input_to_storage(input_data) */
1007
1148
  exception_tag = bs_input_to_storage(handler, Qnil, input_data, path_v, &storage_data);
@@ -1063,6 +1204,7 @@ struct i2o_data {
1063
1204
  VALUE handler;
1064
1205
  VALUE args;
1065
1206
  VALUE input_data;
1207
+ VALUE pathval;
1066
1208
  };
1067
1209
 
1068
1210
  struct i2s_data {
@@ -1075,7 +1217,7 @@ static VALUE
1075
1217
  try_storage_to_output(VALUE arg)
1076
1218
  {
1077
1219
  struct s2o_data * data = (struct s2o_data *)arg;
1078
- return rb_funcall(data->handler, rb_intern("storage_to_output"), 2, data->storage_data, data->args);
1220
+ return rb_funcall(data->handler, id_storage_to_output, 2, data->storage_data, data->args);
1079
1221
  }
1080
1222
 
1081
1223
  static int
@@ -1092,12 +1234,13 @@ bs_storage_to_output(VALUE handler, VALUE args, VALUE storage_data, VALUE * outp
1092
1234
  }
1093
1235
 
1094
1236
  static void
1095
- bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE * output_data, int * exception_tag)
1237
+ bs_input_to_output(VALUE handler, VALUE args, VALUE input_data, VALUE path_v, VALUE * output_data, int * exception_tag)
1096
1238
  {
1097
1239
  struct i2o_data i2o_data = {
1098
1240
  .handler = handler,
1099
1241
  .args = args,
1100
1242
  .input_data = input_data,
1243
+ .pathval = path_v,
1101
1244
  };
1102
1245
  *output_data = rb_protect(prot_input_to_output, (VALUE)&i2o_data, exception_tag);
1103
1246
  }
@@ -1106,14 +1249,14 @@ static VALUE
1106
1249
  prot_input_to_output(VALUE arg)
1107
1250
  {
1108
1251
  struct i2o_data * data = (struct i2o_data *)arg;
1109
- return rb_funcall(data->handler, rb_intern("input_to_output"), 2, data->input_data, data->args);
1252
+ return rb_funcall(data->handler, id_input_to_output, 3, data->input_data, data->pathval, data->args);
1110
1253
  }
1111
1254
 
1112
1255
  static VALUE
1113
1256
  try_input_to_storage(VALUE arg)
1114
1257
  {
1115
1258
  struct i2s_data * data = (struct i2s_data *)arg;
1116
- return rb_funcall(data->handler, rb_intern("input_to_storage"), 2, data->input_data, data->pathval);
1259
+ return rb_funcall(data->handler, id_input_to_storage, 2, data->input_data, data->pathval);
1117
1260
  }
1118
1261
 
1119
1262
  static int