Commit ad68d0e
fix: verify the working directory rather than trust the window
The last commit moved the race from a file inode to a directory inode.
mkdir creates 0777 against the umask, so under 0002 - ordinary - or 0000
the new directory is briefly writable by others, and nothing checked
that it was still empty when the chmod landed. The child names are
predictable, and create_private/1 used File.write/2, which follows a
symlink: an interloper plants sys.config pointing at a file it can read,
keeps a descriptor on that file, and receives the configuration written
through it. Measured, against the previous commit: the target is
truncated, chmodded to 0600 and filled.
The justification was the flaw, not the mechanism. It said the window
was harmless because it covered an empty directory, and offered that
traversal is checked on every lookup rather than captured in a
descriptor. Both halves are true and both are about reading. An empty
directory is safe to read. It is not safe to write into, and the attack
wants a name inside it, not a handle on it.
So work_dir/1 now creates, narrows, and then verifies: File.ls must come
back empty after the chmod, or the directory is removed and the install
refused with the entry named and the umask pointed at. Verification is
the point rather than the belt to a braces - it does not depend on any
judgement about whether a window is harmless, which is the judgement
that has now been wrong five times running in this module. AGENTS.md
says that as the rule, and preserves the argument it replaced so nobody
reconstructs it from first principles.
create_private/1 creates exclusively. This is a different use of
:exclusive from the one measured two commits ago and the comment keeps
them apart: there it was asked to settle the mode and does not, here it
is asked to refuse a name that is already there and does - eexist for a
regular file, a symlink, and a symlink to nothing, the last without
bringing its target into existence. Exclusivity says nothing about the
permissions an inode arrives with, and a private directory says nothing
about a name already inside it, so neither substitutes for the other.
Which makes write_private/2 single-use per path. Both call sites already
were; the one test that wrote a path twice was testing the mode, and now
uses two.
Audited the rest of the module for predictable names it creates. The
working directory's own name is refused if taken, and File.mkdir/1 is
what refuses it - measured against a directory, a regular file, a
symlink to a directory and a dangling symlink, all eexist, since
{:mode, _} being silently ignored on open was the same category of
surprise. sys.config is replaced by File.rename/2, which replaces a
symlink at that name rather than writing through it, target untouched.
The base is published by File.ln/2, which refuses eexist against all
three shapes and creates nothing from a dangling one. What follows an
eexist there is a read of whatever holds the name, so a symlink planted
at sys.config.pristine would be read through - but planting it needs
write permission on the version directory, and an account with that can
write its own configuration into the file directly. That is the boundary
already documented, not a new hole. All recorded in the module comment.
The test that discriminates stands in the mkdir-to-chmod window, which
is why secure_dir/1 is public: a directory at 0777 with a sys.config
symlink planted in it is refused, removed, and what the symlink pointed
at is neither truncated nor written through - File.rm_rf/1 unlinks a
symlink rather than following it, also measured. Its companion plants a
name inside an already-private directory and asserts write_private/2
refuses rather than writing through it, which is the half the directory
does not cover; against 1f726fc that one returns :ok, follows the link
and hands over the file. Both fail against 1f726fc, the second with the
disclosure visible.
Refs: #13
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4oaMvbR1cbxrZBj8qwkqN1 parent 1f726fc commit ad68d0e
3 files changed
Lines changed: 271 additions & 64 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
105 | 132 | | |
106 | 133 | | |
107 | 134 | | |
| |||
112 | 139 | | |
113 | 140 | | |
114 | 141 | | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
120 | 147 | | |
121 | 148 | | |
122 | 149 | | |
| |||
352 | 379 | | |
353 | 380 | | |
354 | 381 | | |
355 | | - | |
356 | | - | |
357 | | - | |
358 | | - | |
359 | | - | |
360 | | - | |
361 | | - | |
362 | | - | |
363 | | - | |
364 | | - | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
365 | 400 | | |
366 | 401 | | |
367 | 402 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
599 | 599 | | |
600 | 600 | | |
601 | 601 | | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
611 | | - | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
612 | 632 | | |
613 | 633 | | |
614 | 634 | | |
615 | 635 | | |
616 | 636 | | |
617 | 637 | | |
618 | 638 | | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
619 | 651 | | |
620 | 652 | | |
621 | 653 | | |
| |||
663 | 695 | | |
664 | 696 | | |
665 | 697 | | |
666 | | - | |
667 | | - | |
668 | | - | |
669 | | - | |
670 | | - | |
671 | | - | |
672 | | - | |
673 | | - | |
674 | | - | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
675 | 703 | | |
676 | 704 | | |
677 | 705 | | |
678 | 706 | | |
679 | 707 | | |
680 | | - | |
681 | | - | |
682 | | - | |
683 | | - | |
| 708 | + | |
| 709 | + | |
684 | 710 | | |
685 | | - | |
686 | | - | |
687 | | - | |
688 | | - | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
689 | 730 | | |
690 | 731 | | |
691 | 732 | | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
692 | 755 | | |
693 | 756 | | |
694 | 757 | | |
| |||
704 | 767 | | |
705 | 768 | | |
706 | 769 | | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
707 | 776 | | |
708 | 777 | | |
709 | 778 | | |
710 | | - | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
711 | 799 | | |
712 | 800 | | |
713 | 801 | | |
| |||
0 commit comments