| 1 | //go:build windows |
| 2 | |
| 3 | package packagegrant |
| 4 | |
| 5 | import ( |
| 6 | "encoding/binary" |
| 7 | "os" |
| 8 | "os/exec" |
| 9 | "path/filepath" |
| 10 | "slices" |
| 11 | "testing" |
| 12 | "unsafe" |
| 13 | |
| 14 | "golang.org/x/sys/windows" |
| 15 | ) |
| 16 | |
| 17 | const testPackageSID = "S-1-15-2-1658335055-2853777128-3014724382-1760870658-2056114279-2893136470-138490047" |
| 18 | |
| 19 | func icacls(t *testing.T, args ...string) { |
| 20 | t.Helper() |
| 21 | if out, err := exec.Command("icacls", args...).CombinedOutput(); err != nil { |
| 22 | t.Fatalf("icacls %v: %v\n%s", args, err, out) |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | // entries lists what path's DACL says about the test package, as "allow", |
| 27 | // "allow-inherited" or "deny", plus whether the DACL is protected. |
| 28 | func entries(t *testing.T, path string) ([]string, bool) { |
| 29 | t.Helper() |
| 30 | sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION) |
| 31 | if err != nil { |
| 32 | t.Fatalf("read %s: %v", path, err) |
| 33 | } |
| 34 | dacl, _, err := sd.DACL() |
| 35 | if err != nil { |
| 36 | t.Fatalf("dacl %s: %v", path, err) |
| 37 | } |
| 38 | control, _, _ := sd.Control() |
| 39 | want, _ := windows.StringToSid(testPackageSID) |
| 40 | var got []string |
| 41 | for i := range uint32(dacl.AceCount) { |
| 42 | var entry *windows.ACCESS_ALLOWED_ACE |
| 43 | if err := windows.GetAce(dacl, i, &entry); err != nil { |
| 44 | t.Fatalf("ace %d: %v", i, err) |
| 45 | } |
| 46 | if !(*windows.SID)(unsafe.Pointer(&entry.SidStart)).Equals(want) { |
| 47 | continue |
| 48 | } |
| 49 | switch { |
| 50 | case entry.Header.AceType == aceTypeAccessAllowed && entry.Header.AceFlags&aceFlagInherited != 0: |
| 51 | got = append(got, "allow-inherited") |
| 52 | case entry.Header.AceType == aceTypeAccessAllowed: |
| 53 | got = append(got, "allow") |
| 54 | default: |
| 55 | got = append(got, "deny") |
| 56 | } |
| 57 | } |
| 58 | return got, control&windows.SE_DACL_PROTECTED != 0 |
| 59 | } |
| 60 | |
| 61 | func tree(t *testing.T) (parent, root, dll string) { |
| 62 | parent = t.TempDir() |
| 63 | root = filepath.Join(parent, "app") |
| 64 | dll = filepath.Join(root, "ffmpeg.dll") |
| 65 | if err := os.MkdirAll(filepath.Join(root, "resources"), 0o755); err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if err := os.WriteFile(dll, []byte("x"), 0o644); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | return parent, root, dll |
| 72 | } |
| 73 | |
| 74 | func TestAGrantOnTheRootLeavesEveryObjectUnderIt(t *testing.T) { |
| 75 | _, root, dll := tree(t) |
| 76 | icacls(t, root, "/grant", "*"+testPackageSID+":(OI)(CI)(RX)") |
| 77 | if got, _ := entries(t, dll); !slices.Equal(got, []string{"allow-inherited"}) { |
| 78 | t.Fatalf("setup: the dll carries %v", got) |
| 79 | } |
| 80 | |
| 81 | report, err := Strip(root) |
| 82 | if err != nil || len(report.Refused) != 0 { |
| 83 | t.Fatalf("strip: %+v, %v", report, err) |
| 84 | } |
| 85 | for _, path := range []string{root, filepath.Join(root, "resources"), dll} { |
| 86 | if got, protected := entries(t, path); len(got) != 0 || protected { |
| 87 | t.Errorf("%s still carries %v (protected=%v)", path, got, protected) |
| 88 | } |
| 89 | } |
| 90 | if !slices.Equal(report.Stripped, []string{root}) { |
| 91 | t.Errorf("stripped %v, want only the root that held the entry", report.Stripped) |
| 92 | } |
| 93 | if _, err := os.ReadFile(dll); err != nil { |
| 94 | t.Fatalf("the pass took this account's own access: %v", err) |
| 95 | } |
| 96 | if again, err := Strip(root); err != nil || len(again.Stripped) != 0 { |
| 97 | t.Fatalf("a second pass changed %v, %v", again.Stripped, err) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestAGrantFromAboveTheTreeIsCutAtTheRootAndTheParentIsUntouched(t *testing.T) { |
| 102 | parent, root, dll := tree(t) |
| 103 | icacls(t, parent, "/grant", "*"+testPackageSID+":(OI)(CI)(RX)") |
| 104 | |
| 105 | if _, err := Strip(root); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | if got, _ := entries(t, parent); !slices.Equal(got, []string{"allow"}) { |
| 109 | t.Fatalf("the parent is not this tree's to change, and now carries %v", got) |
| 110 | } |
| 111 | if got, protected := entries(t, root); len(got) != 0 || !protected { |
| 112 | t.Fatalf("root carries %v, protected=%v", got, protected) |
| 113 | } |
| 114 | if got, _ := entries(t, dll); len(got) != 0 { |
| 115 | t.Fatalf("dll still carries %v", got) |
| 116 | } |
| 117 | if _, err := os.ReadFile(dll); err != nil { |
| 118 | t.Fatalf("the entries root inherited were not kept as its own: %v", err) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func TestAGrantOnOneFileIsRemovedAndADenyStays(t *testing.T) { |
| 123 | _, root, dll := tree(t) |
| 124 | other := filepath.Join(root, "resources", "other.dll") |
| 125 | if err := os.WriteFile(other, []byte("x"), 0o644); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | icacls(t, dll, "/grant", "*"+testPackageSID+":(RX)") |
| 129 | icacls(t, other, "/deny", "*"+testPackageSID+":(RX)") |
| 130 | |
| 131 | report, err := Strip(root) |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | if !slices.Equal(report.Stripped, []string{dll}) { |
| 136 | t.Fatalf("stripped %v, want only %s", report.Stripped, dll) |
| 137 | } |
| 138 | if got, protected := entries(t, dll); len(got) != 0 || protected { |
| 139 | t.Fatalf("dll carries %v, protected=%v", got, protected) |
| 140 | } |
| 141 | if got, _ := entries(t, other); !slices.Equal(got, []string{"deny"}) { |
| 142 | t.Fatalf("a deny narrows access and must stay; other.dll carries %v", got) |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | func TestStripRefusesSomethingThatIsNotADirectory(t *testing.T) { |
| 147 | _, _, dll := tree(t) |
| 148 | if _, err := Strip(dll); err == nil { |
| 149 | t.Fatal("a file was accepted as the tree to strip") |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | func TestTheTestSIDIsAPackageByTheJudgement(t *testing.T) { |
| 154 | s, err := windows.StringToSid(testPackageSID) |
| 155 | if err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | raw := unsafe.Slice((*byte)(unsafe.Pointer(s)), windows.GetLengthSid(s)) |
| 159 | entry := binary.LittleEndian.AppendUint32([]byte{aceTypeAccessAllowed, 0, 0, 0}, 0x1200a9) |
| 160 | entry = append(entry, raw...) |
| 161 | binary.LittleEndian.PutUint16(entry[2:4], uint16(len(entry))) |
| 162 | if !grantsSpecificPackage(entry) { |
| 163 | t.Fatal("the SID these tests grant is not one the judgement recognises, so they prove nothing") |
| 164 | } |
| 165 | } |
| 166 |