def test_patch_does_nothing_if_no_interpreter(self): elf_file = self.fake_elf['fake_elf-static'] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') elf_patcher.patch(elf_file=elf_file) self.assertFalse(self.check_call_mock.called)
def test_patch(self, check_call_mock): elf_file = elf.ElfFile(path='/fake-elf', is_executable=True) elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld') elf_patcher.patch(elf_file=elf_file) check_call_mock.assert_called_once_with([ self.expected_patchelf, '--set-interpreter', '/lib/fake-ld', '/fake-elf' ])
def test_patch_fails_raises_patcherror_exception(self): elf_file = self.fake_elf["fake_elf-bad-patchelf"] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker="/lib/fake-ld", root_path="/fake") self.assertRaises( errors.PatcherGenericError, elf_patcher.patch, elf_file=elf_file )
def prime(self, force=False) -> None: self.makedirs() self.notify_part_progress('Priming') snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) elf_files = elf.get_elf_files(self.primedir, snap_files) all_dependencies = set() # TODO: base snap support core_path = common.get_core_path() for elf_file in elf_files: all_dependencies.update( elf_file.load_dependencies(root_path=self.primedir, core_base_path=core_path)) # Split the necessary dependencies into their corresponding location. # We'll both migrate and track the system dependencies, but we'll only # track the part and staged dependencies, since they should have # already been primed by other means, and migrating them again could # potentially override the `stage` or `snap` filtering. (in_part, staged, primed, system) = _split_dependencies( all_dependencies, self.installdir, self.stagedir, self.primedir) part_dependency_paths = {os.path.dirname(d) for d in in_part} staged_dependency_paths = {os.path.dirname(d) for d in staged} dependency_paths = part_dependency_paths | staged_dependency_paths if not self._build_attributes.no_system_libraries(): system_dependency_paths = {os.path.dirname(d) for d in system} dependency_paths.update(system_dependency_paths) if system: # Lots of dependencies are linked with a symlink, so we need to # make sure we follow those symlinks when we migrate the # dependencies. _migrate_files(system, system_dependency_paths, '/', self.primedir, follow_symlinks=True) formatted_system = '\n'.join(sorted(system)) logger.warning( 'Files from the build host were migrated into the snap to ' 'satisfy dependencies that would otherwise not be met. ' 'This feature will be removed in a future release. If ' 'these libraries are needed in the final snap, ensure ' 'that the following are either satisfied by a ' 'stage-packages entry or through a part:\n{}'.format( formatted_system)) if self._confinement == 'classic': dynamic_linker = self._project_options.get_core_dynamic_linker() elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker, root_path=self.primedir) for elf_file in elf_files: elf_patcher.patch(elf_file=elf_file) self.mark_prime_done(snap_files, snap_dirs, dependency_paths)
def test_patch_fails_raises_patcherror_exception(self): elf_file = elf.ElfFile(path='/fake-elf', magic=self.stub_magic) # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') self.assertRaises(errors.PatcherError, elf_patcher.patch, elf_file=elf_file)
def test_patch_fails_raises_patcherror_exception(self, check_call_mock): stub_magic = ('ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ' 'dynamically linked, interpreter ' '/lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32') elf_file = elf.ElfFile(path='/fake-elf', magic=stub_magic) elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld') self.assertRaises(errors.PatcherError, elf_patcher.patch, elf_file=elf_file)
def test_patch_fails_raises_patcherror_exception(self): elf_file = self.fake_elf['fake_elf-2.23'] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') self.assertRaises(errors.PatcherError, elf_patcher.patch, elf_file=elf_file)
def handle_glibc_mismatch(*, elf_files: FrozenSet[elf.ElfFile], root_path: str, core_base_path: str, snap_base_path: str, preferred_patchelf_path=None, soname_cache: elf.SonameCache = None) -> None: """Copy over libc6 libraries from the host and patch necessary elf files. If no newer glibc version is detected in elf_files, this function returns. The dynamic linker and related libraries to libc6 are expected to be found in root_path. :param snapcraft.internal.elf.ElfFile elf_files: set of candidate elf files to patch if a newer libc6 is required. :param str root_path: the root path of a snap tree. :param str core_base_path: the path to the base snap. :param str snap_base_path: absolute path to the snap once installed to setup proper rpaths. :param str preferred_patchelf_path: patch the necessary elf_files with this patchelf. """ if soname_cache is None: soname_cache = elf.SonameCache() # We assume the current system will satisfy the GLIBC requirement, # get the current libc6 libraries (which includes the linker) libc6_libraries_list = repo.Repo.get_package_libraries('libc6') # For security reasons, we do not want to automatically pull in # libraries but expect them to be consciously brought in by stage-packages # instead. libc6_libraries_paths = [ os.path.join(root_path, l[1:]) for l in libc6_libraries_list ] dynamic_linker = _get_dynamic_linker(libc6_libraries_paths) # Get the path to the "would be" dynamic linker when this snap is # installed. Strip the root_path from the retrieved dynamic_linker # variables + the leading `/` so that os.path.join can perform the # proper join with snap_base_path. dynamic_linker_path = os.path.join(snap_base_path, dynamic_linker[len(root_path) + 1:]) elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker_path, root_path=root_path, preferred_patchelf_path=preferred_patchelf_path) for elf_file in elf_files: # Search for dependencies again now that the new libc6 is # migrated. elf_file.load_dependencies(root_path=root_path, core_base_path=core_base_path, soname_cache=soname_cache) elf_patcher.patch(elf_file=elf_file)
def test_patch_does_nothing_if_no_interpreter(self, check_call_mock): stub_magic = ('ELF 64-bit LSB shared object, x86-64, ' 'version 1 (SYSV), dynamically linked') elf_file = elf.ElfFile(path='/fake-elf', magic=stub_magic) # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') elf_patcher.patch(elf_file=elf_file) self.assertFalse(check_call_mock.called)
def test_patch(self): elf_file = self.fake_elf['fake_elf-2.23'] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') elf_patcher.patch(elf_file=elf_file) self.check_call_mock.assert_called_once_with([ self.expected_patchelf, '--set-interpreter', '/lib/fake-ld', elf_file.path ])
def test_patch(self): elf_file = elf.ElfFile(path='/fake-elf', magic=self.stub_magic) # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') elf_patcher.patch(elf_file=elf_file) self.check_call_mock.assert_called_once_with([ self.expected_patchelf, '--set-interpreter', '/lib/fake-ld', '/fake-elf' ])
def test_patch(self, check_call_mock): stub_magic = ('ELF 64-bit LSB executable, x86-64, version 1 (SYSV), ' 'dynamically linked, interpreter ' '/lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32') elf_file = elf.ElfFile(path='/fake-elf', magic=stub_magic) elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld') elf_patcher.patch(elf_file=elf_file) check_call_mock.assert_called_once_with([ self.expected_patchelf, '--set-interpreter', '/lib/fake-ld', '/fake-elf' ])
def test_patchelf_from_snap_used_if_using_snap(self): self.useFixture( fixtures.EnvironmentVariable('SNAP', '/snap/snapcraft/current')) self.useFixture(fixtures.EnvironmentVariable('SNAP_NAME', 'snapcraft')) # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') expected_patchelf = os.path.join('/snap', 'snapcraft', 'current', 'bin', 'patchelf') self.assertThat(elf_patcher._patchelf_cmd, Equals(expected_patchelf))
def test_patch_fails_with_old_version(self): self.fake_elf = fixture_setup.FakeElf(root_path=self.path, patchelf_version='0.8') self.useFixture(self.fake_elf) elf_file = self.fake_elf['fake_elf-bad-patchelf'] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') self.assertRaises(errors.PatcherNewerPatchelfError, elf_patcher.patch, elf_file=elf_file)
def prime(self, force=False) -> None: self.makedirs() self.notify_part_progress('Priming') snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) elf_files = elf.get_elf_files(self.primedir, snap_files) dependencies = set() for elf_file in elf_files: dependencies.update(elf.get_dependencies(elf_file.path)) # Split the necessary dependencies into their corresponding location. # We'll both migrate and track the system dependencies, but we'll only # track the part and staged dependencies, since they should have # already been primed by other means, and migrating them again could # potentially override the `stage` or `snap` filtering. (in_part, staged, primed, system) = _split_dependencies(dependencies, self.installdir, self.stagedir, self.primedir) part_dependency_paths = {os.path.dirname(d) for d in in_part} staged_dependency_paths = {os.path.dirname(d) for d in staged} dependency_paths = part_dependency_paths | staged_dependency_paths if not self._build_attributes.no_system_libraries(): system_dependency_paths = {os.path.dirname(d) for d in system} dependency_paths.update(system_dependency_paths) if system: # Lots of dependencies are linked with a symlink, so we need to # make sure we follow those symlinks when we migrate the # dependencies. _migrate_files(system, system_dependency_paths, '/', self.primedir, follow_symlinks=True) if self._confinement == 'classic': dynamic_linker = self._project_options.get_core_dynamic_linker() elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker) for elf_file in elf_files: elf_patcher.patch(elf_file=elf_file) self.mark_prime_done(snap_files, snap_dirs, dependency_paths)
def _patch(self, dynamic_linker: str) -> None: preferred_patchelf_path = self._get_preferred_patchelf_path() elf_patcher = elf.Patcher( dynamic_linker=dynamic_linker, root_path=self._primedir, preferred_patchelf_path=preferred_patchelf_path) files_to_patch = elf.get_elf_files_to_patch(self._elf_files) for elf_file in files_to_patch: try: elf_patcher.patch(elf_file=elf_file) except errors.PatcherError as patch_error: logger.warning( 'An attempt to patch {!r} so that it would work ' 'correctly in diverse environments was made and failed. ' 'To disable this behavior set ' '`build-attributes: [no-patchelf]` for the part.'.format( elf_file.path)) if not self._is_go_based_plugin: raise patch_error
def _patch(self, dynamic_linker: str) -> None: preferred_patchelf_path = self._get_preferred_patchelf_path() elf_patcher = elf.Patcher( dynamic_linker=dynamic_linker, root_path=self._primedir, preferred_patchelf_path=preferred_patchelf_path) # Patching all files instead of a subset of them to ensure the # environment is consistent and the chain of dlopens that may # happen remains sane. for elf_file in self._elf_files: try: elf_patcher.patch(elf_file=elf_file) except errors.PatcherError as patch_error: logger.warning( 'An attempt to patch {!r} so that it would work ' 'correctly in diverse environments was made and failed. ' 'To disable this behavior set ' '`build-attributes: [no-patchelf]` for the part.'.format( elf_file.path)) if not self._is_go_based_plugin: raise patch_error
def prime(self, force=False) -> None: # noqa: C901 self.makedirs() self.notify_part_progress('Priming') snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) elf_files = elf.get_elf_files(self.primedir, snap_files) all_dependencies = set() # TODO: base snap support core_path = common.get_core_path() # Reset to take into account new data inside prime provided by other # parts. self._soname_cache.reset() for elf_file in elf_files: all_dependencies.update( elf_file.load_dependencies(root_path=self.primedir, core_base_path=core_path, soname_cache=self._soname_cache)) dependency_paths = self._handle_dependencies(all_dependencies) if not self._build_attributes.keep_execstack(): clear_execstack(elf_files=elf_files) # TODO revisit if we need to support variations and permutations # of this staged_patchelf_path = os.path.join(self.stagedir, 'bin', 'patchelf') if not os.path.exists(staged_patchelf_path): staged_patchelf_path = None # We need to verify now that the GLIBC version would be compatible # with that of the base. # TODO the linker version depends on the chosen base, but that # base may not be installed so we cannot depend on # get_core_dynamic_linker to resolve the final path for which # we resort to our only working base 16, ld-2.23.so. linker_incompat = dict() # type: Dict[str, str] for elf_file in elf_files: if not elf_file.is_linker_compatible(linker='ld-2.23.so'): linker_incompat[elf_file.path] = elf_file.get_required_glibc() # If libc6 is staged, to avoid symbol mixups we will resort to # glibc mangling. libc6_staged = 'libc6' in self._part_properties.get( 'stage-packages', []) is_classic = self._confinement == 'classic' # classic confined snaps built on anything but a host supporting the # the target base will require glibc mangling. classic_mangling_needed = ( is_classic and not self._project_options.is_host_compatible_with_base) if linker_incompat: formatted_items = [ '- {} (requires GLIBC {})'.format(k, v) for k, v in linker_incompat.items() ] logger.warning( 'The GLIBC version of the targeted core is 2.23. A newer ' 'libc will be required for the following files:\n{}'.format( '\n'.join(formatted_items))) if (linker_incompat or libc6_staged or classic_mangling_needed): if not libc6_staged: raise errors.StagePackageMissingError(package='libc6') handle_glibc_mismatch(elf_files=elf_files, root_path=self.primedir, snap_base_path=self._snap_base_path, core_base_path=core_path, preferred_patchelf_path=staged_patchelf_path, soname_cache=self._soname_cache) elif is_classic: dynamic_linker = self._project_options.get_core_dynamic_linker() elf_patcher = elf.Patcher( dynamic_linker=dynamic_linker, root_path=self.primedir, preferred_patchelf_path=staged_patchelf_path) for elf_file in elf_files: elf_patcher.patch(elf_file=elf_file) self.mark_prime_done(snap_files, snap_dirs, dependency_paths)
def test_patch_uses_snapped_strip(self): self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) self.fake_elf = fixture_setup.FakeElf(root_path=self.path, patchelf_version='0.8') self.useFixture(self.fake_elf) elf_file = self.fake_elf['fake_elf-bad-patchelf'] real_check_call = subprocess.check_call real_check_output = subprocess.check_output real_exists = os.path.exists def _fake_check_call(*args, **kwargs): if 'patchelf' in args[0][0]: self.assertThat(args[0][0], Equals('/snap/snapcraft/current/bin/patchelf')) args[0][0] = 'patchelf' elif 'strip' in args[0][0]: self.assertThat( args[0][0], Equals('/snap/snapcraft/current/usr/bin/strip')) args[0][0] = 'strip' real_check_call(*args, **kwargs) def _fake_check_output(*args, **kwargs): if 'patchelf' in args[0][0]: self.assertThat(args[0][0], Equals('/snap/snapcraft/current/bin/patchelf')) args[0][0] = 'patchelf' elif 'strip' in args[0][0]: self.assertThat( args[0][0], Equals('/snap/snapcraft/current/usr/bin/strip')) args[0][0] = 'strip' return real_check_output(*args, **kwargs) def _fake_exists(path): if path == '/snap/snapcraft/current/bin/patchelf': return True elif path == '/snap/snapcraft/current/usr/bin/strip': return True else: return real_exists(path) with mock.patch('subprocess.check_call') as mock_check_call: with mock.patch('subprocess.check_output') as mock_check_output: with mock.patch('os.path.exists', side_effect=_fake_exists): mock_check_call.side_effect = _fake_check_call mock_check_output.side_effect = _fake_check_output # The base_path does not matter here as there are not files # for which to crawl. elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') self.assertRaises(errors.PatcherNewerPatchelfError, elf_patcher.patch, elf_file=elf_file) # Test that .note.go.buildid is stripped off mock_check_call.assert_has_calls([ mock.call([ 'patchelf', '--set-interpreter', '/lib/fake-ld', mock.ANY ]), mock.call([ 'strip', '--remove-section', '.note.go.buildid', mock.ANY ]), mock.call([ 'patchelf', '--set-interpreter', '/lib/fake-ld', mock.ANY ]), ])
def test_patch_does_nothing_if_no_interpreter(self): elf_file = self.fake_elf["fake_elf-static"] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker="/lib/fake-ld", root_path="/fake") elf_patcher.patch(elf_file=elf_file)
def test_patch(self): elf_file = self.fake_elf["fake_elf-2.23"] # The base_path does not matter here as there are not files to # be crawled for. elf_patcher = elf.Patcher(dynamic_linker="/lib/fake-ld", root_path="/fake") elf_patcher.patch(elf_file=elf_file)
def test_patch_uses_snapped_strip(self): self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) self.fake_elf = fixture_setup.FakeElf( root_path=self.path, patchelf_version="0.8" ) self.useFixture(self.fake_elf) elf_file = self.fake_elf["fake_elf-bad-patchelf"] real_check_call = subprocess.check_call real_check_output = subprocess.check_output real_exists = os.path.exists def _fake_check_call(*args, **kwargs): if "patchelf" in args[0][0]: self.assertThat( args[0][0], Equals("/snap/snapcraft/current/bin/patchelf") ) args[0][0] = "patchelf" elif "strip" in args[0][0]: self.assertThat( args[0][0], Equals("/snap/snapcraft/current/usr/bin/strip") ) args[0][0] = "strip" real_check_call(*args, **kwargs) def _fake_check_output(*args, **kwargs): if "patchelf" in args[0][0]: self.assertThat( args[0][0], Equals("/snap/snapcraft/current/bin/patchelf") ) args[0][0] = "patchelf" elif "strip" in args[0][0]: self.assertThat( args[0][0], Equals("/snap/snapcraft/current/usr/bin/strip") ) args[0][0] = "strip" return real_check_output(*args, **kwargs) def _fake_exists(path): if path == "/snap/snapcraft/current/bin/patchelf": return True elif path == "/snap/snapcraft/current/usr/bin/strip": return True else: return real_exists(path) with mock.patch("subprocess.check_call") as mock_check_call: with mock.patch("subprocess.check_output") as mock_check_output: with mock.patch("os.path.exists", side_effect=_fake_exists): mock_check_call.side_effect = _fake_check_call mock_check_output.side_effect = _fake_check_output # The base_path does not matter here as there are not files # for which to crawl. elf_patcher = elf.Patcher( dynamic_linker="/lib/fake-ld", root_path="/fake" ) self.assertRaises( errors.PatcherNewerPatchelfError, elf_patcher.patch, elf_file=elf_file, ) # Test that .note.go.buildid is stripped off mock_check_call.assert_has_calls( [ mock.call( [ "patchelf", "--set-interpreter", "/lib/fake-ld", mock.ANY, ] ), mock.call( [ "strip", "--remove-section", ".note.go.buildid", mock.ANY, ] ), mock.call( [ "patchelf", "--set-interpreter", "/lib/fake-ld", mock.ANY, ] ), ] )
def prime(self, force=False) -> None: self.makedirs() self.notify_part_progress('Priming') snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) elf_files = elf.get_elf_files(self.primedir, snap_files) all_dependencies = set() # TODO: base snap support core_path = common.get_core_path() for elf_file in elf_files: all_dependencies.update( elf_file.load_dependencies(root_path=self.primedir, core_base_path=core_path)) # Split the necessary dependencies into their corresponding location. # We'll both migrate and track the system dependencies, but we'll only # track the part and staged dependencies, since they should have # already been primed by other means, and migrating them again could # potentially override the `stage` or `snap` filtering. (in_part, staged, primed, system) = _split_dependencies(all_dependencies, self.installdir, self.stagedir, self.primedir) part_dependency_paths = {os.path.dirname(d) for d in in_part} staged_dependency_paths = {os.path.dirname(d) for d in staged} dependency_paths = part_dependency_paths | staged_dependency_paths if not self._build_attributes.no_system_libraries(): system_dependency_paths = {os.path.dirname(d) for d in system} dependency_paths.update(system_dependency_paths) if system: # Lots of dependencies are linked with a symlink, so we need to # make sure we follow those symlinks when we migrate the # dependencies. _migrate_files(system, system_dependency_paths, '/', self.primedir, follow_symlinks=True) formatted_system = '\n'.join(sorted(system)) logger.warning( 'Files from the build host were migrated into the snap to ' 'satisfy dependencies that would otherwise not be met. ' 'This feature will be removed in a future release. If ' 'these libraries are needed in the final snap, ensure ' 'that the following are either satisfied by a ' 'stage-packages entry or through a part:\n{}'.format( formatted_system)) # TODO revisit if we need to support variations and permutations # of this staged_patchelf_path = os.path.join(self.stagedir, 'bin', 'patchelf') if not os.path.exists(staged_patchelf_path): staged_patchelf_path = None # We need to verify now that the GLIBC version would be compatible # with that of the base. # TODO the linker version depends on the chosen base, but that # base may not be installed so we cannot depend on # get_core_dynamic_linker to resolve the final path for which # we resort to our only working base 16, ld-2.23.so. linker_compatible = (e.is_linker_compatible(linker='ld-2.23.so') for e in elf_files) if not all((x for x in linker_compatible)): handle_glibc_mismatch(elf_files=elf_files, root_path=self.primedir, snap_base_path=self._snap_base_path, core_base_path=core_path, preferred_patchelf_path=staged_patchelf_path) elif self._confinement == 'classic': dynamic_linker = self._project_options.get_core_dynamic_linker() elf_patcher = elf.Patcher( dynamic_linker=dynamic_linker, root_path=self.primedir, preferred_patchelf_path=staged_patchelf_path) for elf_file in elf_files: elf_patcher.patch(elf_file=elf_file) self.mark_prime_done(snap_files, snap_dirs, dependency_paths)
def prime(self, force=False) -> None: self.makedirs() self.notify_part_progress('Priming') snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) elf_files = elf.get_elf_files(self.primedir, snap_files) all_dependencies = set() for elf_file in elf_files: all_dependencies.update(elf_file.load_dependencies()) # Split the necessary dependencies into their corresponding location. # We'll both migrate and track the system dependencies, but we'll only # track the part and staged dependencies, since they should have # already been primed by other means, and migrating them again could # potentially override the `stage` or `snap` filtering. (in_part, staged, primed, system) = _split_dependencies( all_dependencies, self.installdir, self.stagedir, self.primedir) part_dependency_paths = {os.path.dirname(d) for d in in_part} staged_dependency_paths = {os.path.dirname(d) for d in staged} dependency_paths = part_dependency_paths | staged_dependency_paths if not self._build_attributes.no_system_libraries(): system_dependency_paths = {os.path.dirname(d) for d in system} dependency_paths.update(system_dependency_paths) if system: # Lots of dependencies are linked with a symlink, so we need to # make sure we follow those symlinks when we migrate the # dependencies. _migrate_files(system, system_dependency_paths, '/', self.primedir, follow_symlinks=True) # TODO: base snap support core_path = common.get_core_path() def mangle_library_path(library_path: str, elf_file_path: str) -> str: # If the path is is in the core snap, use the absolute path, # if the path is primed, use $ORIGIN, and last if the dependency # is not anywhere return an empty string. # # Once we move away from the system library grabbing logic # we can move to a smarter library capturing mechanism. library_path = library_path.replace(self.installdir, self.primedir) if library_path.startswith(core_path): return os.path.dirname(library_path) elif (library_path.startswith(self.primedir) and os.path.exists(library_path)): rel_library_path = os.path.relpath(library_path, elf_file_path) rel_library_path_dir = os.path.dirname(rel_library_path) # return the dirname, with the first .. replace with $ORIGIN return rel_library_path_dir.replace('..', '$ORIGIN', 1) else: return '' if self._confinement == 'classic': core_rpaths = common.get_library_paths( core_path, self._project_options.arch_triplet, existing_only=False) dynamic_linker = self._project_options.get_core_dynamic_linker() elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker, library_path_func=mangle_library_path, base_rpaths=core_rpaths) for elf_file in elf_files: elf_patcher.patch(elf_file=elf_file) self.mark_prime_done(snap_files, snap_dirs, dependency_paths)
def test_patch_does_nothing_if_no_interpreter(self, check_call_mock): elf_file = elf.ElfFile(path='/fake-elf', is_executable=False) elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld') elf_patcher.patch(elf_file=elf_file) self.assertFalse(check_call_mock.called)
def handle_glibc_mismatch(*, elf_files: FrozenSet[elf.ElfFile], root_path: str, core_base_path: str, snap_base_path: str, preferred_patchelf_path=None) -> None: """Copy over libc6 libraries from the host and patch necessary elf files. If no newer glibc version is detected in elf_files, this function returns. The dynamic linker and related libraries to libc6 are expected to be found in root_path. :param snapcraft.internal.elf.ElfFile elf_files: set of candidate elf files to patch if a newer libc6 is required. :param str root_path: the root path of a snap tree. :param str core_base_path: the path to the base snap. :param str snap_base_path: absolute path to the snap once installed to setup proper rpaths. :param str preferred_patchelf_path: patch the necessary elf_files with this patchelf. """ formatted_list = list() # type: List[str] patch_elf_files = list() # type: List[elf.ElfFile] for elf_file in elf_files: required_glibc = elf_file.get_required_glibc() if not required_glibc: continue # We need to verify now that the GLIBC version would be compatible # with that of the base. # TODO the linker version depends on the chosen base, but that # base may not be installed so we cannot depend on # get_core_dynamic_linker to resolve the final path for which # we resort to our only working base 16, ld-2.23.so. if not elf_file.is_linker_compatible(linker='ld-2.23.so'): formatted_list.append('- {} -> GLIBC {}'.format( elf_file.path, required_glibc)) patch_elf_files.append(elf_file) if not patch_elf_files: return logger.warning('The primed files will not work with the current ' 'base given the GLIBC mismatch of the primed ' 'files and the linker version (2.23) used in the ' 'base. These are the GLIBC versions required by ' 'the primed files that do not match and will be ' 'patched:\n{}\n'.format('\n'.join(formatted_list))) # We assume the current system will satisfy the GLIBC requirement, # get the current libc6 libraries (which includes the linker) libc6_libraries_list = repo.Repo.get_package_libraries('libc6') # For security reasons, we do not want to automatically pull in # libraries but expect them to be consciously brought in by stage-packages # instead. libc6_libraries_paths = [ os.path.join(root_path, l[1:]) for l in libc6_libraries_list ] dynamic_linker = _get_dynamic_linker(libc6_libraries_paths) # Get the path to the "would be" dynamic linker when this snap is # installed. Strip the root_path from the retrieved dynamic_linker # variables + the leading `/` so that os.path.join can perform the # proper join with snap_base_path. dynamic_linker_path = os.path.join(snap_base_path, dynamic_linker[len(root_path) + 1:]) elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker_path, root_path=root_path, preferred_patchelf_path=preferred_patchelf_path) for elf_file in patch_elf_files: # Search for dependencies again now that the new libc6 is # migrated. elf_file.load_dependencies(root_path=root_path, core_base_path=core_base_path) elf_patcher.patch(elf_file=elf_file)
def handle_glibc_mismatch(*, elf_files: FrozenSet[elf.ElfFile], root_path: str, core_base_path: str, snap_base_path: str, preferred_patchelf_path=None) -> None: """Copy over libc6 libraries from the host and patch necessary elf files. If no newer glibc version is detected in elf_files, this function returns. :param snapcraft.internal.elf.ElfFile elf_files: set of candidate elf files to patch if a newer libc6 is required. :param str root_path: the root path of a snap tree. :param str core_base_path: the path to the base snap. :param str snap_base_path: absolute path to the snap once installed to setup proper rpaths. :param str preferred_patchelf_path: patch the necessary elf_files with this patchelf. """ formatted_list = list() # type: List[str] patch_elf_files = list() # type: List[elf.ElfFile] for elf_file in elf_files: required_glibc = elf_file.get_required_glibc() if not required_glibc: continue # We need to verify now that the GLIBC version would be compatible # with that of the base. # TODO the linker version depends on the chosen base, but that # base may not be installed so we cannot depend on # get_core_dynamic_linker to resolve the final path for which # we resort to our only working base 16, ld-2.23.so. if not elf_file.is_linker_compatible(linker='ld-2.23.so'): formatted_list.append('- {} -> GLIBC {}'.format( elf_file.path, required_glibc)) patch_elf_files.append(elf_file) if not patch_elf_files: return logger.warning('The primed files will not work with the current ' 'base given the GLIBC mismatch of the primed ' 'files and the linker version (2.23) used in the ' 'base. These are the GLIBC versions required by ' 'the primed files that do not match and will be ' 'patched:\n {}\n' 'To work around this, the newer libc will be ' 'migrated into the snap, and these files will be ' 'patched to use it.'.format('\n'.join(formatted_list))) # We assume the current system will satisfy the GLIBC requirement, # get the current libc6 libraries (which includes the linker) libc6_libraries = repo.Repo.get_package_libraries('libc6') libc6_path = os.path.join('snap', 'libc6') # Before doing anything else, verify there's a dynamic linker we can use. dynamic_linker_path = os.path.join(snap_base_path, libc6_path, _get_dynamic_linker(libc6_libraries)) dest_dir = os.path.join(root_path, libc6_path) os.makedirs(dest_dir, exist_ok=True) for src in libc6_libraries: dst = os.path.join(dest_dir, os.path.basename(src)) # follow_symlinks is set to True for elf crawling to work. file_utils.link_or_copy(src, dst, follow_symlinks=True) elf_patcher = elf.Patcher(dynamic_linker=dynamic_linker_path, root_path=root_path, preferred_patchelf_path=preferred_patchelf_path) for elf_file in patch_elf_files: # Search for dependencies again now that the new libc6 is # migrated. elf_file.load_dependencies(root_path=root_path, core_base_path=core_base_path) elf_patcher.patch(elf_file=elf_file)