@@ -197,6 +197,11 @@ func traverseUp(dir string, walkFn func(dir string, owner int) error) error {
197// traverseUp instead. If stopDir is set but is not an ancestor of dir
197// traverseUp instead. If stopDir is set but is not an ancestor of dir
198// the walk still stops at the filesystem root, so callers cannot
198// the walk still stops at the filesystem root, so callers cannot
199// accidentally produce an infinite walk by passing a sibling path.
199// accidentally produce an infinite walk by passing a sibling path.
201+// Boundary comparison is performed against symlink-resolved paths so
202+// that callers passing logically equivalent paths (a symlinked /var vs
203+// the underlying /private/var, for example) still terminate at the
204+// expected directory.
200func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) error) error {
205func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) error) error {
201 cwd, err := filepath.Abs(dir)
206 cwd, err := filepath.Abs(dir)
202 if err != nil {
207 if err != nil {
@@ -210,6 +215,7 @@ func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) e
210 return fmt.Errorf("cannot convert stop dir to absolute path: %w", err)
215 return fmt.Errorf("cannot convert stop dir to absolute path: %w", err)
218+ canonStop := canonicalize(stop)
214 owner, err := Owner(dir)
220 owner, err := Owner(dir)
215 if err != nil {
221 if err != nil {
@@ -219,7 +225,7 @@ func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) e
220 err := walkFn(cwd, owner)
226 err := walkFn(cwd, owner)
221 if err == nil || errors.Is(err, filepath.SkipDir) {
227 if err == nil || errors.Is(err, filepath.SkipDir) {
222− if cwd == stop {
228+ if canonicalize(cwd) == canonStop {
223 return nil
229 return nil
@@ -240,6 +246,16 @@ func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) e
249+// canonicalize resolves any symbolic links in path. If resolution fails
250+// (typically because path does not exist yet) the original path is
251+// returned cleaned, so callers can still perform stable equality checks.
252+func canonicalize(path string) string {
253+ if resolved, err := filepath.EvalSymlinks(path); err == nil {
256+ return filepath.Clean(path)
243// probeEnt checks if entity at given path exists and belongs to given owner
259// probeEnt checks if entity at given path exists and belongs to given owner
244func probeEnt(fspath string, owner int) error {
260func probeEnt(fspath string, owner int) error {
245 _, err := os.Stat(fspath)
261 _, err := os.Stat(fspath)