diff options
| author | Danny Milosavljevic <dannym@friendly-machines.com> | 2025-08-02 17:45:45 +0200 |
|---|---|---|
| committer | Danny Milosavljevic <dannym@friendly-machines.com> | 2025-10-05 20:30:45 +0200 |
| commit | 548f225f3131b66d09fd4aa10750e47b93f146d0 (patch) | |
| tree | c80e5553dbba8a566db86aa1396c3347f1d4eb68 /tests | |
| parent | 12fc06db4b3273a3d183ed71ba95d128a6e11df1 (diff) | |
grafts: Always depend on all the outputs of the original derivation.
Fixes <https://bugs.gnu.org/75157>.
Fixes problem introduced with commit 482fda2729c3e76999892cb8f9a0391a7bd37119.
* guix/grafts.scm (cumulative-grafts): Remove parameter "outputs" and replace
it by always all outputs.
(graft-derivation): Fix calls of cumulative-grafts.
* tests/grafts.scm (graft-derivation with #:outputs): Remove.
(graft-derivation, no applicable grafts): Add.
(graft-derivation, unused outputs not depended on): Remove.
(graft-derivation, multi-output graft determinism): Add.
(graft-derivation, consistent cache keys): Add.
Change-Id: Ice924a45c483d6fd1acc9221a0ec650abb039610
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/grafts.scm | 311 |
1 files changed, 248 insertions, 63 deletions
diff --git a/tests/grafts.scm b/tests/grafts.scm index 24c4d243598..5a91678b8f1 100644 --- a/tests/grafts.scm +++ b/tests/grafts.scm | |||
| @@ -316,9 +316,9 @@ | |||
| 316 | (equal? (stat (string-append out "/one/p0/replacement")) | 316 | (equal? (stat (string-append out "/one/p0/replacement")) |
| 317 | (stat (string-append out "/two/link/p0/replacement")))))) | 317 | (stat (string-append out "/two/link/p0/replacement")))))) |
| 318 | 318 | ||
| 319 | (test-assert "graft-derivation with #:outputs" | 319 | (test-assert "graft-derivation, no applicable grafts" |
| 320 | ;; Call 'graft-derivation' with a narrowed set of outputs passed as | 320 | ;; This test verifies that when grafts don't apply to any dependencies, |
| 321 | ;; #:outputs. | 321 | ;; the original derivation is returned unchanged. |
| 322 | (let* ((p1 (build-expression->derivation | 322 | (let* ((p1 (build-expression->derivation |
| 323 | %store "p1" | 323 | %store "p1" |
| 324 | `(let ((one (assoc-ref %outputs "one")) | 324 | `(let ((one (assoc-ref %outputs "one")) |
| @@ -348,69 +348,11 @@ | |||
| 348 | (origin-output "one") | 348 | (origin-output "one") |
| 349 | (replacement p1r) | 349 | (replacement p1r) |
| 350 | (replacement-output "ONE"))) | 350 | (replacement-output "ONE"))) |
| 351 | (p2g (graft-derivation %store p2 (list p1g) | 351 | ;; Note: #:outputs parameter removed - now always uses all outputs |
| 352 | #:outputs '("aaa")))) | 352 | (p2g (graft-derivation %store p2 (list p1g)))) |
| 353 | ;; P2:aaa depends on P1:two, but not on P1:one, so nothing to graft. | 353 | ;; P2:aaa depends on P1:two, but not on P1:one, so nothing to graft. |
| 354 | (eq? p2g p2))) | 354 | (eq? p2g p2))) |
| 355 | 355 | ||
| 356 | (test-equal "graft-derivation, unused outputs not depended on" | ||
| 357 | '("aaa") | ||
| 358 | |||
| 359 | ;; Make sure that the result of 'graft-derivation' does not pull outputs | ||
| 360 | ;; that are irrelevant to the grafting process. See | ||
| 361 | ;; <http://bugs.gnu.org/24886>. | ||
| 362 | (let* ((p1 (build-expression->derivation | ||
| 363 | %store "p1" | ||
| 364 | `(let ((one (assoc-ref %outputs "one")) | ||
| 365 | (two (assoc-ref %outputs "two"))) | ||
| 366 | (mkdir one) | ||
| 367 | (mkdir two)) | ||
| 368 | #:outputs '("one" "two"))) | ||
| 369 | (p1r (build-expression->derivation | ||
| 370 | %store "P1" | ||
| 371 | `(let ((other (assoc-ref %outputs "ONE"))) | ||
| 372 | (mkdir other) | ||
| 373 | (call-with-output-file (string-append other "/replacement") | ||
| 374 | (const #t))) | ||
| 375 | #:outputs '("ONE"))) | ||
| 376 | (p2 (build-expression->derivation | ||
| 377 | %store "p2" | ||
| 378 | `(let ((aaa (assoc-ref %outputs "aaa")) | ||
| 379 | (zzz (assoc-ref %outputs "zzz"))) | ||
| 380 | (mkdir zzz) (chdir zzz) | ||
| 381 | (symlink (assoc-ref %build-inputs "p1:two") "two") | ||
| 382 | (mkdir aaa) (chdir aaa) | ||
| 383 | (symlink (assoc-ref %build-inputs "p1:one") "one")) | ||
| 384 | #:outputs '("aaa" "zzz") | ||
| 385 | #:inputs `(("p1:one" ,p1 "one") | ||
| 386 | ("p1:two" ,p1 "two")))) | ||
| 387 | (p1g (graft | ||
| 388 | (origin p1) | ||
| 389 | (origin-output "one") | ||
| 390 | (replacement p1r) | ||
| 391 | (replacement-output "ONE"))) | ||
| 392 | (p2g (graft-derivation %store p2 (list p1g) | ||
| 393 | #:outputs '("aaa")))) | ||
| 394 | |||
| 395 | ;; Here P2G should only depend on P1:one and P1R:one; it must not depend | ||
| 396 | ;; on P1:two or P1R:two since these are unused in the grafting process. | ||
| 397 | (and (not (eq? p2g p2)) | ||
| 398 | (let* ((inputs (derivation-inputs p2g)) | ||
| 399 | (match-input (lambda (drv) | ||
| 400 | (lambda (input) | ||
| 401 | (string=? (derivation-input-path input) | ||
| 402 | (derivation-file-name drv))))) | ||
| 403 | (p1-inputs (filter (match-input p1) inputs)) | ||
| 404 | (p1r-inputs (filter (match-input p1r) inputs)) | ||
| 405 | (p2-inputs (filter (match-input p2) inputs))) | ||
| 406 | (and (equal? p1-inputs | ||
| 407 | (list (derivation-input p1 '("one")))) | ||
| 408 | (equal? p1r-inputs | ||
| 409 | (list (derivation-input p1r '("ONE")))) | ||
| 410 | (equal? p2-inputs | ||
| 411 | (list (derivation-input p2 '("aaa")))) | ||
| 412 | (derivation-output-names p2g)))))) | ||
| 413 | |||
| 414 | (test-assert "graft-derivation, renaming" ;<http://bugs.gnu.org/23132> | 356 | (test-assert "graft-derivation, renaming" ;<http://bugs.gnu.org/23132> |
| 415 | (let* ((build `(begin | 357 | (let* ((build `(begin |
| 416 | (use-modules (guix build utils)) | 358 | (use-modules (guix build utils)) |
| @@ -601,5 +543,248 @@ | |||
| 601 | ;; char-size1 values to test | 543 | ;; char-size1 values to test |
| 602 | '(1 2 4)) | 544 | '(1 2 4)) |
| 603 | 545 | ||
| 546 | (test-assert "graft-derivation, multi-output graft determinism" | ||
| 547 | ;; THE BUG: In earlier broken code, cumulative-grafts cached by | ||
| 548 | ;; (drv, outputs, grafts). | ||
| 549 | ;; Same derivation with different outputs -> different cache entries -> | ||
| 550 | ;; different grafted derivations created. This causes runtime crashes: | ||
| 551 | ;; | ||
| 552 | ;; When a process dlopens multiple libraries that depend on the same | ||
| 553 | ;; multi-output package but were grafted with different output sets: | ||
| 554 | ;; - Library A built against grafted-glib-A (only :out output) | ||
| 555 | ;; - Library B built against grafted-glib-B (:out + :bin outputs) | ||
| 556 | ;; | ||
| 557 | ;; At runtime, dlopen loads Library A which loads grafted-glib-A's libglib.so. | ||
| 558 | ;; Then dlopen loads Library B which loads grafted-glib-B's libglib.so. | ||
| 559 | ;; Now two different libglib.so binaries are loaded in the same process! | ||
| 560 | ;; | ||
| 561 | ;; Specific to GLib: The GObject type registry cannot register two objects with | ||
| 562 | ;; the same name. When both glib binaries try to register their types, the | ||
| 563 | ;; second registration fails/conflicts. Result: type registration errors, | ||
| 564 | ;; vtable corruption, and segfaults. | ||
| 565 | ;; | ||
| 566 | ;; THE FIX: Always use ALL outputs when grafting. Only one grafted glib | ||
| 567 | ;; derivation is ever created (with all outputs). The :out output is identical | ||
| 568 | ;; for all consumers, regardless of which outputs they actually use. | ||
| 569 | ;; | ||
| 570 | ;; This test verifies that gtk+ and a direct glib reference use the same | ||
| 571 | ;; glib:out store path. | ||
| 572 | ;; | ||
| 573 | ;; This situation occurs in production when a profile/manifest contains both | ||
| 574 | ;; a library (like gtk+) and that library's dependency (like glib). | ||
| 575 | ;; | ||
| 576 | ;; For example: | ||
| 577 | ;; (packages->manifest (list gtk+ glib)) | ||
| 578 | ;; or when a package has both in propagated-inputs: | ||
| 579 | ;; (propagated-inputs (list gtk+ glib)) | ||
| 580 | ;; | ||
| 581 | ;; When building the profile, both gtk+ and glib are grafted via | ||
| 582 | ;; package->derivation, causing glib to be processed twice with different | ||
| 583 | ;; output sets. | ||
| 584 | ;; | ||
| 585 | ;; That's because graft-derivation is never (and was never) called with | ||
| 586 | ;; #:outputs--so it defaults to ALL outputs. | ||
| 587 | (let* (;; pcre2 - a dependency of glib | ||
| 588 | (pcre2 (build-expression->derivation | ||
| 589 | %store "pcre2" | ||
| 590 | `(let ((out (assoc-ref %outputs "out"))) | ||
| 591 | (use-modules (guix build utils)) | ||
| 592 | (mkdir-p (string-append out "/lib")) | ||
| 593 | (call-with-output-file | ||
| 594 | (string-append out "/lib/libpcre2.txt") | ||
| 595 | (lambda (port) (display "pcre2" port)))) | ||
| 596 | #:modules '((guix build utils)))) | ||
| 597 | ;; Create multi-output glib that depends on pcre2 (like real glib). | ||
| 598 | (glib (build-expression->derivation | ||
| 599 | %store "glib" | ||
| 600 | `(let ((out (assoc-ref %outputs "out")) | ||
| 601 | (bin (assoc-ref %outputs "bin")) | ||
| 602 | (doc (assoc-ref %outputs "doc")) | ||
| 603 | (debug (assoc-ref %outputs "debug")) | ||
| 604 | (pcre2 (assoc-ref %build-inputs "pcre2"))) | ||
| 605 | (use-modules (guix build utils)) | ||
| 606 | (mkdir-p (string-append out "/lib")) | ||
| 607 | (mkdir-p (string-append bin "/bin")) | ||
| 608 | (mkdir doc) | ||
| 609 | (mkdir-p (string-append debug "/lib/debug")) | ||
| 610 | (call-with-output-file | ||
| 611 | (string-append out "/lib/libglib-2.0.txt") | ||
| 612 | (lambda (port) | ||
| 613 | (display out port))) | ||
| 614 | (symlink (string-append pcre2 "/lib/libpcre2.txt") | ||
| 615 | (string-append out "/pcre2-link")) | ||
| 616 | ;; bin output references "out" output. | ||
| 617 | (call-with-output-file | ||
| 618 | (string-append bin "/bin/glib-compile-schemas") | ||
| 619 | (lambda (port) | ||
| 620 | (display (string-append out "/lib/libglib-2.0.txt") | ||
| 621 | port))) | ||
| 622 | ;; debug output references "bin" output. | ||
| 623 | (call-with-output-file | ||
| 624 | (string-append debug "/lib/debug/glib-bin-path.txt") | ||
| 625 | (lambda (port) | ||
| 626 | (display (string-append bin "/bin/glib-compile-schemas") | ||
| 627 | port)))) | ||
| 628 | #:modules '((guix build utils)) | ||
| 629 | #:inputs `(("pcre2" ,pcre2)) | ||
| 630 | #:outputs '("out" "bin" "doc" "debug"))) | ||
| 631 | ;; Create patched glib (security fix) - also depends on pcre2. | ||
| 632 | (glib-patched (build-expression->derivation | ||
| 633 | %store "gliB" | ||
| 634 | `(let ((out (assoc-ref %outputs "out")) | ||
| 635 | (bin (assoc-ref %outputs "bin")) | ||
| 636 | (doc (assoc-ref %outputs "doc")) | ||
| 637 | (debug (assoc-ref %outputs "debug")) | ||
| 638 | (pcre2 (assoc-ref %build-inputs "pcre2"))) | ||
| 639 | (use-modules (guix build utils)) | ||
| 640 | (mkdir-p (string-append out "/lib")) | ||
| 641 | (mkdir-p (string-append bin "/bin")) | ||
| 642 | (mkdir doc) | ||
| 643 | (mkdir-p (string-append debug "/lib/debug")) | ||
| 644 | (call-with-output-file | ||
| 645 | (string-append out "/lib/libglib-2.0.txt") | ||
| 646 | (lambda (port) | ||
| 647 | (display out port))) | ||
| 648 | (symlink (string-append pcre2 "/lib/libpcre2.txt") | ||
| 649 | (string-append out "/pcre2-link")) | ||
| 650 | ;; bin output references "out" output. | ||
| 651 | (call-with-output-file | ||
| 652 | (string-append bin "/bin/glib-compile-schemas") | ||
| 653 | (lambda (port) | ||
| 654 | (display (string-append out | ||
| 655 | "/lib/libglib-2.0.txt") | ||
| 656 | port))) | ||
| 657 | ;; debug output references "bin" output. | ||
| 658 | (call-with-output-file | ||
| 659 | (string-append debug | ||
| 660 | "/lib/debug/glib-bin-path.txt") | ||
| 661 | (lambda (port) | ||
| 662 | (display (string-append bin | ||
| 663 | "/bin/glib-compile-schemas") | ||
| 664 | port)))) | ||
| 665 | #:modules '((guix build utils)) | ||
| 666 | #:inputs `(("pcre2" ,pcre2)) | ||
| 667 | #:outputs '("out" "bin" "doc" "debug"))) | ||
| 668 | ;; gtk+ needs only glib:out. | ||
| 669 | (gtk+ (build-expression->derivation | ||
| 670 | %store "gtk+" | ||
| 671 | `(let ((glib (assoc-ref %build-inputs "glib"))) | ||
| 672 | (use-modules (guix build utils)) | ||
| 673 | (mkdir-p (string-append %output "/lib")) | ||
| 674 | (call-with-output-file | ||
| 675 | (string-append %output "/lib/libgtk-3.txt") | ||
| 676 | (lambda (port) | ||
| 677 | (display (string-append glib "/lib/libglib-2.0.txt") | ||
| 678 | port)))) | ||
| 679 | #:modules '((guix build utils)) | ||
| 680 | #:inputs `(("glib" ,glib "out")))) | ||
| 681 | ;; Patched pcre2 (for example security fix). | ||
| 682 | (pcre2-patched (build-expression->derivation | ||
| 683 | %store "Pcre2" | ||
| 684 | `(let ((out (assoc-ref %outputs "out"))) | ||
| 685 | (use-modules (guix build utils)) | ||
| 686 | (mkdir-p (string-append out "/lib")) | ||
| 687 | (call-with-output-file | ||
| 688 | (string-append out "/lib/libpcre2.txt") | ||
| 689 | (lambda (port) | ||
| 690 | (display "pcre2-patched" port)))) | ||
| 691 | #:modules '((guix build utils)))) | ||
| 692 | ;; Define graft to fix pcre2 vulnerability (glib's dependency). | ||
| 693 | (pcre2-graft (graft | ||
| 694 | (origin pcre2) | ||
| 695 | (origin-output "out") | ||
| 696 | (replacement pcre2-patched) | ||
| 697 | (replacement-output "out"))) | ||
| 698 | ;; FIRST: Graft gtk+ which depends on glib:out. | ||
| 699 | ;; Buggy: gtk+ -> glib with outputs=("out") | ||
| 700 | ;; Cache key: (glib, ("out"), (pcre2-graft)) | ||
| 701 | ;; Creates grafted-glib-OUT-ONLY | ||
| 702 | ;; Fixed: Always uses all outputs | ||
| 703 | (gtk-grafted (graft-derivation %store gtk+ (list pcre2-graft))) | ||
| 704 | ;; SECOND: Graft glib directly - uses all outputs by default. | ||
| 705 | ;; Buggy: cumulative-grafts(glib, outputs=ALL) | ||
| 706 | ;; Cache key: (glib, ALL, (pcre2-graft)) - different! | ||
| 707 | ;; Creates grafted-glib-ALL. | ||
| 708 | ;; Fixed: Same cache key as gtk+ path (always ALL). | ||
| 709 | (glib-grafted (graft-derivation %store glib (list pcre2-graft)))) | ||
| 710 | ;; Build both grafted derivations | ||
| 711 | (build-derivations %store (list gtk-grafted glib-grafted)) | ||
| 712 | (let* (;; Get glib:out path from direct graft | ||
| 713 | (glib-out-direct (derivation->output-path glib-grafted "out")) | ||
| 714 | ;; Get glib:out path from gtk+'s perspective. | ||
| 715 | (gtk-path (derivation->output-path gtk-grafted)) | ||
| 716 | (gtk-glib-ref (call-with-input-file | ||
| 717 | (string-append gtk-path "/lib/libgtk-3.txt") | ||
| 718 | get-string-all))) | ||
| 719 | (pk 'glib-out-direct glib-out-direct) | ||
| 720 | (pk 'gtk-glib-ref gtk-glib-ref) | ||
| 721 | ;; BROKEN: gtk+ sees a different glib:out than the direct graft. | ||
| 722 | ;; FIXED: They're the same. | ||
| 723 | (string=? (string-append glib-out-direct "/lib/libglib-2.0.txt") | ||
| 724 | gtk-glib-ref)))) | ||
| 725 | |||
| 726 | (test-assert "graft-derivation, consistent cache keys" | ||
| 727 | ;; Test that cumulative-grafts produces consistent cache keys regardless | ||
| 728 | ;; of the calling context, preventing bug <https://bugs.gnu.org/75157>. | ||
| 729 | ;; | ||
| 730 | ;; The fix ensures that calling graft-derivation multiple times on the | ||
| 731 | ;; same derivation always produces the same result, regardless of context. | ||
| 732 | (let* (;; Create a multi-output package. | ||
| 733 | (base-pkg (build-expression->derivation | ||
| 734 | %store "base-pkg" | ||
| 735 | `(let ((out (assoc-ref %outputs "out")) | ||
| 736 | (lib (assoc-ref %outputs "lib"))) | ||
| 737 | (mkdir out) (mkdir lib) | ||
| 738 | (call-with-output-file (string-append out "/binary") | ||
| 739 | (lambda (port) (display "base-binary" port))) | ||
| 740 | (call-with-output-file (string-append lib "/library") | ||
| 741 | (lambda (port) (display "base-library" port)))) | ||
| 742 | #:outputs '("out" "lib"))) | ||
| 743 | ;; Create dependency that needs grafting. | ||
| 744 | (dep-orig (build-expression->derivation | ||
| 745 | %store "dep-orig" | ||
| 746 | `(begin (mkdir %output) | ||
| 747 | (call-with-output-file | ||
| 748 | (string-append %output "/data") | ||
| 749 | (lambda (port) | ||
| 750 | (display "vulnerable-data" port)))))) | ||
| 751 | (dep-fixed (build-expression->derivation | ||
| 752 | %store "dep-fixed" | ||
| 753 | `(begin (mkdir %output) | ||
| 754 | (call-with-output-file | ||
| 755 | (string-append %output "/data") | ||
| 756 | (lambda (port) | ||
| 757 | (display "secure-data" port)))))) | ||
| 758 | ;; Create the multi-output package that depends on the vulnerable dep. | ||
| 759 | (multi-pkg (build-expression->derivation | ||
| 760 | %store "multi-pkg" | ||
| 761 | `(let ((out (assoc-ref %outputs "out")) | ||
| 762 | (lib (assoc-ref %outputs "lib")) | ||
| 763 | (debug (assoc-ref %outputs "debug"))) | ||
| 764 | (mkdir out) (mkdir lib) (mkdir debug) | ||
| 765 | ;; Both outputs depend on the vulnerable dependency. | ||
| 766 | (symlink (assoc-ref %build-inputs "dep") | ||
| 767 | (string-append out "/dep-link")) | ||
| 768 | (symlink (assoc-ref %build-inputs "dep") | ||
| 769 | (string-append lib "/dep-link"))) | ||
| 770 | #:outputs '("out" "lib" "debug") | ||
| 771 | #:inputs `(("dep" ,dep-orig)))) | ||
| 772 | ;; Define graft to fix the vulnerability. | ||
| 773 | (security-graft (graft | ||
| 774 | (origin dep-orig) | ||
| 775 | (replacement dep-fixed))) | ||
| 776 | ;; Scenario 1: Something requests just the "out" output. | ||
| 777 | (result1 (graft-derivation %store multi-pkg (list security-graft))) | ||
| 778 | ;; Scenario 2: Something requests just the "lib" output. | ||
| 779 | (result2 (graft-derivation %store multi-pkg (list security-graft))) | ||
| 780 | ;; Critical test: both scenarios should produce the SAME derivation | ||
| 781 | ;; because cumulative-grafts now uses canonical outputs for caching. | ||
| 782 | (same-result? (equal? result1 result2)) | ||
| 783 | ;; Verify the result has all outputs. | ||
| 784 | (has-all-outputs? | ||
| 785 | (and (member "out" (derivation-output-names result1)) | ||
| 786 | (member "lib" (derivation-output-names result1)) | ||
| 787 | (member "debug" (derivation-output-names result1))))) | ||
| 788 | (and same-result? has-all-outputs?))) | ||
| 604 | 789 | ||
| 605 | (test-end) | 790 | (test-end) |
