Skip to content

Commit bba54a8

Browse files
fix: protect nested watch-root traversal from cross-trigger ignores and apply review fixes
Signed-off-by: ManManavadaria <manmanavadaria@gmail.com>
1 parent 9453c6c commit bba54a8

3 files changed

Lines changed: 84 additions & 11 deletions

File tree

pkg/watch/watcher_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (d *fseventNotify) Errors() chan error {
115115
return d.errors
116116
}
117117

118-
func newWatcher(paths []string, _ PathMatcher) (Notify, error) {
118+
func newWatcher(paths []string, _ ...PathMatcher) (Notify, error) {
119119
dw := &fseventNotify{
120120
stream: &fsevents.EventStream{
121121
Latency: 50 * time.Millisecond,

pkg/watch/watcher_naive.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func (d *naiveNotify) watchRecursively(dir string) error {
116116
if err != nil {
117117
if os.IsPermission(err) && d.shouldIgnore(path) {
118118
logrus.Debugf("Ignoring path: %s", path)
119-
return nil
119+
return filepath.SkipDir
120120
}
121121
return err
122122
}
@@ -185,6 +185,10 @@ func (d *naiveNotify) loop() { //nolint:gocyclo
185185
// TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking?
186186
err := filepath.WalkDir(e.Name, func(path string, info fs.DirEntry, err error) error {
187187
if err != nil {
188+
if os.IsPermission(err) && d.shouldIgnore(path) {
189+
logrus.Debugf("Ignoring path: %s", path)
190+
return filepath.SkipDir
191+
}
188192
return err
189193
}
190194

@@ -245,11 +249,6 @@ func (d *naiveNotify) shouldSkipDir(path string) bool {
245249
return false
246250
}
247251

248-
// If path is present in the ignore list then we should ignore it
249-
if d.shouldIgnore(path) {
250-
return true
251-
}
252-
253252
// Suppose we're watching
254253
// /src/.tiltignore
255254
// but the .tiltignore file doesn't exist.
@@ -262,15 +261,28 @@ func (d *naiveNotify) shouldSkipDir(path string) bool {
262261
// - A child of a directory that's in our notify list, or
263262
// - A parent of a directory that's in our notify list
264263
// (i.e., to cover the "path doesn't exist" case).
264+
//
265+
// We prioritize "parent of watched path" checks before ignore checks so
266+
// one trigger's ignore rules can't hide another trigger's nested watch root.
267+
isChildOfWatchedDir := false
265268
for root := range d.notifyList {
266-
if pathutil.IsChild(root, path) || pathutil.IsChild(path, root) {
269+
if pathutil.IsChild(path, root) {
267270
return false
268271
}
272+
if pathutil.IsChild(root, path) {
273+
isChildOfWatchedDir = true
274+
}
269275
}
270-
return true
276+
277+
// skip the dir if ignore rules match this full subtree.
278+
if d.shouldIgnoreEntireDir(path) {
279+
return true
280+
}
281+
282+
return !isChildOfWatchedDir
271283
}
272284

273-
func (d *naiveNotify) shouldIgnore(path string) bool {
285+
func (d *naiveNotify) shouldIgnoreEntireDir(path string) bool {
274286
if d.ignore == nil {
275287
return false
276288
}
@@ -282,7 +294,14 @@ func (d *naiveNotify) shouldIgnore(path string) bool {
282294
if matches {
283295
return true
284296
}
285-
matches, err = d.ignore.Matches(path)
297+
return false
298+
}
299+
300+
func (d *naiveNotify) shouldIgnore(path string) bool {
301+
if d.ignore == nil {
302+
return false
303+
}
304+
matches, err := d.ignore.Matches(path)
286305
if err != nil {
287306
logrus.Debugf("error checking ignored path %q: %v", path, err)
288307
return false

pkg/watch/watcher_naive_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,57 @@ func TestDontRecurseWhenWatchingParentsOfNonExistentFiles(t *testing.T) {
157157
t.Fatalf("watching more than 5 files: %d", n)
158158
}
159159
}
160+
161+
func TestShouldSkipDirWithNegatedChildException(t *testing.T) {
162+
repoRoot := t.TempDir()
163+
ignore, err := DockerIgnoreTesterFromContents(repoRoot, "bazel-bin/\n!bazel-bin/app-binary\n")
164+
assert.NilError(t, err)
165+
166+
d := &naiveNotify{
167+
ignore: ignore,
168+
notifyList: map[string]bool{repoRoot: true},
169+
}
170+
171+
bazelBin := filepath.Join(repoRoot, "bazel-bin")
172+
assert.Assert(t, !d.shouldSkipDir(bazelBin), "expected bazel-bin to remain traversable for negated child patterns")
173+
}
174+
175+
func TestShouldIgnorePathStillMatchesDirectoryPattern(t *testing.T) {
176+
repoRoot := t.TempDir()
177+
ignore, err := DockerIgnoreTesterFromContents(repoRoot, "bazel-bin/\n!bazel-bin/app-binary\n")
178+
assert.NilError(t, err)
179+
180+
d := &naiveNotify{ignore: ignore}
181+
182+
bazelBin := filepath.Join(repoRoot, "bazel-bin")
183+
assert.Assert(t, d.shouldIgnore(bazelBin), "expected bazel-bin path to match ignore pattern")
184+
}
185+
186+
func TestShouldSkipDirForIgnoredSubtreeWithoutException(t *testing.T) {
187+
repoRoot := t.TempDir()
188+
ignore, err := DockerIgnoreTesterFromContents(repoRoot, "bazel-bin/\n")
189+
assert.NilError(t, err)
190+
191+
d := &naiveNotify{
192+
ignore: ignore,
193+
notifyList: map[string]bool{repoRoot: true},
194+
}
195+
196+
bazelBin := filepath.Join(repoRoot, "bazel-bin")
197+
assert.Assert(t, d.shouldSkipDir(bazelBin), "expected fully ignored directory subtree to be skipped")
198+
}
199+
200+
func TestShouldSkipDirDoesNotSkipAncestorOfWatchedPath(t *testing.T) {
201+
repoRoot := t.TempDir()
202+
ignore, err := DockerIgnoreTesterFromContents(repoRoot, "parent/\n")
203+
assert.NilError(t, err)
204+
205+
watchedPath := filepath.Join(repoRoot, "parent", "child", "non-existent")
206+
d := &naiveNotify{
207+
ignore: ignore,
208+
notifyList: map[string]bool{watchedPath: true},
209+
}
210+
211+
parent := filepath.Join(repoRoot, "parent")
212+
assert.Assert(t, !d.shouldSkipDir(parent), "expected parent directory to remain traversable when it contains a watched path")
213+
}

0 commit comments

Comments
 (0)