rubox 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b21effe4e87ff543efe951e8bdc3456466d247675c1d33f03519558bc8517d0
4
+ data.tar.gz: 6d3dbc24179304e03d74e43e76cbcfdceac06bfe0e24b91f9449ce9b2a46b4a6
5
+ SHA512:
6
+ metadata.gz: 45eeded1e6e8993ee97f1743ccaa5abf38b4749dd500e9cdba02b44119e3fdd1a11579c5914ddb21060650fbbfb2049b18688f3711fe3c37379219dfa085aa52
7
+ data.tar.gz: dbf27ce0b7e63225cf8e16fb1308e403972b0a2db1c80c79bb0b731d5b429341e9ef540c40aee076316175803b36c244353e357c93af25b011c5559695e6e478
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hasinski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,119 @@
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # Builds CRuby on Alpine Linux (musl) with system deps statically linked.
4
+ # The binary links against musl libc dynamically (for dlopen support),
5
+ # but we bundle musl's ld.so in the payload so the result works on
6
+ # ANY Linux distro -- glibc, musl, or anything else.
7
+ #
8
+ ARG RUBY_VERSION=4.0.0
9
+ ARG JOBS=4
10
+
11
+ FROM alpine:3.21 AS builder
12
+
13
+ ARG RUBY_VERSION
14
+ ARG JOBS
15
+
16
+ # Build deps - both shared and static
17
+ RUN apk add --no-cache \
18
+ build-base \
19
+ linux-headers \
20
+ openssl-dev openssl-libs-static \
21
+ zlib-dev zlib-static \
22
+ yaml-dev yaml-static \
23
+ libffi-dev \
24
+ readline-dev readline-static \
25
+ ncurses-dev ncurses-static \
26
+ gdbm-dev \
27
+ curl \
28
+ autoconf \
29
+ bison
30
+
31
+ # Rust is needed for YJIT but may not be available on all architectures
32
+ # (e.g. cross-compiling x86_64 on arm64 via qemu). Optional.
33
+ RUN apk add --no-cache rust cargo || true
34
+
35
+ # Extra native extension deps (static where available)
36
+ RUN apk add --no-cache \
37
+ libxml2-dev libxml2-static \
38
+ libxslt-dev \
39
+ postgresql16-dev \
40
+ mariadb-connector-c-dev \
41
+ sqlite-dev sqlite-static \
42
+ || true
43
+
44
+ WORKDIR /build
45
+
46
+ RUN set -ex && \
47
+ MAJOR_MINOR=$(echo "$RUBY_VERSION" | sed 's/\.[^.]*$//') && \
48
+ curl -fSL "https://cache.ruby-lang.org/pub/ruby/${MAJOR_MINOR}/ruby-${RUBY_VERSION}.tar.gz" \
49
+ -o ruby.tar.gz && \
50
+ tar xzf ruby.tar.gz && \
51
+ rm ruby.tar.gz
52
+
53
+ # Create staging dir with ONLY .a files for third-party libs we want static.
54
+ # IMPORTANT: We specifically exclude libc.a, libdl.a, libpthread.a, libm.a etc.
55
+ # because statically linking those disables dlopen, which we need for gem extensions.
56
+ RUN mkdir -p /build/static-libs && \
57
+ # NOTE: libffi excluded -- its .a is not compiled with -fPIC on x86_64,
58
+ # which breaks fiddle.so linking. fiddle uses libffi dynamically instead.
59
+ for lib in libssl libcrypto libyaml libz libreadline libhistory libncursesw \
60
+ libgdbm libxml2 libxslt libsqlite3; do \
61
+ [ -f "/usr/lib/${lib}.a" ] && ln -sf "/usr/lib/${lib}.a" /build/static-libs/; \
62
+ done && \
63
+ echo "Staged static libs:" && ls /build/static-libs/
64
+
65
+ WORKDIR /build/ruby-${RUBY_VERSION}
66
+
67
+ # Configure:
68
+ # - --disable-shared + --with-static-linked-ext: Ruby extensions linked into binary
69
+ # - -L/build/static-libs: linker finds .a only (no .so), forcing static linkage
70
+ # of openssl, zlib, yaml, readline, etc.
71
+ # - We do NOT use LDFLAGS="-static" because that disables dlopen entirely,
72
+ # which is needed for loading gem native extensions (.so files).
73
+ RUN YJIT_FLAG="--disable-yjit" && \
74
+ if command -v rustc >/dev/null 2>&1; then YJIT_FLAG="--enable-yjit"; fi && \
75
+ echo "YJIT: $YJIT_FLAG" && \
76
+ ./configure \
77
+ --prefix=/opt/ruby \
78
+ --disable-shared \
79
+ --enable-static \
80
+ --disable-install-doc \
81
+ --disable-install-rdoc \
82
+ --disable-install-capi \
83
+ --with-static-linked-ext \
84
+ --without-gmp \
85
+ $YJIT_FLAG \
86
+ LDFLAGS="-L/build/static-libs" \
87
+ CFLAGS="-O2 -fPIC"
88
+
89
+ RUN make -j${JOBS}
90
+ RUN make install
91
+ RUN strip /opt/ruby/bin/ruby
92
+
93
+ # Bundle the musl dynamic linker and libgcc_s into the Ruby installation's lib/.
94
+ # These are the only dynamic deps the Ruby binary needs.
95
+ # The stub will invoke Ruby through this bundled ld-musl loader,
96
+ # making the binary portable to ANY Linux distro.
97
+ RUN echo "=== Bundling runtime libraries ===" && \
98
+ for lib in $(ldd /opt/ruby/bin/ruby 2>/dev/null | grep '=>' | awk '{print $3}'); do \
99
+ bn=$(basename "$lib"); \
100
+ echo " Bundling: $bn"; \
101
+ cp "$lib" "/opt/ruby/lib/$bn"; \
102
+ done && \
103
+ # Also bundle the musl dynamic linker itself
104
+ INTERP=$(readelf -l /opt/ruby/bin/ruby | grep 'interpreter' | sed 's/.*: \(.*\)]/\1/') && \
105
+ echo " Bundling loader: $INTERP" && \
106
+ cp "$INTERP" "/opt/ruby/lib/$(basename $INTERP)" && \
107
+ chmod +x "/opt/ruby/lib/$(basename $INTERP)"
108
+
109
+ # Verify
110
+ RUN echo "=== Dynamic deps ===" && \
111
+ ldd /opt/ruby/bin/ruby 2>&1 && \
112
+ echo "=== Bundled libs ===" && \
113
+ ls -la /opt/ruby/lib/ld-musl-* /opt/ruby/lib/lib*.so* 2>/dev/null && \
114
+ echo "=== Ruby ===" && \
115
+ /opt/ruby/bin/ruby --version && \
116
+ /opt/ruby/bin/ruby -e 'puts "dlopen: #{defined?(Fiddle) ? "yes" : "yes (via dl)"}"'
117
+
118
+ FROM scratch AS output
119
+ COPY --from=builder /opt/ruby /
data/data/ext/stub.c ADDED
@@ -0,0 +1,489 @@
1
+ /*
2
+ * rubox stub
3
+ *
4
+ * Self-extracting loader with content-addressed caching.
5
+ *
6
+ * At runtime:
7
+ * 1. Opens its own executable (via /proc/self/exe or argv[0])
8
+ * 2. Reads the 24-byte footer to find the payload offset and size
9
+ * 3. Derives a cache key from offset+size (unique per build)
10
+ * 4. Checks ~/.cache/rubox/<key>/ for existing extraction
11
+ * 5. If not cached, extracts payload there
12
+ * 6. Execs the bundled Ruby interpreter with the entry script
13
+ *
14
+ * On Linux, Ruby is exec'd through the bundled musl dynamic linker
15
+ * (lib/ld-musl-*.so.1) so the binary works on ANY Linux distro
16
+ * regardless of the host's libc (glibc, musl, etc).
17
+ *
18
+ * Footer layout (last 24 bytes of the binary):
19
+ * [8 bytes] payload offset (little-endian uint64)
20
+ * [8 bytes] payload size (little-endian uint64)
21
+ * [8 bytes] magic "CRUBY\x00\x01\x00"
22
+ *
23
+ * Env vars:
24
+ * RUBOX_CACHE Override cache directory
25
+ * RUBOX_NO_CACHE Set to 1 to extract to tmpdir with cleanup
26
+ * RUBOX_VERBOSE Set to 1 for debug messages
27
+ */
28
+
29
+ #define _GNU_SOURCE
30
+ #include <stdio.h>
31
+ #include <stdlib.h>
32
+ #include <string.h>
33
+ #include <unistd.h>
34
+ #include <sys/stat.h>
35
+ #include <sys/wait.h>
36
+ #include <sys/file.h>
37
+ #include <errno.h>
38
+ #include <limits.h>
39
+ #include <fcntl.h>
40
+ #include <dirent.h>
41
+ #include <glob.h>
42
+
43
+ #ifdef __linux__
44
+ #include <sys/syscall.h>
45
+ #endif
46
+
47
+ #ifdef __APPLE__
48
+ #include <mach-o/dyld.h>
49
+ #endif
50
+
51
+ #define FOOTER_SIZE 24
52
+ #define MAGIC "CRUBY\x00\x01\x00"
53
+ #define MAGIC_SIZE 8
54
+
55
+ static const char PAYLOAD_MAGIC[MAGIC_SIZE] = MAGIC;
56
+ static int verbose = 0;
57
+
58
+ #define LOG(...) do { if (verbose) fprintf(stderr, "rubox: " __VA_ARGS__); } while(0)
59
+
60
+ static int self_exe_path(char *buf, size_t bufsize) {
61
+ #ifdef __linux__
62
+ ssize_t len = readlink("/proc/self/exe", buf, bufsize - 1);
63
+ if (len > 0) {
64
+ buf[len] = '\0';
65
+ return 0;
66
+ }
67
+ #endif
68
+ #ifdef __APPLE__
69
+ uint32_t size = (uint32_t)bufsize;
70
+ if (_NSGetExecutablePath(buf, &size) == 0) {
71
+ char resolved[PATH_MAX];
72
+ if (realpath(buf, resolved)) {
73
+ strncpy(buf, resolved, bufsize - 1);
74
+ buf[bufsize - 1] = '\0';
75
+ }
76
+ return 0;
77
+ }
78
+ #endif
79
+ return -1;
80
+ }
81
+
82
+ static int mkdirp(const char *path, mode_t mode) {
83
+ char tmp[PATH_MAX];
84
+ char *p = NULL;
85
+ size_t len;
86
+
87
+ snprintf(tmp, sizeof(tmp), "%s", path);
88
+ len = strlen(tmp);
89
+ if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
90
+
91
+ for (p = tmp + 1; *p; p++) {
92
+ if (*p == '/') {
93
+ *p = '\0';
94
+ mkdir(tmp, mode);
95
+ *p = '/';
96
+ }
97
+ }
98
+ return mkdir(tmp, mode);
99
+ }
100
+
101
+ static int make_tmpdir(char *buf, size_t bufsize) {
102
+ const char *tmpdir = getenv("TMPDIR");
103
+ if (!tmpdir) tmpdir = getenv("TMP");
104
+ if (!tmpdir) tmpdir = "/tmp";
105
+
106
+ snprintf(buf, bufsize, "%s/rubox.XXXXXX", tmpdir);
107
+ if (mkdtemp(buf) == NULL) {
108
+ perror("mkdtemp");
109
+ return -1;
110
+ }
111
+ return 0;
112
+ }
113
+
114
+ static void cleanup_dir(const char *path) {
115
+ char cmd[PATH_MAX + 16];
116
+ snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path);
117
+ (void)system(cmd);
118
+ }
119
+
120
+ static void build_cache_path(char *buf, size_t bufsize, const char *cache_key) {
121
+ const char *override = getenv("RUBOX_CACHE");
122
+ if (override && override[0]) {
123
+ snprintf(buf, bufsize, "%s", override);
124
+ return;
125
+ }
126
+
127
+ #ifdef __linux__
128
+ /*
129
+ * On Linux, prefer /dev/shm (tmpfs / shared memory) for the cache.
130
+ * This is RAM-backed on virtually all Linux systems, so extraction
131
+ * and subsequent loads happen entirely in memory with no disk I/O.
132
+ * Skip if /dev/shm is mounted noexec (common in Docker) since we
133
+ * need to exec the loader and mmap Ruby's binary with PROT_EXEC.
134
+ */
135
+ struct stat shm_st;
136
+ if (stat("/dev/shm", &shm_st) == 0 && S_ISDIR(shm_st.st_mode) &&
137
+ access("/dev/shm", W_OK) == 0) {
138
+ /* Check for noexec by trying to create and exec a tiny script */
139
+ int shm_exec_ok = 0;
140
+ char test_path[] = "/dev/shm/.rubox-exec-test";
141
+ int tfd = open(test_path, O_CREAT | O_WRONLY | O_TRUNC, 0755);
142
+ if (tfd >= 0) {
143
+ const char *script = "#!/bin/sh\nexit 0\n";
144
+ if (write(tfd, script, strlen(script)) > 0) {
145
+ close(tfd);
146
+ char test_cmd[PATH_MAX + 32];
147
+ snprintf(test_cmd, sizeof(test_cmd), "%s 2>/dev/null", test_path);
148
+ shm_exec_ok = (system(test_cmd) == 0);
149
+ } else {
150
+ close(tfd);
151
+ }
152
+ unlink(test_path);
153
+ }
154
+ if (shm_exec_ok) {
155
+ snprintf(buf, bufsize, "/dev/shm/rubox/%s", cache_key);
156
+ LOG("/dev/shm supports exec, using RAM cache\n");
157
+ return;
158
+ } else {
159
+ LOG("/dev/shm is noexec, using disk cache\n");
160
+ }
161
+ }
162
+ #endif
163
+
164
+ const char *xdg = getenv("XDG_CACHE_HOME");
165
+ if (xdg && xdg[0]) {
166
+ snprintf(buf, bufsize, "%s/rubox/%s", xdg, cache_key);
167
+ } else {
168
+ const char *home = getenv("HOME");
169
+ if (!home) home = "/tmp";
170
+ snprintf(buf, bufsize, "%s/.cache/rubox/%s", home, cache_key);
171
+ }
172
+ }
173
+
174
+ static int verify_extraction(const char *dest_dir) {
175
+ char path[PATH_MAX];
176
+ snprintf(path, sizeof(path), "%s/bin/ruby", dest_dir);
177
+ /* Use R_OK not X_OK: /dev/shm is mounted noexec on many systems,
178
+ * but we exec through the bundled musl loader, not directly. */
179
+ return access(path, R_OK);
180
+ }
181
+
182
+ static int extract_payload(const char *exe_path, off_t offset, off_t size,
183
+ const char *dest_dir) {
184
+ char cmd[PATH_MAX * 2 + 256];
185
+
186
+ /*
187
+ * Payload is gzip-compressed tar. Extract with:
188
+ * tail -c +<offset+1> <file> | head -c <size> | gzip -d | tar x
189
+ *
190
+ * tail/head is fast on all platforms (no bs=1 byte-at-a-time reads).
191
+ * tail -c +N starts output at byte N (1-indexed).
192
+ */
193
+ snprintf(cmd, sizeof(cmd),
194
+ "tail -c +%lld '%s' | head -c %lld | gzip -d -c | tar xf - -C '%s' 2>/dev/null",
195
+ (long long)(offset + 1), exe_path, (long long)size, dest_dir);
196
+ LOG("extracting: tail+head+gzip\n");
197
+ (void)system(cmd);
198
+ if (verify_extraction(dest_dir) == 0) return 0;
199
+
200
+ /* Fallback: GNU dd (for systems where tail -c doesn't work) */
201
+ snprintf(cmd, sizeof(cmd),
202
+ "dd if='%s' iflag=skip_bytes,count_bytes bs=65536 skip=%lld count=%lld 2>/dev/null | "
203
+ "gzip -d -c | tar xf - -C '%s' 2>/dev/null",
204
+ exe_path, (long long)offset, (long long)size, dest_dir);
205
+ LOG("fallback: dd+gzip\n");
206
+ (void)system(cmd);
207
+ return verify_extraction(dest_dir);
208
+ }
209
+
210
+ static uint64_t read_le64(const unsigned char *p) {
211
+ return (uint64_t)p[0]
212
+ | (uint64_t)p[1] << 8
213
+ | (uint64_t)p[2] << 16
214
+ | (uint64_t)p[3] << 24
215
+ | (uint64_t)p[4] << 32
216
+ | (uint64_t)p[5] << 40
217
+ | (uint64_t)p[6] << 48
218
+ | (uint64_t)p[7] << 56;
219
+ }
220
+
221
+ #ifdef __linux__
222
+ /*
223
+ * Find the bundled musl dynamic linker.
224
+ * It's at <cache_dir>/lib/ld-musl-<arch>.so.1
225
+ * Returns 0 on success and fills loader_path.
226
+ */
227
+ static int find_musl_loader(const char *cache_dir, char *loader_path, size_t bufsize) {
228
+ char pattern[PATH_MAX];
229
+ snprintf(pattern, sizeof(pattern), "%s/lib/ld-musl-*.so.1", cache_dir);
230
+
231
+ glob_t g;
232
+ int ret = glob(pattern, 0, NULL, &g);
233
+ if (ret == 0 && g.gl_pathc > 0) {
234
+ snprintf(loader_path, bufsize, "%s", g.gl_pathv[0]);
235
+ globfree(&g);
236
+ return 0;
237
+ }
238
+ if (ret != GLOB_NOMATCH) globfree(&g);
239
+ return -1;
240
+ }
241
+ #endif
242
+
243
+ int main(int argc, char **argv) {
244
+ char exe_path[PATH_MAX];
245
+ char cache_dir[PATH_MAX];
246
+ char ruby_bin[PATH_MAX];
247
+ char entry_script[PATH_MAX];
248
+ char lock_path[PATH_MAX];
249
+ unsigned char footer[FOOTER_SIZE];
250
+
251
+ verbose = (getenv("RUBOX_VERBOSE") != NULL);
252
+ int no_cache = 0;
253
+ const char *nc = getenv("RUBOX_NO_CACHE");
254
+ if (nc && nc[0] == '1') no_cache = 1;
255
+
256
+ /* Find our own executable */
257
+ if (self_exe_path(exe_path, sizeof(exe_path)) != 0) {
258
+ if (argv[0] && realpath(argv[0], exe_path) == NULL) {
259
+ fprintf(stderr, "rubox: cannot determine executable path\n");
260
+ return 1;
261
+ }
262
+ }
263
+ LOG("exe: %s\n", exe_path);
264
+
265
+ /* Read the footer */
266
+ FILE *fp = fopen(exe_path, "rb");
267
+ if (!fp) {
268
+ fprintf(stderr, "rubox: cannot open %s: %s\n",
269
+ exe_path, strerror(errno));
270
+ return 1;
271
+ }
272
+
273
+ if (fseek(fp, -FOOTER_SIZE, SEEK_END) != 0) {
274
+ fprintf(stderr, "rubox: cannot seek to footer\n");
275
+ fclose(fp);
276
+ return 1;
277
+ }
278
+
279
+ if (fread(footer, 1, FOOTER_SIZE, fp) != FOOTER_SIZE) {
280
+ fprintf(stderr, "rubox: cannot read footer\n");
281
+ fclose(fp);
282
+ return 1;
283
+ }
284
+ fclose(fp);
285
+
286
+ /* Verify magic */
287
+ if (memcmp(footer + 16, PAYLOAD_MAGIC, MAGIC_SIZE) != 0) {
288
+ fprintf(stderr, "rubox: no payload found (bad magic)\n");
289
+ return 1;
290
+ }
291
+
292
+ uint64_t payload_offset = read_le64(footer);
293
+ uint64_t payload_size = read_le64(footer + 8);
294
+
295
+ char cache_key[33];
296
+ snprintf(cache_key, sizeof(cache_key), "%08llx%08llx",
297
+ (unsigned long long)payload_offset,
298
+ (unsigned long long)payload_size);
299
+
300
+ LOG("payload: offset=%llu size=%llu key=%s\n",
301
+ (unsigned long long)payload_offset,
302
+ (unsigned long long)payload_size, cache_key);
303
+
304
+ int needs_extract = 1;
305
+ int is_tmpdir = 0;
306
+
307
+ if (no_cache) {
308
+ if (make_tmpdir(cache_dir, sizeof(cache_dir)) != 0) return 1;
309
+ is_tmpdir = 1;
310
+ LOG("no-cache mode, extracting to %s\n", cache_dir);
311
+ } else {
312
+ build_cache_path(cache_dir, sizeof(cache_dir), cache_key);
313
+ snprintf(ruby_bin, sizeof(ruby_bin), "%s/bin/ruby", cache_dir);
314
+
315
+ if (access(ruby_bin, R_OK) == 0) {
316
+ needs_extract = 0;
317
+ LOG("cache hit: %s\n", cache_dir);
318
+ } else {
319
+ LOG("cache miss, extracting to %s\n", cache_dir);
320
+ mkdirp(cache_dir, 0755);
321
+ }
322
+ }
323
+
324
+ if (needs_extract) {
325
+ snprintf(lock_path, sizeof(lock_path), "%s.lock", cache_dir);
326
+ int lock_fd = open(lock_path, O_CREAT | O_WRONLY, 0644);
327
+ if (lock_fd >= 0) {
328
+ if (flock(lock_fd, LOCK_EX) == 0) {
329
+ snprintf(ruby_bin, sizeof(ruby_bin), "%s/bin/ruby", cache_dir);
330
+ if (access(ruby_bin, R_OK) == 0) {
331
+ needs_extract = 0;
332
+ LOG("cache populated by another process\n");
333
+ }
334
+ }
335
+ }
336
+
337
+ if (needs_extract) {
338
+ int extract_ok = (extract_payload(exe_path, (off_t)payload_offset,
339
+ (off_t)payload_size, cache_dir) == 0);
340
+
341
+ #ifdef __linux__
342
+ /* If extraction to /dev/shm failed (e.g. size limit), fall back
343
+ * to ~/.cache on disk. */
344
+ if (!extract_ok && strncmp(cache_dir, "/dev/shm/", 9) == 0) {
345
+ LOG("shm extraction failed, falling back to disk cache\n");
346
+ cleanup_dir(cache_dir);
347
+ if (lock_fd >= 0) { unlink(lock_path); close(lock_fd); lock_fd = -1; }
348
+
349
+ /* Rebuild path without /dev/shm preference */
350
+ const char *home = getenv("HOME");
351
+ if (!home) home = "/tmp";
352
+ snprintf(cache_dir, sizeof(cache_dir),
353
+ "%s/.cache/rubox/%s", home, cache_key);
354
+ mkdirp(cache_dir, 0755);
355
+
356
+ snprintf(lock_path, sizeof(lock_path), "%s.lock", cache_dir);
357
+ lock_fd = open(lock_path, O_CREAT | O_WRONLY, 0644);
358
+ if (lock_fd >= 0) flock(lock_fd, LOCK_EX);
359
+
360
+ LOG("retrying extraction to %s\n", cache_dir);
361
+ extract_ok = (extract_payload(exe_path, (off_t)payload_offset,
362
+ (off_t)payload_size, cache_dir) == 0);
363
+ }
364
+ #endif
365
+
366
+ if (!extract_ok) {
367
+ fprintf(stderr, "rubox: extraction failed\n");
368
+ fprintf(stderr, "\n");
369
+ fprintf(stderr, "Troubleshooting:\n");
370
+ fprintf(stderr, " - Check disk space: df -h %s\n", cache_dir);
371
+ fprintf(stderr, " - Ensure gzip is installed: which gzip\n");
372
+ fprintf(stderr, " - Try verbose mode: RUBOX_VERBOSE=1 %s\n",
373
+ exe_path);
374
+ fprintf(stderr, " - Try without cache: RUBOX_NO_CACHE=1 %s\n",
375
+ exe_path);
376
+ fprintf(stderr, "\n");
377
+ if (is_tmpdir) cleanup_dir(cache_dir);
378
+ if (lock_fd >= 0) { unlink(lock_path); close(lock_fd); }
379
+ return 1;
380
+ }
381
+ LOG("extraction complete\n");
382
+ }
383
+
384
+ if (lock_fd >= 0) {
385
+ flock(lock_fd, LOCK_UN);
386
+ unlink(lock_path);
387
+ close(lock_fd);
388
+ }
389
+ }
390
+
391
+ /* Build paths */
392
+ snprintf(ruby_bin, sizeof(ruby_bin), "%s/bin/ruby", cache_dir);
393
+ snprintf(entry_script, sizeof(entry_script), "%s/entry.rb", cache_dir);
394
+
395
+ if (access(ruby_bin, R_OK) != 0) {
396
+ fprintf(stderr, "rubox: ruby not found at %s\n", ruby_bin);
397
+ fprintf(stderr, "The cache may be corrupt. Try: rm -rf %s\n", cache_dir);
398
+ if (is_tmpdir) cleanup_dir(cache_dir);
399
+ return 1;
400
+ }
401
+
402
+ if (access(entry_script, R_OK) != 0) {
403
+ fprintf(stderr, "rubox: entry.rb not found at %s\n", entry_script);
404
+ fprintf(stderr, "The binary may have been packaged incorrectly.\n");
405
+ if (is_tmpdir) cleanup_dir(cache_dir);
406
+ return 1;
407
+ }
408
+
409
+ /* Set environment */
410
+ char root_env[PATH_MAX + 32];
411
+ snprintf(root_env, sizeof(root_env), "RUBOX_ROOT=%s", cache_dir);
412
+ putenv(root_env);
413
+
414
+ putenv("BUNDLE_GEMFILE=");
415
+
416
+ if (is_tmpdir) {
417
+ char cleanup_env[PATH_MAX + 32];
418
+ snprintf(cleanup_env, sizeof(cleanup_env),
419
+ "RUBOX_CLEANUP=%s", cache_dir);
420
+ putenv(cleanup_env);
421
+ }
422
+
423
+ /*
424
+ * On Linux we exec Ruby through the bundled musl dynamic linker.
425
+ * This makes the binary work on ANY Linux distro (glibc, musl, etc.)
426
+ * because we carry our own libc.
427
+ *
428
+ * The invocation is:
429
+ * /path/to/ld-musl-<arch>.so.1 --library-path /path/to/lib \
430
+ * /path/to/ruby entry.rb [args...]
431
+ *
432
+ * On macOS we exec Ruby directly (system dyld handles everything).
433
+ */
434
+
435
+ #ifdef __linux__
436
+ char loader_path[PATH_MAX];
437
+ char lib_path[PATH_MAX];
438
+
439
+ if (find_musl_loader(cache_dir, loader_path, sizeof(loader_path)) == 0) {
440
+ snprintf(lib_path, sizeof(lib_path), "%s/lib", cache_dir);
441
+ LOG("using bundled loader: %s\n", loader_path);
442
+
443
+ /* argv: loader --library-path <lib> ruby entry.rb [user args...] */
444
+ char *exec_loader = loader_path;
445
+ int new_argc = argc + 5;
446
+ char **new_argv = malloc(sizeof(char *) * (new_argc + 1));
447
+ if (!new_argv) { perror("malloc"); return 1; }
448
+
449
+ new_argv[0] = exec_loader;
450
+ new_argv[1] = "--library-path";
451
+ new_argv[2] = lib_path;
452
+ new_argv[3] = ruby_bin;
453
+ new_argv[4] = entry_script;
454
+ for (int i = 1; i < argc; i++) {
455
+ new_argv[i + 4] = argv[i];
456
+ }
457
+ new_argv[argc + 4] = NULL;
458
+
459
+ execv(exec_loader, new_argv);
460
+ fprintf(stderr, "rubox: exec loader failed: %s\n", strerror(errno));
461
+ free(new_argv);
462
+ /* Fall through to direct exec as fallback */
463
+ } else {
464
+ LOG("no bundled loader found, exec'ing ruby directly\n");
465
+ }
466
+ #endif
467
+
468
+ /* Direct exec (macOS, or Linux fallback if no bundled loader) */
469
+ char **new_argv = malloc(sizeof(char *) * (argc + 2));
470
+ if (!new_argv) {
471
+ perror("malloc");
472
+ if (is_tmpdir) cleanup_dir(cache_dir);
473
+ return 1;
474
+ }
475
+
476
+ new_argv[0] = ruby_bin;
477
+ new_argv[1] = entry_script;
478
+ for (int i = 1; i < argc; i++) {
479
+ new_argv[i + 1] = argv[i];
480
+ }
481
+ new_argv[argc + 1] = NULL;
482
+
483
+ execv(ruby_bin, new_argv);
484
+
485
+ fprintf(stderr, "rubox: exec failed: %s\n", strerror(errno));
486
+ if (is_tmpdir) cleanup_dir(cache_dir);
487
+ free(new_argv);
488
+ return 1;
489
+ }
@@ -0,0 +1,38 @@
1
+ /*
2
+ * write-footer: append the 24-byte rubox footer to a file.
3
+ * Usage: write-footer <file> <payload_offset> <payload_size>
4
+ */
5
+ #include <stdio.h>
6
+ #include <stdlib.h>
7
+ #include <stdint.h>
8
+ #include <string.h>
9
+
10
+ static void write_le64(FILE *fp, uint64_t v) {
11
+ for (int i = 0; i < 8; i++) {
12
+ fputc((int)(v & 0xff), fp);
13
+ v >>= 8;
14
+ }
15
+ }
16
+
17
+ int main(int argc, char **argv) {
18
+ if (argc != 4) {
19
+ fprintf(stderr, "Usage: %s <file> <payload_offset> <payload_size>\n", argv[0]);
20
+ return 1;
21
+ }
22
+
23
+ const char *path = argv[1];
24
+ uint64_t offset = (uint64_t)strtoull(argv[2], NULL, 10);
25
+ uint64_t size = (uint64_t)strtoull(argv[3], NULL, 10);
26
+
27
+ FILE *fp = fopen(path, "ab");
28
+ if (!fp) {
29
+ perror("fopen");
30
+ return 1;
31
+ }
32
+
33
+ write_le64(fp, offset);
34
+ write_le64(fp, size);
35
+ fwrite("CRUBY\x00\x01\x00", 1, 8, fp);
36
+ fclose(fp);
37
+ return 0;
38
+ }