|
11 | 11 | * RPC dispatch, transport limits, receive deadline, clean shutdown. |
12 | 12 | */ |
13 | 13 | #include "../src/foundation/compat.h" |
| 14 | +#if defined(__APPLE__) |
| 15 | +#include <mach-o/dyld.h> /* _NSGetExecutablePath — deleted-self driver */ |
| 16 | +#endif |
14 | 17 | #include "../src/foundation/compat_fs.h" |
15 | 18 | #include "../src/foundation/compat_thread.h" |
16 | 19 | #include "../src/foundation/log.h" |
@@ -457,6 +460,86 @@ TEST(httpd_resolves_bare_binary_path_from_path) { |
457 | 460 | #endif |
458 | 461 | } |
459 | 462 |
|
| 463 | +TEST(httpd_deleted_self_spawn_path_follows_platform_ruling) { |
| 464 | +#if defined(__linux__) || defined(__APPLE__) |
| 465 | + char tmpdir[256]; |
| 466 | + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_httpd_deleted_self_XXXXXX"); |
| 467 | + ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir)); |
| 468 | + |
| 469 | + char current[1024]; |
| 470 | +#if defined(__linux__) |
| 471 | + ssize_t current_len = readlink("/proc/self/exe", current, sizeof(current) - 1); |
| 472 | + ASSERT_GT(current_len, 0); |
| 473 | + ASSERT_LT(current_len, (ssize_t)(sizeof(current) - 1)); |
| 474 | + current[current_len] = '\0'; |
| 475 | +#else |
| 476 | + uint32_t current_sz = sizeof(current); |
| 477 | + ASSERT_EQ(_NSGetExecutablePath(current, ¤t_sz), 0); |
| 478 | +#endif |
| 479 | + |
| 480 | + char launch_path[512]; |
| 481 | + char replacement_path[512]; |
| 482 | + snprintf(launch_path, sizeof(launch_path), "%s/test-runner", tmpdir); |
| 483 | + snprintf(replacement_path, sizeof(replacement_path), "%s/test-runner.new", tmpdir); |
| 484 | + ASSERT_EQ(cbm_copy_file(current, launch_path), 0); |
| 485 | + ASSERT_EQ(chmod(launch_path, 0755), 0); |
| 486 | + |
| 487 | + int ready_pipe[2]; |
| 488 | + int continue_pipe[2]; |
| 489 | + ASSERT_EQ(pipe(ready_pipe), 0); |
| 490 | + ASSERT_EQ(pipe(continue_pipe), 0); |
| 491 | + |
| 492 | + pid_t child = fork(); |
| 493 | + if (child == 0) { |
| 494 | + close(ready_pipe[0]); |
| 495 | + close(continue_pipe[1]); |
| 496 | + char ready_fd[32]; |
| 497 | + char continue_fd[32]; |
| 498 | + snprintf(ready_fd, sizeof(ready_fd), "%d", ready_pipe[1]); |
| 499 | + snprintf(continue_fd, sizeof(continue_fd), "%d", continue_pipe[0]); |
| 500 | + execl(launch_path, launch_path, "__cbm_deleted_self_probe", ready_fd, continue_fd, |
| 501 | + launch_path, (char *)NULL); |
| 502 | + _exit(127); |
| 503 | + } |
| 504 | + ASSERT_GT(child, 0); |
| 505 | + |
| 506 | + close(ready_pipe[1]); |
| 507 | + close(continue_pipe[0]); |
| 508 | + char ready = '\0'; |
| 509 | + ASSERT_EQ(read(ready_pipe[0], &ready, 1), 1); |
| 510 | + ASSERT_EQ(ready, 'R'); |
| 511 | + |
| 512 | +#if defined(__linux__) |
| 513 | + /* Atomic rename-over: the installer's real move. /proc/self/exe now reads |
| 514 | + * "(deleted)" in the child, which is the state the magic-link contract |
| 515 | + * covers. */ |
| 516 | + ASSERT_EQ(cbm_copy_file(current, replacement_path), 0); |
| 517 | + ASSERT_EQ(chmod(replacement_path, 0755), 0); |
| 518 | + ASSERT_EQ(rename(replacement_path, launch_path), 0); |
| 519 | +#else |
| 520 | + /* macOS: a rename-over leaves a VALID executable at the launch path, which |
| 521 | + * resolve is right to return (the worker's build-fingerprint gate is the |
| 522 | + * layer that refuses a mismatched build — covered by its own tests). The |
| 523 | + * fail-closed branch guards the image-GONE case, so remove the image. */ |
| 524 | + (void)replacement_path; |
| 525 | + ASSERT_EQ(unlink(launch_path), 0); |
| 526 | +#endif |
| 527 | + ASSERT_EQ(write(continue_pipe[1], "G", 1), 1); |
| 528 | + close(ready_pipe[0]); |
| 529 | + close(continue_pipe[1]); |
| 530 | + |
| 531 | + int status = 0; |
| 532 | + ASSERT_EQ(waitpid(child, &status, 0), child); |
| 533 | + ASSERT_TRUE(WIFEXITED(status)); |
| 534 | + ASSERT_EQ(WEXITSTATUS(status), 0); |
| 535 | + unlink(launch_path); |
| 536 | + rmdir(tmpdir); |
| 537 | + PASS(); |
| 538 | +#else |
| 539 | + PASS(); |
| 540 | +#endif |
| 541 | +} |
| 542 | + |
460 | 543 | /* ── Transport integration (listener only) ────────────────────── */ |
461 | 544 |
|
462 | 545 | TEST(httpd_listen_ephemeral_port) { |
@@ -2278,6 +2361,7 @@ SUITE(httpd) { |
2278 | 2361 | RUN_TEST(httpd_query_param_edge_cases); |
2279 | 2362 | RUN_TEST(httpd_path_match_matrix); |
2280 | 2363 | RUN_TEST(httpd_resolves_bare_binary_path_from_path); |
| 2364 | + RUN_TEST(httpd_deleted_self_spawn_path_follows_platform_ruling); |
2281 | 2365 | RUN_TEST(repo_info_web_base_normalizes_to_https); |
2282 | 2366 | RUN_TEST(repo_info_strips_credentials_from_remote); |
2283 | 2367 |
|
|
0 commit comments