Skip to content

Commit bdd6833

Browse files
committed
Rework _copy_decouple to follow relative symlinks and symlinks to directories.
The previous code handled absolute symlinks fine but when there were relative symlinks it would traceback. Additionally, it did not handle symlinks to directories that occurred outside of /etc/pki. This should fix both of those.
1 parent 3be2b5b commit bdd6833

File tree

1 file changed

+127
-43
lines changed
  • repos/system_upgrade/common/actors/targetuserspacecreator/libraries

1 file changed

+127
-43
lines changed

repos/system_upgrade/common/actors/targetuserspacecreator/libraries/userspacegen.py

Lines changed: 127 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ def _check_deprecated_rhsm_skip():
7373
)
7474

7575

76+
class BrokenSymlinkError(Exception):
77+
"""Raised when we encounter a broken symlink where we weren't expecting it."""
78+
79+
7680
class _InputData(object):
7781
def __init__(self):
7882
self._consume_data()
@@ -328,68 +332,144 @@ def _get_files_owned_by_rpms(context, dirpath, pkgs=None, recursive=False):
328332
return files_owned_by_rpms
329333

330334

335+
def _mkdir_with_copied_mode(path, mode_from):
336+
"""
337+
Create directories with a file to copy the mode from.
338+
339+
:param path: The directory path to create.
340+
:param mode_from: A file or directory whose mode we will copy to the
341+
newly created directory.
342+
:raises subprocess.CalledProcessError: mkdir or chmod fails. For instance,
343+
the directory already exists, the file to get permissions from does
344+
not exist, a parent directory does not exist.
345+
"""
346+
# Create with maximally restrictive permissions
347+
run(['mkdir', '-m', '0', '-p', path])
348+
run(['chmod', '--reference={}'.format(mode_from), path])
349+
350+
351+
def _copy_or_link(symlink, srcdir):
352+
"""
353+
Copy file contents or create a symlink depending on where the pointee resides.
354+
355+
:param symlink: The source symlink to follow. This must be an absolute path.
356+
:returns: A tuple of action and sourcefile. Action is one of 'copy' or 'link' and means that
357+
the caller should either copy the sourcefile to the target location or create a symlink from
358+
the sourcefile to the target location. sourcefile is the path to the file that should be
359+
the source of the operation. It is either a real file outside of the srcdir hierarchy or
360+
a file (real, directory, symlink or otherwise) inside of the srcdir hierarchy.
361+
:raises ValueError: if the arguments are not correct
362+
:raises BrokenSymlinkError: if the symlink is invalid
363+
364+
Determine whether the file pointed to by the symlink chain is within srcdir. If it is within,
365+
then create a synlink that points from symlink to it.
366+
367+
If it is not within, then walk the symlink chain until we find something that is within srcdir.
368+
369+
If we reach a real file and it is outside of srcdir, then copy the file instead.
370+
"""
371+
if not symlink.startswith('/'):
372+
raise ValueError('File{} must be an absolute path!'.format(symlink))
373+
374+
# os.path.exists follows symlinks
375+
if not os.path.exists(symlink):
376+
raise BrokenSymlinkError('File {} is a broken symlink!'.format(symlink))
377+
378+
pointee_as_abspath = symlink
379+
seen = set([pointee_as_abspath])
380+
381+
while os.path.islink(pointee_as_abspath):
382+
pointee = os.readlink(pointee_as_abspath)
383+
384+
# Note: os.path.join()'s behaviour if the pointee is an absolute path
385+
# essentially ignores the first argument (which is what we want).
386+
pointee_as_abspath = os.path.normpath(os.path.join(os.path.dirname(pointee_as_abspath), pointee))
387+
388+
if pointee_as_abspath in seen:
389+
# On Linux, this should not happen as the os.path.exists() call
390+
# before the loop should catch it.
391+
if symlink == pointee_as_abspath:
392+
error_msg = ('File {} is a broken symlink that references'
393+
' itself!'.format(pointee_as_abspath))
394+
else:
395+
error_msg = ('File {} references {} which is a broken symlink'
396+
' that references itself!'.format(symlink, pointee_as_abspath))
397+
398+
raise BrokenSymlinkError(error_msg)
399+
400+
seen.add(pointee_as_abspath)
401+
402+
if pointee_as_abspath.startswith(srcdir):
403+
# Absolute path inside of the correct dir so we need to link to it
404+
return ("link", pointee_as_abspath)
405+
406+
# The file is not a link so copy it
407+
return ('copy', pointee_as_abspath)
408+
409+
331410
def _copy_decouple(srcdir, dstdir):
332411
"""
333-
Copy `srcdir` to `dstdir` while decoupling symlinks.
412+
Copy files inside of `srcdir` to `dstdir` while decoupling symlinks.
334413
335414
What we mean by decoupling the `srcdir` is that any symlinks pointing
336415
outside the directory will be copied as regular files. This means that the
337416
directory will become independent from its surroundings with respect to
338417
symlinks. Any symlink (or symlink chains) within the directory will be
339418
preserved.
340419
420+
.. warning::
421+
`dstdir` must already exist.
341422
"""
423+
symlinks_to_process = []
424+
for root, directories, files in os.walk(srcdir):
425+
# relative path from srcdir because srcdir is replaced with dstdir for
426+
# the copy.
427+
relpath = os.path.relpath(root, srcdir)
428+
429+
# Create all directories with proper permissions for security
430+
# reasons (Putting private data into directories that haven't had their
431+
# permissions set appropriately may leak the private information.)
432+
for directory in directories:
433+
source_dirpath = os.path.join(root, directory)
434+
target_dirpath = os.path.join(dstdir, relpath, directory)
435+
_mkdir_with_copied_mode(target_dirpath, source_dirpath)
342436

343-
for root, dummy_dirs, files in os.walk(srcdir):
344437
for filename in files:
345-
relpath = os.path.relpath(root, srcdir)
346438
source_filepath = os.path.join(root, filename)
347439
target_filepath = os.path.join(dstdir, relpath, filename)
348440

349-
# Skip and report broken symlinks
350-
if not os.path.exists(source_filepath):
351-
api.current_logger().warning(
352-
'File {} is a broken symlink! Will not copy the file.'.format(source_filepath))
353-
continue
354-
355-
# Copy symlinks to the target userspace
356-
source_is_symlink = os.path.islink(source_filepath)
357-
pointee = None
358-
if source_is_symlink:
359-
pointee = os.readlink(source_filepath)
360-
361-
# If source file is a symlink within `srcdir` then preserve it,
362-
# otherwise resolve and copy it as a file it points to
363-
if pointee is not None and not pointee.startswith(srcdir):
364-
# Follow the path until we hit a file or get back to /etc/pki
365-
while not pointee.startswith(srcdir) and os.path.islink(pointee):
366-
pointee = os.readlink(pointee)
367-
368-
# Pointee points to a _regular file_ outside /etc/pki so we
369-
# copy it instead
370-
if not pointee.startswith(srcdir) and not os.path.islink(pointee):
371-
source_is_symlink = False
372-
source_filepath = pointee
373-
else:
374-
# pointee points back to /etc/pki
375-
pass
376-
377-
# Ensure parent directory exists
378-
parent_dir = os.path.dirname(target_filepath)
379-
# Note: This is secure because we know that parent_dir is located
380-
# inside of `$target_userspace/etc/pki` which is a directory that
381-
# is not writable by unprivileged users. If this function is used
382-
# elsewhere we may need to be more careful before running `mkdir -p`.
383-
run(['mkdir', '-p', parent_dir])
384-
385-
if source_is_symlink:
386-
# Preserve the owner and permissions of the original symlink
387-
run(['ln', '-s', pointee, target_filepath])
388-
run(['chmod', '--reference={}'.format(source_filepath), target_filepath])
441+
# Defer symlinks until later because we may end up having to copy
442+
# the file contents and the directory may not exist yet.
443+
if os.path.islink(source_filepath):
444+
symlinks_to_process.append((source_filepath, target_filepath))
389445
continue
390446

447+
# Not a symlink so we can copy it now too
391448
run(['cp', '-a', source_filepath, target_filepath])
392449

450+
# Now process all symlinks
451+
for source_linkpath, target_linkpath in symlinks_to_process:
452+
try:
453+
action, source_path = _copy_or_link(source_linkpath, srcdir)
454+
except BrokenSymlinkError as e:
455+
# Skip and report broken symlinks
456+
api.current_logger().warning('{} Will not copy the file!'.format(str(e)))
457+
continue
458+
459+
if action == "copy":
460+
# Note: source_path could be a directory, so '-a' or '-r' must be
461+
# given to cp.
462+
run(['cp', '-a', source_path, target_linkpath])
463+
elif action == 'link':
464+
# If the original link is a relative link, then we want the new
465+
# link to be relative as well
466+
if not os.readlink(source_linkpath).startswith('/'):
467+
source_path = os.path.relpath(source_path, srcdir)
468+
run(["ln", "-s", source_path, target_linkpath])
469+
else:
470+
# This will not happen unless _copy_or_link() has a bug.
471+
raise RuntimeError("Programming error: _copy_or_link() returned an unknown action:{}".format(action))
472+
393473

394474
def _copy_certificates(context, target_userspace):
395475
"""
@@ -414,6 +494,10 @@ def _copy_certificates(context, target_userspace):
414494
# Backup container /etc/pki
415495
run(['mv', target_pki, backup_pki])
416496

497+
# _copy_decouple() requires we create the target_pki directory here because we don't know
498+
# the mode inside of _copy_decouple().
499+
_mkdir_with_copied_mode(target_pki, backup_pki)
500+
417501
# Copy source /etc/pki to the container
418502
_copy_decouple('/etc/pki', target_pki)
419503

0 commit comments

Comments
 (0)