Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
======================


fmf-1.8.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The symlink loop detection has been improved to correctly handle
multiple symlinks pointing to the same target directory. Previously,
only the first symlink was followed while subsequent ones were
incorrectly skipped as loops. The detection now tracks ancestor
directories per branch instead of globally across the whole tree.


fmf-1.7.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
16 changes: 5 additions & 11 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,11 @@ def __init__(self, data, name=None, parent=None):
# Special directives
self._directives = dict()

# Store symlinks in while walking tree in grow() to detect
# symlink loops
# Track real paths of ancestor directories to detect symlink loops
if parent is None:
self._symlinkdirs = []
self._realpaths = set()
else:
self._symlinkdirs = parent._symlinkdirs
self._realpaths = set(parent._realpaths)

# Special handling for top parent
if self.parent is None:
Expand Down Expand Up @@ -710,6 +709,7 @@ def grow(self, path):
if path in IGNORED_DIRECTORIES: # pragma: no cover
log.debug("Ignoring '{0}' (special directory).".format(path))
return
self._realpaths.add(os.path.realpath(path))
log.info("Walking through directory {0}".format(
os.path.abspath(path)))
try:
Expand Down Expand Up @@ -756,16 +756,10 @@ def grow(self, path):
continue
fulldir = os.path.join(dirpath, dirname)
if os.path.islink(fulldir):
# According to the documentation, calling os.path.realpath
# with strict = True will raise OSError if a symlink loop
# is encountered. But it does not do that with a loop with
# more than one node
fullpath = os.path.realpath(fulldir)
if fullpath in self._symlinkdirs:
if fullpath in self._realpaths:
log.debug("Not entering symlink loop {}".format(fulldir))
continue
else:
self._symlinkdirs.append(fullpath)
Comment thread
LecrisUT marked this conversation as resolved.

# Ignore metadata subtrees
if os.path.isdir(os.path.join(path, dirname, SUFFIX)):
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,59 @@ def test_validation_invalid_schema(self):
with pytest.raises(fmf.utils.JsonSchemaError):
self.wget.find('/recursion/deep').validate('invalid')

def test_symlink_shared_target(self):
"""
Multiple symlinks pointing to the same directory should all be followed
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you embed the output of tree in here. If I follow this correctly it is:

$ tree -al .
.
├── common
│   └── main.fmf
├── .fmf
│   └── version
├── one
│   ├── main.fmf
│   └── plan -> ../common  [recursive, not followed]
└── two
    ├── main.fmf
    └── plan -> ../common  [recursive, not followed]


directory = tempfile.mkdtemp()
try:
Tree.init(directory)
common = os.path.join(directory, 'common')
os.mkdir(common)
Comment on lines +586 to +587
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let's use pathlib

with open(os.path.join(common, 'main.fmf'), 'w') as main:
main.write('execute:\n how: tmt\n')
for name in ('one', 'two'):
subdir = os.path.join(directory, name)
os.mkdir(subdir)
with open(os.path.join(subdir, 'main.fmf'), 'w') as main:
main.write(f'environment:\n VARIABLE: {name}\n')
os.symlink('../common', os.path.join(subdir, 'plan'))

tree = Tree(directory)
one_plan = tree.find('/one/plan')
two_plan = tree.find('/two/plan')
assert one_plan is not None
assert two_plan is not None
assert one_plan.get('execute') == {'how': 'tmt'}
assert two_plan.get('execute') == {'how': 'tmt'}
assert one_plan.get('environment') == {'VARIABLE': 'one'}
assert two_plan.get('environment') == {'VARIABLE': 'two'}
finally:
rmtree(directory)

def test_symlink_loop(self):
"""
Symlink loops should be detected and silently skipped
"""

directory = tempfile.mkdtemp()
try:
Tree.init(directory)
subdir = os.path.join(directory, 'child')
os.mkdir(subdir)
with open(os.path.join(subdir, 'main.fmf'), 'w') as main:
main.write('key: value\n')
os.symlink('..', os.path.join(subdir, 'loop'))

tree = Tree(directory)
child = tree.find('/child')
assert child is not None
assert child.get('key') == 'value'
assert tree.find('/child/loop') is None
finally:
rmtree(directory)


class TestRemote:
"""
Expand Down
Loading