Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package ansible-core for openSUSE:Factory checked in at 2026-11-06 19:55:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ansible-core (Old) and /work/SRC/openSUSE:Factory/.ansible-core.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ansible-core" Fri Nov 6 19:55:27 2026 rev:68 rq:1395737 version:2.21.6 Changes: -------- --- /work/SRC/openSUSE:Factory/ansible-core/ansible-core.changes 2026-10-18 19:25:20.670836665 +0200 +++ /work/SRC/openSUSE:Factory/.ansible-core.new.3759290/ansible-core.changes 2026-11-06 20:00:44.406122067 +0100 @@ -1,0 +2,30 @@ +Fri Nov 6 08:45:29 UTC 2026 - Johannes Kastl <[email protected]> + +- update to 2.21.6: + https://github.com/ansible/ansible/blob/v2.21.6/changelogs/CHANGELOG-v2.21.rst + * Minor Changes + - ansible-test - Replace FreeBSD 14.4 remote with 14.5. + - ansible-test - Replace FreeBSD 15.0 remote with 15.1. + - ansible-test - Replace RHEL 10.1 remote with 10.2. + - ansible-test - Replace RHEL 9.7 remote with 9.8. + * Security Fixes + - ansible-galaxy collection install - fix validating FILES.json + entries are contained in the collection before creating them. + (CVE-2026-89091) + - ansible-galaxy role install - fix validating the role name is + contained to the configured role path. (CVE-2026-105929) + * Bugfixes + - ansible_facts - initial facts from run_once tasks no longer + reference the same object and can be updated for individual + hosts + - git module - updated to avoid possible incorrect parameters + read from the repo targets. CVE-2026-84503 + - module_utils.mask_url - handle exceptions on parseurl to + avoid disclosing url itself. + - sudo become plugin, sudo-rs compatibility fix for non english + locales + - user - On BusyBox systems, preserve the existing password + lock marker when password_lock is omitted +- remove ansible-core-CVE-2026-89091.patch + +------------------------------------------------------------------- Old: ---- ansible-core-CVE-2026-89091.patch ansible_core-2.21.5.tar.gz ansible_core-2.21.5.tar.gz.sha256 New: ---- ansible_core-2.21.6.tar.gz ansible_core-2.21.6.tar.gz.sha256 ----------(Old B)---------- Old: lock marker when password_lock is omitted - remove ansible-core-CVE-2026-89091.patch ----------(Old E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ansible-core.spec ++++++ --- /var/tmp/diff_new_pack.HF1NMW/_old 2026-11-06 20:00:46.925227518 +0100 +++ /var/tmp/diff_new_pack.HF1NMW/_new 2026-11-06 20:00:46.930227727 +0100 @@ -43,7 +43,7 @@ %endif Name: ansible-core -Version: 2.21.5 +Version: 2.21.6 Release: 0 Summary: Radically simple IT automation License: GPL-3.0-or-later @@ -52,8 +52,6 @@ Source1: ansible_core-%{version}.tar.gz.sha256 # CVE-2026-16493 (bsc#1272369) Patch0: ansible-core-CVE-2026-16493.patch -# CVE-2026-89091 (bsc#1284847) -Patch1: ansible-core-CVE-2026-89091.patch BuildArch: noarch # cannot be installed with ansible < 3 or ansible-base ++++++ ansible_core-2.21.5.tar.gz -> ansible_core-2.21.6.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/PKG-INFO new/ansible_core-2.21.6/PKG-INFO --- old/ansible_core-2.21.5/PKG-INFO 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/PKG-INFO 2026-11-02 18:32:18.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.21.5 +Version: 2.21.6 Summary: Radically simple IT automation Author: Ansible Project License-Expression: GPL-3.0-or-later diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/ansible_core.egg-info/PKG-INFO new/ansible_core-2.21.6/ansible_core.egg-info/PKG-INFO --- old/ansible_core-2.21.5/ansible_core.egg-info/PKG-INFO 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/ansible_core.egg-info/PKG-INFO 2026-11-02 18:32:18.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ansible-core -Version: 2.21.5 +Version: 2.21.6 Summary: Radically simple IT automation Author: Ansible Project License-Expression: GPL-3.0-or-later diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/ansible_core.egg-info/SOURCES.txt new/ansible_core-2.21.6/ansible_core.egg-info/SOURCES.txt --- old/ansible_core-2.21.5/ansible_core.egg-info/SOURCES.txt 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/ansible_core.egg-info/SOURCES.txt 2026-11-02 18:32:18.000000000 +0100 @@ -1020,12 +1020,14 @@ test/integration/targets/ansible-galaxy-collection/handlers/main.yml test/integration/targets/ansible-galaxy-collection/meta/main.yml test/integration/targets/ansible-galaxy-collection/tasks/build.yml +test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml test/integration/targets/ansible-galaxy-collection/tasks/check_requires_ansible.yml test/integration/targets/ansible-galaxy-collection/tasks/download.yml test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml test/integration/targets/ansible-galaxy-collection/tasks/init.yml test/integration/targets/ansible-galaxy-collection/tasks/install.yml test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml +test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml test/integration/targets/ansible-galaxy-collection/tasks/list.yml test/integration/targets/ansible-galaxy-collection/tasks/main.yml test/integration/targets/ansible-galaxy-collection/tasks/pinned_pre_releases_in_deptree.yml @@ -5069,7 +5071,6 @@ test/units/cli/test_playbook.py test/units/cli/test_vault.py test/units/cli/arguments/test_option_helpers.py -test/units/cli/galaxy/test_collection_extract_tar.py test/units/cli/galaxy/test_display_collection.py test/units/cli/galaxy/test_display_header.py test/units/cli/galaxy/test_display_role.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/changelogs/CHANGELOG-v2.21.rst new/ansible_core-2.21.6/changelogs/CHANGELOG-v2.21.rst --- old/ansible_core-2.21.5/changelogs/CHANGELOG-v2.21.rst 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/changelogs/CHANGELOG-v2.21.rst 2026-11-02 18:32:18.000000000 +0100 @@ -4,6 +4,38 @@ .. contents:: Topics +v2.21.6 +======= + +Release Summary +--------------- + +| Release Date: 2026-11-02 +| `Porting Guide <https://docs.ansible.com/ansible-core/2.21/porting_guides/porting_guide_core_2.21.html>`__ + +Minor Changes +------------- + +- ansible-test - Replace FreeBSD 14.4 remote with 14.5. +- ansible-test - Replace FreeBSD 15.0 remote with 15.1. +- ansible-test - Replace RHEL 10.1 remote with 10.2. +- ansible-test - Replace RHEL 9.7 remote with 9.8. + +Security Fixes +-------------- + +- ``ansible-galaxy collection install`` - fix validating ``FILES.json`` entries are contained in the collection before creating them. (CVE-2026-89091) +- ``ansible-galaxy role install`` - fix validating the role name is contained to the configured role path. (CVE-2026-105929) + +Bugfixes +-------- + +- ansible_facts - initial facts from ``run_once`` tasks no longer reference the same object and can be updated for individual hosts +- git module - updated to avoid possible incorrect parameters read from the repo targets. CVE-2026-84503 +- module_utils.mask_url - handle exceptions on parseurl to avoid disclosing url itself. +- sudo become plugin, sudo-rs compatibility fix for non english locales +- user - On BusyBox systems, preserve the existing password lock marker when ``password_lock`` is omitted. + v2.21.5 ======= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/changelogs/changelog.yaml new/ansible_core-2.21.6/changelogs/changelog.yaml --- old/ansible_core-2.21.5/changelogs/changelog.yaml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/changelogs/changelog.yaml 2026-11-02 18:32:18.000000000 +0100 @@ -852,3 +852,55 @@ - dnf5-download_dir-download_only.yml - ensure_conn_cli_dir_perms.yml release_date: '2026-09-29' + 2.21.6: + changes: + release_summary: '| Release Date: 2026-11-02 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.21/porting_guides/porting_guide_core_2.21.html>`__ + + ' + codename: The Rain Song + fragments: + - 2.21.6_summary.yaml + release_date: '2026-11-02' + 2.21.6rc1: + changes: + bugfixes: + - ansible_facts - initial facts from ``run_once`` tasks no longer reference + the same object and can be updated for individual hosts + - git module - updated to avoid possible incorrect parameters read from the + repo targets. CVE-2026-84503 + - module_utils.mask_url - handle exceptions on parseurl to avoid disclosing + url itself. + - sudo become plugin, sudo-rs compatibility fix for non english locales + - user - On BusyBox systems, preserve the existing password lock marker when + ``password_lock`` is omitted. + minor_changes: + - ansible-test - Replace FreeBSD 14.4 remote with 14.5. + - ansible-test - Replace FreeBSD 15.0 remote with 15.1. + - ansible-test - Replace RHEL 10.1 remote with 10.2. + - ansible-test - Replace RHEL 9.7 remote with 9.8. + release_summary: '| Release Date: 2026-10-27 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.21/porting_guides/porting_guide_core_2.21.html>`__ + + ' + security_fixes: + - '``ansible-galaxy collection install`` - fix validating ``FILES.json`` entries + are contained in the collection before creating them. (CVE-2026-89091)' + - '``ansible-galaxy role install`` - fix validating the role name is contained + to the configured role path. (CVE-2026-105929)' + codename: The Rain Song + fragments: + - 2.21.6rc1_summary.yaml + - 87415-set-host-facts-copy.yml + - ansible-galaxy-collection-check-path-containment.yml + - ansible-galaxy-role-name-containment.yml + - ansible-test-freebsd.yml + - ansible-test-rhel.yml + - bump-freebsd-15-0-to-15-1.yml + - git_fixes.yml + - mask_url_except.yml + - sudo_moar_rs.yml + - user-busybox-password-lock.yml + release_date: '2026-10-27' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/cli/galaxy.py new/ansible_core-2.21.6/lib/ansible/cli/galaxy.py --- old/ansible_core-2.21.5/lib/ansible/cli/galaxy.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/cli/galaxy.py 2026-11-02 18:32:18.000000000 +0100 @@ -60,6 +60,7 @@ from ansible.template import trust_as_template from ansible.utils.collection_loader import AnsibleCollectionConfig from ansible.utils.display import Display +from ansible.utils.path import is_subpath from ansible.utils.plugin_docs import get_versioned_doclink from ansible.utils.vars import load_extra_vars @@ -1473,6 +1474,15 @@ display.vvv('Processing role %s ' % role.name) + if not any( + is_subpath(os.path.join(path, role.name), path, real=True) + for path in context.CLIARGS['roles_path'] + ): + display.warning(f"The role name '{role.name}' resolves to a path outside of the configured role directory") + self.exit_without_ignore() + display.display(f'- skipping role {role.name}') + continue + # query the galaxy API for the role data if role.install_info is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/galaxy/collection/__init__.py new/ansible_core-2.21.6/lib/ansible/galaxy/collection/__init__.py --- old/ansible_core-2.21.5/lib/ansible/galaxy/collection/__init__.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/galaxy/collection/__init__.py 2026-11-02 18:32:18.000000000 +0100 @@ -1682,6 +1682,9 @@ except KeyError: raise AnsibleError("Unable to extract '%s' from collection" % dirname) + if not _is_child_path(to_bytes(tar_member.name, errors='surrogate_or_strict'), b_dest): + raise AnsibleError(f"Cannot extract directory '{dirname}' in collection: path is outside of collection '{tar_member.name}'") + b_dir_path = os.path.join(b_dest, to_bytes(dirname, errors='surrogate_or_strict')) b_parent_path = os.path.dirname(b_dir_path) @@ -1786,13 +1789,16 @@ def _is_child_path(path, parent_path, link_name=None): """ Checks that path is a path within the parent_path specified. """ b_path = to_bytes(path, errors='surrogate_or_strict') + b_parent_path = to_bytes(parent_path, errors='surrogate_or_strict') if link_name and not os.path.isabs(b_path): # If link_name is specified, path is the source of the link and we need to resolve the absolute path. b_link_dir = os.path.dirname(to_bytes(link_name, errors='surrogate_or_strict')) - b_path = os.path.abspath(os.path.join(b_link_dir, b_path)) + b_path = os.path.join(b_link_dir, b_path) + elif not os.path.isabs(b_path): + b_path = os.path.join(b_parent_path, b_path) - b_parent_path = to_bytes(parent_path, errors='surrogate_or_strict') + b_path = os.path.normpath(b_path) return b_path == b_parent_path or b_path.startswith(b_parent_path + to_bytes(os.path.sep)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/module_utils/ansible_release.py new/ansible_core-2.21.6/lib/ansible/module_utils/ansible_release.py --- old/ansible_core-2.21.5/lib/ansible/module_utils/ansible_release.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/module_utils/ansible_release.py 2026-11-02 18:32:18.000000000 +0100 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.21.5' +__version__ = '2.21.6' __author__ = 'Ansible, Inc.' __codename__ = "The Rain Song" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/module_utils/urls.py new/ansible_core-2.21.6/lib/ansible/module_utils/urls.py --- old/ansible_core-2.21.5/lib/ansible/module_utils/urls.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/module_utils/urls.py 2026-11-02 18:32:18.000000000 +0100 @@ -47,6 +47,7 @@ import tempfile import traceback import types # pylint: disable=unused-import +import unicodedata import urllib.error import urllib.request @@ -322,11 +323,18 @@ Safely display a url by masking confidential data from a string or the result from urlparse/split """ - if (parsed_url := urlparse(url)) and not parsed_url.username and not parsed_url.password: - return url + mask = '****' + try: + if (parsed_url := urlparse(url)) and not parsed_url.username and not parsed_url.password: + return url + except Exception as e: + msg = 'mask_url could not parse the url provided' + # skip replace on unicode issues, specially since they can be subsets of url. + if unicodedata.is_normalized('NFKC', url): + msg = f'{msg}: {str(e).replace(url, mask)}' + raise ValueError(msg) netloc: str - mask = '****' if parsed_url.password: netloc = parsed_url.netloc.replace(f'{parsed_url.username}:{parsed_url.password}@', f'{mask}:{mask}@') else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/modules/git.py new/ansible_core-2.21.6/lib/ansible/modules/git.py --- old/ansible_core-2.21.5/lib/ansible/modules/git.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/modules/git.py 2026-11-02 18:32:18.000000000 +0100 @@ -379,14 +379,14 @@ module.fail_json(msg="Unable to read %s" % headfile) if rawdata: try: - rawdata = rawdata.replace('refs/remotes/%s' % remote, '', 1) + rawdata = rawdata.replace(f'refs/remotes/{remote}', '', 1) refparts = rawdata.split(' ') newref = refparts[-1] nrefparts = newref.split('/', 2) res = nrefparts[-1].rstrip('\n') except Exception: if fail_on_error and module: - module.fail_json(msg="Unable to split head from '%s'" % rawdata) + module.fail_json(msg=f"Unable to split head from '{rawdata}'") return res @@ -406,7 +406,7 @@ params = [] # run a bad submodule command to get valid params - cmd = "%s submodule update --help" % (git_path) + cmd = [git_path, 'submodule', 'update', '--help'] rc, stdout, stderr = module.run_command(cmd, cwd=cwd) lines = stderr.split('\n') update_line = None @@ -519,7 +519,7 @@ def get_version(module, git_path, dest, ref="HEAD"): """ samples the version of the git repo """ - cmd = "%s rev-parse %s" % (git_path, ref) + cmd = [git_path, "rev-parse", ref] rc, stdout, stderr = module.run_command(cmd, cwd=dest) sha = to_native(stdout).rstrip('\n') return sha @@ -639,7 +639,7 @@ # git before 1.7.5 doesn't have separate-git-dir argument, do fallback needs_separate_git_dir_fallback = True else: - cmd.append('--separate-git-dir=%s' % separate_git_dir) + cmd.append(f'--separate-git-dir={separate_git_dir}') cmd.extend([repo, dest]) module.run_command(cmd, check_rc=True, cwd=dest_dirname) @@ -664,7 +664,7 @@ if bare: return False - cmd = "%s status --porcelain" % (git_path) + cmd = [git_path, "status", "--porcelain"] rc, stdout, stderr = module.run_command(cmd, cwd=dest) lines = stdout.splitlines() lines = list(filter(lambda c: not re.search('^\\?\\?.*$', c), lines)) @@ -678,7 +678,7 @@ Discards any changes to tracked files in working tree since that commit. """ - cmd = "%s reset --hard HEAD" % (git_path,) + cmd = [git_path, "reset", "--hard", "HEAD"] return module.run_command(cmd, check_rc=True, cwd=dest) @@ -690,7 +690,7 @@ # Ensure we have the object we are referring to during git diff ! git_version_used = git_version(git_path, module) fetch(git_path, module, repo, dest, after, remote, depth, bare, refspec, git_version_used, force) - cmd = '%s diff %s %s' % (git_path, before, after) + cmd = [git_path, 'diff', before, after] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc == 0 and out: return {'prepared': out} @@ -721,6 +721,7 @@ cloning = False cwd = None tag = False + cmd = [git_path, 'ls-remote', remote] if remote == module.params['repo']: cloning = True elif remote == 'file://' + os.path.expanduser(module.params['repo']): @@ -730,15 +731,15 @@ if version == 'HEAD': if cloning: # cloning the repo, just get the remote's HEAD version - cmd = '%s ls-remote %s -h HEAD' % (git_path, remote) + cmd.extend(['-h', 'HEAD']) else: head_branch = get_head_branch(git_path, module, dest, remote, bare) - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, head_branch) + cmd.extend(['-h', '--', shlex.quote(f'refs/heads/{head_branch}')]) elif is_remote_branch(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) + cmd.extend(['-h', f'refs/heads/{version}']) elif is_remote_tag(git_path, module, dest, remote, version): tag = True - cmd = '%s ls-remote %s -t refs/tags/%s*' % (git_path, remote, version) + cmd.extend(['-t', f'refs/tags/{version}*']) else: # Appears to be a sha hash. Checking requires special action rev = get_sha_hash(module, git_path, remote, version, cwd) @@ -765,7 +766,7 @@ def is_remote_tag(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version) + cmd = [git_path, 'ls-remote', remote, '-t', f'refs/tags/{version}'] (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) if to_native(version, errors='surrogate_or_strict') in out: return True @@ -775,10 +776,10 @@ def get_branches(git_path, module, dest): branches = [] - cmd = '%s branch --no-color -a' % (git_path,) + cmd = [git_path, 'branch', '--no-color', '-a'] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Could not determine branch data - received %s" % out, stdout=out, stderr=err) + module.fail_json(msg=f"Could not determine branch data - received {out}", stdout=out, stderr=err) for line in out.split('\n'): if line.strip(): branches.append(line.strip()) @@ -790,7 +791,7 @@ cmd = [git_path, 'for-each-ref', 'refs/tags/', '--format', '%(objecttype):%(refname:short)'] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Could not determine tag data - received %s" % out, stdout=out, stderr=err) + module.fail_json(msg=f"Could not determine tag data - received {out}", stdout=out, stderr=err) for line in to_native(out).split('\n'): if line.strip(): tagtype, tagname = line.strip().split(':') @@ -800,7 +801,7 @@ def is_remote_branch(git_path, module, dest, remote, version): - cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) + cmd = [git_path, 'ls-remote', remote, '-h', f'refs/heads/{version}'] (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) if to_native(version, errors='surrogate_or_strict') in out: return True @@ -810,13 +811,7 @@ def is_local_branch(git_path, module, dest, branch): branches = get_branches(git_path, module, dest) - lbranch = '%s' % branch - if lbranch in branches: - return True - elif '* %s' % branch in branches: - return True - else: - return False + return f'{branch}' in branches or f'* {branch}' in branches def is_not_a_branch(git_path, module, dest): @@ -878,6 +873,11 @@ if is_not_a_branch(git_path, module, dest): headfile = os.path.join(repo_path, 'refs', 'remotes', remote, 'HEAD') branch = head_splitter(headfile, remote, module=module, fail_on_error=True) + + if branch.startswith('-'): + # prevent branch names that can be confused as options + module.fail_json(msg=f'Invalid branch name ({branch}) from {headfile} in {remote}') + return branch @@ -1002,13 +1002,13 @@ after = {} for submodule in begin: branch = get_submodule_branch(git_path, module, dest, submodule) - version_ref = f'{remote}/{branch}' if branch != 'HEAD' else 'HEAD' + version_ref = 'HEAD' if branch == 'HEAD' else shlex.quote(f'{remote}/{branch}') submodule_path = os.path.join(dest, submodule) cmd = [git_path, 'rev-parse', version_ref] (rc, out, err) = module.run_command(cmd, cwd=submodule_path) if rc != 0: module.fail_json( - msg='Unable to determine hash of submodule %s at %s' % (submodule, version_ref), + msg=f'Unable to determine hash of submodule {submodule} at {version_ref}', stdout=out, stderr=err, rc=rc) after[submodule] = out.strip() if begin != after: @@ -1056,48 +1056,52 @@ Can't use git remote set-branches, as it is not available in git 1.7.1 (centos6) """ - branchref = "+refs/heads/%s:refs/heads/%s" % (version, version) - branchref += ' +refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version) - cmd = "%s fetch --depth=%s %s %s" % (git_path, depth, remote, branchref) + cmd = [ + git_path, + "fetch", + f"--depth={depth}", + remote, + f"+refs/heads/{version}:refs/heads/{version}", + f"+refs/heads/{version}:refs/remotes/{remote}/{version}", + ] (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: - module.fail_json(msg="Failed to fetch branch from remote: %s" % version, stdout=out, stderr=err, rc=rc) + module.fail_json(msg=f"Failed to fetch branch from remote: {version}", stdout=out, stderr=err, rc=rc) def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_allowlist): cmd = '' if version == 'HEAD': branch = get_head_branch(git_path, module, dest, remote) - (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest) + (rc, out, err) = module.run_command([git_path, "checkout", "--force", branch], cwd=dest) if rc != 0: - module.fail_json(msg="Failed to checkout branch %s" % branch, - stdout=out, stderr=err, rc=rc) - cmd = "%s reset --hard %s/%s --" % (git_path, remote, branch) + module.fail_json(msg=f"Failed to checkout branch {branch} HEAD", stdout=out, stderr=err, rc=rc) + cmd = [git_path, "reset", "--hard", shlex.quote("/".join([remote, branch]))] else: # FIXME check for local_branch first, should have been fetched already if is_remote_branch(git_path, module, dest, remote, version): + rv = "/".join([remote, version]) if depth and not is_local_branch(git_path, module, dest, version): # git clone --depth implies --single-branch, which makes # the checkout fail if the version changes # fetch the remote branch, to be able to check it out next set_remote_branch(git_path, module, dest, remote, version, depth) if not is_local_branch(git_path, module, dest, version): - cmd = "%s checkout --track -b %s %s/%s" % (git_path, version, remote, version) + cmd = [git_path, "checkout", "--track", "-b", version, rv] else: - (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, version), cwd=dest) + (rc, out, err) = module.run_command([git_path, "checkout", "--force", version], cwd=dest) if rc != 0: module.fail_json(msg="Failed to checkout branch %s" % version, stdout=out, stderr=err, rc=rc) - cmd = "%s reset --hard %s/%s" % (git_path, remote, version) + cmd = [git_path, "reset", "--hard", rv] else: - cmd = "%s checkout --force %s" % (git_path, version) + cmd = [git_path, "checkout", "--force", version] (rc, out1, err1) = module.run_command(cmd, cwd=dest) if rc != 0: - if version != 'HEAD': - module.fail_json(msg="Failed to checkout %s" % (version), - stdout=out1, stderr=err1, rc=rc, cmd=cmd) + if version == 'HEAD': + msg = f"Failed to checkout branch {branch}" else: - module.fail_json(msg="Failed to checkout branch %s" % (branch), - stdout=out1, stderr=err1, rc=rc, cmd=cmd) + msg = f"Failed to checkout {version}" + module.fail_json(msg=msg, stdout=out1, stderr=err1, rc=rc, cmd=cmd) if verify_commit: verify_commit_sign(git_path, module, dest, version, gpg_allowlist) @@ -1110,9 +1114,9 @@ git_sub = "verify-tag" else: git_sub = "verify-commit" - cmd = "%s %s %s" % (git_path, git_sub, version) + cmd = [git_path, git_sub, version] if gpg_allowlist: - cmd += " --raw" + cmd.append("--raw") (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc) @@ -1141,7 +1145,7 @@ def git_version(git_path, module): """return the installed version of git""" - cmd = "%s --version" % git_path + cmd = [git_path, "--version"] (rc, out, err) = module.run_command(cmd) if rc != 0: # one could fail_json here, but the version info is not that important, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/modules/user.py new/ansible_core-2.21.6/lib/ansible/modules/user.py --- old/ansible_core-2.21.5/lib/ansible/modules/user.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/modules/user.py 2026-11-02 18:32:18.000000000 +0100 @@ -3132,7 +3132,7 @@ - remove_user() - modify_user() """ - def _build_password_string(self, current_password=None): + def _build_password_string(self, current_password: str | None = None) -> str: """ Build the appropriate password string based on the current password and module parameters. @@ -3140,27 +3140,26 @@ This method will return '*' at a minimum to avoid creating an enabled account with no password. """ - lock = LOCK_INDICATOR if self.password_lock else '' - - # Order of precedence when choosing the password: - # 1. password from module parameters - # 2. current password - # 3. string to enable the account but without a password - password = '*' if self.password is not None: password = self.password elif current_password: password = current_password - if current_password == LOCK_INDICATOR: - # Special handling when the password is only a '!' to avoid - # unnecessary changes to the password to values like '!!' or '!*'. - lock = '' - elif current_password.startswith(LOCK_INDICATOR): - # Preserve the existing password but unlock the account even if - # no password hash was provided in the module parameters. - password = current_password.lstrip(LOCK_INDICATOR) + else: + password = '*' + + currently_locked = bool(current_password) and current_password.startswith(LOCK_INDICATOR) + + if self.password_lock is True: + if not password.startswith(LOCK_INDICATOR): + password = f'{LOCK_INDICATOR}{password}' + elif self.password_lock is False: + # Leave a lone '!' as-is so unlocking does not produce an empty field. + if password.startswith(LOCK_INDICATOR) and password != LOCK_INDICATOR: + password = password.removeprefix(LOCK_INDICATOR) + elif currently_locked and not password.startswith(LOCK_INDICATOR): + password = f'{LOCK_INDICATOR}{password}' - return f'{lock}{password}' + return password def create_user(self): cmd = [self.module.get_bin_path('adduser', True)] @@ -3296,11 +3295,9 @@ # Manage password current_password = to_native(user_info[1]) - new_password = self._build_password_string(current_password) - if self.update_password == 'always': - lock_status_mismatch = self.password_lock and not current_password.startswith('!') - password_changed = new_password != current_password - if lock_status_mismatch or password_changed: + if self.password is not None or self.password_lock is not None: + new_password = self._build_password_string(current_password) + if self.update_password == 'always' and new_password != current_password: cmd = [self.module.get_bin_path('chpasswd', True), '--encrypted'] data = f'{self.name}:{new_password}' rc, out, err = self.execute_command(cmd, data=data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/plugins/become/sudo.py new/ansible_core-2.21.6/lib/ansible/plugins/become/sudo.py --- old/ansible_core-2.21.5/lib/ansible/plugins/become/sudo.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/plugins/become/sudo.py 2026-11-02 18:32:18.000000000 +0100 @@ -101,6 +101,17 @@ fail = ('Sorry, try again.',) missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required') + def check_password_prompt(self, b_output: bytes) -> bool: + # try GNU sudo first + matched = super().check_password_prompt(b_output) + if not matched: + # try sudo-rs, which is not backwards compatible and does i18n + # Sample: '[sudo: [sudo via ansible, key=csfhjsshkagnlthoutcsjownopciscow] password:] Password:' + sudo_rs = re.compile(fr"\[sudo:\s+{re.escape(self.prompt)}\]\s+\S.+\Z".encode()) + # using match as the line MUST start with this + matched = any(sudo_rs.match(l) for l in b_output.splitlines()) + return matched + def build_become_command(self, cmd, shell): super(BecomeModule, self).build_become_command(cmd, shell) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/release.py new/ansible_core-2.21.6/lib/ansible/release.py --- old/ansible_core-2.21.5/lib/ansible/release.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/release.py 2026-11-02 18:32:18.000000000 +0100 @@ -17,6 +17,6 @@ from __future__ import annotations -__version__ = '2.21.5' +__version__ = '2.21.6' __author__ = 'Ansible, Inc.' __codename__ = "The Rain Song" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/lib/ansible/vars/manager.py new/ansible_core-2.21.6/lib/ansible/vars/manager.py --- old/ansible_core-2.21.5/lib/ansible/vars/manager.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/lib/ansible/vars/manager.py 2026-11-02 18:32:18.000000000 +0100 @@ -580,8 +580,8 @@ try: host_cache = self._fact_cache.get(host) except KeyError: - # We get to set this as new - host_cache = facts + # We get to set this as new. Copy to ensure shared facts from run_once can be updated individually. + host_cache = dict(facts) else: if not isinstance(host_cache, MutableMapping): raise TypeError('The object retrieved for {0} must be a MutableMapping but was' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/pyproject.toml new/ansible_core-2.21.6/pyproject.toml --- old/ansible_core-2.21.5/pyproject.toml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/pyproject.toml 2026-11-02 18:32:18.000000000 +0100 @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 77.0.3, <= 84.0.0"] # lower bound to support license/license-files (PEP 639), upper bound for latest version tested at release +requires = ["setuptools >= 77.0.3, <= 84.2.1"] # lower bound to support license/license-files (PEP 639), upper bound for latest version tested at release build-backend = "setuptools.build_meta" [project] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py 2026-11-02 18:32:18.000000000 +0100 @@ -5,11 +5,11 @@ from __future__ import annotations +import argparse import hashlib import io import json import os -import sys import tarfile from ansible.module_utils.common.file import S_IRWXU_RXG_RXO @@ -44,9 +44,23 @@ } -def add_file(tar_file, filename, b_content, update_files=True): +def calculate_target_filename(symlink_path, linkname): + """The target is relative to the symlink.""" + symlink_dir = os.path.dirname(symlink_path) + if symlink_dir: + # tarfile will extract the linkname as {{ symlink_dir }}/{{ linkname }}, even if linkname is an absolute path + return "/".join([symlink_dir, linkname]) + return linkname + + +def add_file(tar_file, filename, b_content, update_files=True, symlink_linkname=None): tar_info = tarfile.TarInfo(filename) - tar_info.size = len(b_content) + if symlink_linkname is not None: + add_file(tar_file, calculate_target_filename(filename, symlink_linkname), b"", update_files=False) + tar_info.type = tarfile.SYMTYPE + tar_info.linkname = symlink_linkname + else: + tar_info.size = len(b_content) tar_info.mode = S_IRWXU_RXG_RXO tar_file.addfile(tarinfo=tar_info, fileobj=io.BytesIO(b_content)) @@ -58,14 +72,76 @@ 'name': filename, 'ftype': 'file', 'chksum_type': 'sha256', - 'chksum_sha256': sha256.hexdigest(), + 'chksum_sha256': sha256.hexdigest() if symlink_linkname is None else None, 'format': 1 }) -collection_tar = os.path.join(sys.argv[1], 'suspicious-test-1.0.0.tar.gz') +def add_dir(tar_file, dir_name, symlink_linkname=None, update_files=True): + if symlink_linkname: + add_dir(tar_file, symlink_linkname, update_files=False) + + tar_info = tarfile.TarInfo(dir_name) + + if symlink_linkname is None: + tar_info.type = tarfile.DIRTYPE + else: + tar_info.type = tarfile.SYMTYPE + tar_info.linkname = symlink_linkname + + tar_info.mode = 0o777 + tar_file.addfile(tar_info) + + if not update_files: + return + + files['files'].append({ + 'name': dir_name, + 'ftype': 'dir', + 'chksum_type': 'sha256', + 'chksum_sha256': None, + 'format': 1, + }) + + +parser = argparse.ArgumentParser(description="Test suspiciously crafted collection artifacts.") +parser.add_argument("dest", help="The destination directory for the collection artifact.") + +mutually_exclusive_group = parser.add_mutually_exclusive_group() +mutually_exclusive_group.add_argument( + "--file", help="A file to add to the collection artifact.", +) +mutually_exclusive_group.add_argument( + "--dir", help="A directory to add to the collection artifact.", +) +mutually_exclusive_group.add_argument( + "--symlink-linkname", help="Add a symlink linkname to the collection artifact.", +) +parser.add_argument( + "--symlink", help="The name of the symlink to use with --symlink-linkname. Defaults to 'symlink'.", +) + + +args = parser.parse_args() + +collection_tar = os.path.join(args.dest, 'suspicious-test-1.0.0.tar.gz') with tarfile.open(collection_tar, mode='w:gz') as tar_file: - add_file(tar_file, '../../outside.sh', b"#!/usr/bin/env bash\necho \"you got pwned\"") + filename = args.file + dirname = args.dir + + if args.symlink and not args.symlink_linkname: + parser.error("--symlink and --symlink-linkname must be used together") + + if args.symlink_linkname: + if args.symlink_linkname.endswith(os.path.sep): + dirname = args.symlink or "symlink" + else: + filename = args.symlink or "symlink" + + if dirname: + add_dir(tar_file, dirname, symlink_linkname=args.symlink_linkname) + elif filename: + add_file(tar_file, filename, b"#!/usr/bin/env bash\necho \"you got pwned\"", symlink_linkname=args.symlink_linkname) b_files = json.dumps(files).encode('utf-8') b_files_hash = hashlib.sha256() @@ -73,12 +149,3 @@ manifest['file_manifest_file']['chksum_sha256'] = b_files_hash.hexdigest() add_file(tar_file, 'FILES.json', b_files) add_file(tar_file, 'MANIFEST.json', json.dumps(manifest).encode('utf-8')) - - b_manifest = json.dumps(manifest).encode('utf-8') - - for name, b in [('MANIFEST.json', b_manifest), ('FILES.json', b_files)]: - b_io = io.BytesIO(b) - tar_info = tarfile.TarInfo(name) - tar_info.size = len(b) - tar_info.mode = 0o0644 - tar_file.addfile(tarinfo=tar_info, fileobj=b_io) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/build_and_install_tarball.yml 2026-11-02 18:32:18.000000000 +0100 @@ -0,0 +1,37 @@ +- name: Test a specially built tarball + debug: + msg: "Creating tarball with arguments: {{ tarfile_arguments }}" + +- name: Create tarball + script: build_bad_tar.py {{ galaxy_dir | quote }} {{ tarfile_arguments }} + +- name: Try to install the tarball + command: ansible-galaxy collection install '{{ galaxy_dir }}/suspicious-test-1.0.0.tar.gz' {{ galaxy_verbosity }} + register: fail_bad_tar + ignore_errors: true + environment: + ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' + +- name: Get the result of the install attempt + stat: + path: '{{ galaxy_dir }}/ansible_collections/suspicious' + register: fail_bad_tar_actual + +- name: Remove collection if it was installed + file: + path: '{{ galaxy_dir }}/ansible_collections' + state: absent + when: cleanup | default(True) + +- name: Assert the expected result + assert: + fail_msg: "{{ rc_msg }}{{ stderr_msg }}{{ stdout_msg }}" + that: + - fail_bad_tar.rc == expected_rc + - fail_bad_tar.stderr is search(expected_error) + - fail_bad_tar.stdout is search(expected_stdout) + - (expected_rc == 0) | ternary(fail_bad_tar_actual.stat.exists, not fail_bad_tar_actual.stat.exists) + vars: + rc_msg: "Expected rc: {{ expected_rc }}" + stderr_msg: "{{ (expected_error == '') | ternary('', ', error: ' ~ expected_error) }}" + stdout_msg: "{{ (expected_stdout == '') | ternary('', ', stdout: ' ~ expected_stdout) }}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/install.yml new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/install.yml --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/install.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/install.yml 2026-11-02 18:32:18.000000000 +0100 @@ -297,26 +297,6 @@ - "{{ galaxy_dir }}/scratch/tmp_parent/" - "{{ galaxy_dir }}/tmp_parent-name-1.0.0.tar.gz" -- name: setup bad tarball - {{ test_id }} - script: build_bad_tar.py {{ galaxy_dir | quote }} - -- name: fail to install a collection from a bad tarball - {{ test_id }} - command: ansible-galaxy collection install '{{ galaxy_dir }}/suspicious-test-1.0.0.tar.gz' {{ galaxy_verbosity }} - register: fail_bad_tar - failed_when: fail_bad_tar.rc != 1 and "Cannot extract tar entry '../../outside.sh' as it will be placed outside the collection directory" not in fail_bad_tar.stderr - environment: - ANSIBLE_COLLECTIONS_PATH: '{{ galaxy_dir }}/ansible_collections' - -- name: get result of failed collection install - {{ test_id }} - stat: - path: '{{ galaxy_dir }}/ansible_collections\suspicious' - register: fail_bad_tar_actual - -- name: assert result of failed collection install - {{ test_id }} - assert: - that: - - not fail_bad_tar_actual.stat.exists - - name: install a collection from a URI - {{ test_id }} command: ansible-galaxy collection install {{ amanda }}/artifacts/namespace4-name-1.0.0.tar.gz {{ galaxy_verbosity }} register: install_uri diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/install_tarball.yml 2026-11-02 18:32:18.000000000 +0100 @@ -0,0 +1,97 @@ +- name: Test unsafe tarball extraction + vars: + unsafe_rel_path: '../../outside' + unsafe_abs_path: '{{ galaxy_dir }}/ansible_collections/suspicious/test/../../outside' + safe_linkname: '{{ galaxy_dir }}/ansible_collections/suspicious/test/inside' + expected_rc: 1 + expected_stdout: "" + tarfile_arguments: "{{ item }}" + block: + - name: Test unsafe file extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--file '{{ unsafe_rel_path }}.sh'" + - "--file '{{ unsafe_abs_path }}.sh'" + - "--symlink '{{ unsafe_rel_path }}.sh' --symlink-linkname '{{ safe_linkname }}.sh'" + - "--symlink '{{ unsafe_abs_path }}.sh' --symlink-linkname '{{ safe_linkname }}.sh'" + vars: + expected_error: "Cannot extract tar entry" + + - name: Test unsafe directory extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--dir '{{ unsafe_rel_path }}'" + - "--dir '{{ unsafe_abs_path }}'" + - "--symlink '{{ unsafe_rel_path }}' --symlink-linkname '{{ safe_linkname }}/'" + - "--symlink '{{ unsafe_abs_path }}' --symlink-linkname '{{ safe_linkname }}/'" + vars: + expected_error: "Cannot extract directory" + + - name: Test unsafe symlink linkname + include_tasks: build_and_install_tarball.yml + loop: + - "--symlink-linkname '{{ unsafe_rel_path }}.sh'" + - "--symlink-linkname '{{ unsafe_abs_path }}.sh'" + - "--symlink-linkname '{{ unsafe_rel_path }}/'" + - "--symlink-linkname '{{ unsafe_abs_path }}/'" + - "--symlink 'some_directory/symlink' --symlink-linkname 'safe/../../../unsafe'" + vars: + expected_error: >- + Cannot extract symlink '.*symlink' in collection: path points to location outside of collection + +- name: Test safe tarball extraction + vars: + expected_rc: 0 + expected_error: "" + expected_stdout: "suspicious\\.test:1\\.0\\.0 was installed successfully" + cleanup: False + block: + - name: Test successful directory extraction + include_tasks: build_and_install_tarball.yml + loop: + - "--dir 'safe/'" + - "--dir 'safe'" + loop_control: + loop_var: tarfile_arguments + vars: + cleanup: True + + - name: Test successful relative linkname extraction + include_tasks: build_and_install_tarball.yml + vars: + tarfile_arguments: "--symlink 'some_directory/symlink' --symlink-linkname '../safe'" + + - &stat_symlink + name: Check extracted the symlink exists + stat: + path: "{{ galaxy_dir }}/ansible_collections/suspicious/test/some_directory/symlink" + register: expected_symlink + + - &realpath_symlink + name: Check extracted symlink is not broken + command: "realpath {{ galaxy_dir }}/ansible_collections/suspicious/test/some_directory/symlink" + ignore_errors: True + register: realpath_result + + - &assert_symlink + assert: + that: + - expected_symlink.stat.exists + - realpath_result is not failed + - expected_symlink.stat.lnk_source == realpath_result.stdout == galaxy_dir ~ '/ansible_collections/suspicious/test/safe' + + - &cleanup + name: Remove installed collection + file: + path: "{{ galaxy_dir }}/ansible_collections" + state: absent + + - name: Test successful absolute linkname extraction + include_tasks: build_and_install_tarball.yml + vars: + tarfile_arguments: "--symlink some_directory/symlink --symlink-linkname '{{ galaxy_dir }}/ansible_collections/suspicious/test/safe'" + + - <<: *stat_symlink + - <<: *realpath_symlink + - <<: *assert_symlink + - <<: *cleanup diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/main.yml new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/main.yml --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-collection/tasks/main.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-collection/tasks/main.yml 2026-11-02 18:32:18.000000000 +0100 @@ -57,6 +57,9 @@ environment: ANSIBLE_CONFIG: '{{ galaxy_dir }}/ansible.cfg' +- name: run ansible-galaxy collection tarball extraction tests + include_tasks: install_tarball.yml + - name: run ansible-galaxy collection publish tests for {{ test_name }} include_tasks: publish.yml args: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-role/tasks/main.yml new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-role/tasks/main.yml --- old/ansible_core-2.21.5/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-galaxy-role/tasks/main.yml 2026-11-02 18:32:18.000000000 +0100 @@ -63,11 +63,47 @@ register: invalid - assert: - that: "invalid.rc != 0" + that: + - invalid.rc != 0 + - 'invalid.stderr is search("Invalid linkname for tarfile member: path (:?/|/root/invalid) is not a subpath of the role")' - name: Uninstall invalid role command: ansible-galaxy role remove invalid-testrole +- name: Test invalid role requirement name + vars: + abs_name: "{{ remote_tmp_dir }}/unsafe" + install_msg: "{{ abs_name }} was installed successfully" + external_msg: "The role name '{{ abs_name }}' resolves to a path outside of the configured role directory" + block: + - name: Create invalid requirements file + copy: + dest: invalid-requirements.yml + content: |- + - src: "{{ remote_tmp_dir }}/valid-role.tar" + name: "{{ abs_name }}" + + - name: Test installing the role + command: ansible-galaxy install -r invalid-requirements.yml + register: fail_external + ignore_errors: True + + - assert: + that: + - fail_external is failed + - fail_external.stderr is search(external_msg) + - fail_external.stdout is not search(install_msg) + +- name: Test --ignore-errors skips unsatisfiable invalid requirement name + command: ansible-galaxy install {{ remote_tmp_dir }}/roles/../valid-role.tar --roles-path {{ remote_tmp_dir }}/roles --ignore-errors + register: warn_external + +- assert: + that: + - warn_external is success + - warn_external.stderr is search("The role name '.*' resolves to a path outside of the configured role directory") + - warn_external.stdout is not search(".* was installed successfully") + - import_tasks: dir-traversal.yml - import_tasks: valid-role-symlinks.yml - import_tasks: git-config-injection.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/ansible-test-container/runme.py new/ansible_core-2.21.6/test/integration/targets/ansible-test-container/runme.py --- old/ansible_core-2.21.5/test/integration/targets/ansible-test-container/runme.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/ansible-test-container/runme.py 2026-11-02 18:32:18.000000000 +0100 @@ -883,6 +883,7 @@ dict( user='root', password=root_password, + password_lock='false', ), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/become_sudo/files/sudoshim.sh new/ansible_core-2.21.6/test/integration/targets/become_sudo/files/sudoshim.sh --- old/ansible_core-2.21.5/test/integration/targets/become_sudo/files/sudoshim.sh 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/become_sudo/files/sudoshim.sh 2026-11-02 18:32:18.000000000 +0100 @@ -11,6 +11,9 @@ "--intermediate-user") intermediate_user_idx="${i}" ;; + "--set-lang") + lang_idx="${i}" + ;; esac done @@ -19,4 +22,11 @@ unset "args[intermediate_user_idx]" unset "args[intermediate_user_idx+1]" +if [ -n "${lang_idx-}" ]; then + export LANG="${args[lang_idx+1]}" + export LC_ALL="${args[lang_idx+1]}" + unset "args[lang_idx]" + unset "args[lang_idx+1]" +fi + exec sudo -n -u "${intermediate_user_name}" sudo -k "${args[@]}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/become_sudo/tasks/main.yml new/ansible_core-2.21.6/test/integration/targets/become_sudo/tasks/main.yml --- old/ansible_core-2.21.5/test/integration/targets/become_sudo/tasks/main.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/become_sudo/tasks/main.yml 2026-11-02 18:32:18.000000000 +0100 @@ -82,3 +82,42 @@ that: - custom_flags.stdout.strip() == test_user_name - no_user.stdout.strip() == "root" + +- name: Test sudo with i18n, required for sudo-rs, only ubuntu + when: + - ansible_facts['system'] == 'Linux' + - ansible_facts['distribution'] == 'Ubuntu' + vars: + testlocales: + - ar_EG.UTF-8 # كَلِمَةُ الْسِّرِ + - fr_FR.UTF-8 # Mot de passe + - es_ES.UTF-8 # Contraseña + - de_DE.UTF-8 # Passwort + - ja_JP.UTF-8 # パスワード + - km_KH.UTF-8 # ពាក្យសម្ងាត់ + - th_TH.UTF-8 # Sหัสผ่าน + - tr_TR.UTF-8 # Şifre + - uk_UA.UTF-8 # Пароль + - vi_VN.UTF-8 # Mật khẩu + block: + - name: Setup the locales + become: yes + block: + - name: ensure debian locales package is available + apt: + name: locales + state: present + + - name: install language packs (also generates locales) + apt: + name: "{{ testlocales | map( 'regex_replace', '^(\\w{2})_.+', 'language-pack-\\1') | list }}" + + - name: Actually test sudo with locales + become_method: sudo + become_user: '{{ target_user_name }}' + become_exe: /tmp/sudoshim.sh + become_flags: '{{ default_become_flags }} {{ intermediate_flags }} --set-lang "{{item}}"' + vars: + ansible_become_password: '{{ intermediate_user_password }}' + shell: 'whoami && echo "${LANG} is lang and ${LC_ALL} is lc_all"' + loop: '{{ testlocales }}' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/gathering_facts/test_run_once.yml new/ansible_core-2.21.6/test/integration/targets/gathering_facts/test_run_once.yml --- old/ansible_core-2.21.5/test/integration/targets/gathering_facts/test_run_once.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/gathering_facts/test_run_once.yml 2026-11-02 18:32:18.000000000 +0100 @@ -30,3 +30,35 @@ file: path: /etc/ansible/facts.d/uuid.fact state: absent + +# https://github.com/ansible/ansible/issues/87415 -- run_once broadcasts one +# templated result object to every target host; a later per-host fact update +# on one of them must not leak onto the others via a shared cached dict. +- hosts: facthost3,facthost4 + gather_facts: no + tasks: + - name: Gather initial facts with run_once + facts_one: + run_once: True + + - name: Gather additional facts for one host + facts_two: + when: inventory_hostname == "facthost4" + + - name: Assert the untouched host still has only its run_once facts + assert: + that: + - hostvars.facthost3.ansible_facts.factsone == 'from facts_one module' + - hostvars.facthost3.ansible_facts.common_fact == 'also from facts_one module' + - "hostvars.facthost3.ansible_facts.common_dict_fact == {'key_one': 'from facts_one', 'key_two': 'from facts_one'}" + - "'factstwo' not in hostvars.facthost3.ansible_facts" + msg: "facthost3 was polluted by facthost4's own later fact update -- the run_once broadcast aliased a shared dict across hosts" + + - name: Assert the updated host has its own additional facts + assert: + that: + - hostvars.facthost4.ansible_facts.factsone == 'from facts_one module' + - hostvars.facthost4.ansible_facts.factstwo == 'from facts_two module' + - hostvars.facthost4.ansible_facts.common_fact == 'also from facts_two module' + - "hostvars.facthost4.ansible_facts.common_dict_fact == {'key_two': 'from facts_two', 'key_four': 'from facts_two'}" + msg: "facthost4 did not receive its own per-host fact update" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/integration/targets/user/tasks/test_password_lock.yml new/ansible_core-2.21.6/test/integration/targets/user/tasks/test_password_lock.yml --- old/ansible_core-2.21.5/test/integration/targets/user/tasks/test_password_lock.yml 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/integration/targets/user/tasks/test_password_lock.yml 2026-11-02 18:32:18.000000000 +0100 @@ -77,6 +77,74 @@ - password_lock_7 is changed - password_lock_8 is not changed + - name: Lock account then omit password_lock + user: + name: ansibulluser + password_lock: yes + + - name: Modify user with neither password nor password_lock + user: + name: ansibulluser + register: password_lock_omit_1 + + - name: Modify user with neither password nor password_lock again + user: + name: ansibulluser + register: password_lock_omit_2 + + - name: Ensure omitted password_lock does not change the account + assert: + that: + - password_lock_omit_1 is not changed + - password_lock_omit_2 is not changed + + - name: LINUX | Get shadow after omitting password_lock + getent: + database: shadow + key: ansibulluser + when: ansible_facts.system == 'Linux' + + - name: LINUX | Ensure lock is preserved when password_lock is omitted + assert: + that: + - getent_shadow['ansibulluser'][0].startswith('!') + when: ansible_facts.system == 'Linux' + + - name: Change password without password_lock on BusyBox + when: ansible_distribution == 'Alpine' + block: + - name: Set new password and lock account + user: + name: ansibulluser + password: "$y$jCT$ZiF3ZV39/maUl9Lzt2Hk80$Ih6bI4OXU52OnWWqt3T1BAmVn8eH.4qlcP.8/NOjGN5" #gitleaks:allow + password_lock: yes + register: password_lock_9 + + - name: Ensure lock marker is preserved on password change + assert: + that: + - password_lock_9.changed + + - name: Modify user without password_lock + user: + name: ansibulluser + register: password_lock_10 + + - name: Ensure lock marker is preserved on password change + assert: + that: + - not password_lock_10.changed + + - name: Get shadow after password change + getent: + database: shadow + key: ansibulluser + + - name: Ensure lock marker is preserved when user is modified without password_lock + assert: + that: + - getent_shadow['ansibulluser'][0].startswith('!') + - name: Lock account user: name: ansibulluser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/lib/ansible_test/_data/completion/remote.txt new/ansible_core-2.21.6/test/lib/ansible_test/_data/completion/remote.txt --- old/ansible_core-2.21.5/test/lib/ansible_test/_data/completion/remote.txt 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/lib/ansible_test/_data/completion/remote.txt 2026-11-02 18:32:18.000000000 +0100 @@ -2,14 +2,14 @@ alpine become=doas_sudo provider=aws arch=x86_64 fedora/43 python=3.14 become=sudo provider=aws arch=x86_64 alias=fedora/latest fedora become=sudo provider=aws arch=x86_64 -freebsd/14.4 python=3.14 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/14 -freebsd/15.0 python=3.12 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/15,freebsd/latest +freebsd/14.5 python=3.14 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/14 +freebsd/15.1 python=3.14 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 alias=freebsd/15,freebsd/latest freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 macos/26.3 python=3.14 python_dir=/usr/local/bin become=sudo provider=mac arch=aarch64 alias=macos/26,macos/latest macos python_dir=/usr/local/bin become=sudo provider=mac arch=aarch64 rhel/8.10 python=3.12 become=sudo provider=aws arch=x86_64 alias=rhel/8 -rhel/9.7 python=3.9,3.12 become=sudo provider=aws arch=x86_64 alias=rhel/9 -rhel/10.1 python=3.12 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest +rhel/9.8 python=3.9,3.14 become=sudo provider=aws arch=x86_64 alias=rhel/9 +rhel/10.2 python=3.12,3.14 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest rhel become=sudo provider=aws arch=x86_64 ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64 ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64 alias=ubuntu/latest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/lib/ansible_test/_util/target/setup/bootstrap.sh new/ansible_core-2.21.6/test/lib/ansible_test/_util/target/setup/bootstrap.sh --- old/ansible_core-2.21.5/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/lib/ansible_test/_util/target/setup/bootstrap.sh 2026-11-02 18:32:18.000000000 +0100 @@ -297,12 +297,12 @@ # Declare platform/python version combinations which do not have supporting OS packages available. # For these combinations ansible-test will use pip to install the requirements instead. case "${platform_version}/${python_version}" in - "14.4/3.14") + "14.5/3.14") jinja2_pkg="" # not available cryptography_pkg="" # not available pyyaml_pkg="" # not available ;; - "15.0/3.12") + "15.1/3.14") jinja2_pkg="" # not available cryptography_pkg="" # not available pyyaml_pkg="" # not available @@ -426,10 +426,15 @@ { optimize_dnf - py_pkg_prefix="python3" + if [ "${python_version}" = "3.12" ]; then + py_pkg_prefix="python3" + else + py_pkg_prefix="python${python_version}" + fi packages=" gcc + python3-packaging ${py_pkg_prefix}-devel ${py_pkg_prefix}-pip " @@ -438,11 +443,19 @@ packages=" ${packages} ${py_pkg_prefix}-cryptography - ${py_pkg_prefix}-jinja2 - ${py_pkg_prefix}-packaging ${py_pkg_prefix}-pyyaml - ${py_pkg_prefix}-resolvelib " + + if [ "${python_version}" = "3.12" ]; then + # These packages are not available for all Python versions. + # Install them when possible, otherwise let ansible-test install them from PyPI. + packages=" + ${packages} + ${py_pkg_prefix}-jinja2 + ${py_pkg_prefix}-packaging + ${py_pkg_prefix}-resolvelib + " + fi fi retry_init diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/units/cli/galaxy/test_collection_extract_tar.py new/ansible_core-2.21.6/test/units/cli/galaxy/test_collection_extract_tar.py --- old/ansible_core-2.21.5/test/units/cli/galaxy/test_collection_extract_tar.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/units/cli/galaxy/test_collection_extract_tar.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import annotations - -import pytest - -from ansible.galaxy.collection import _extract_tar_dir - - [email protected] -def fake_tar_obj(mocker): - m_tarfile = mocker.Mock() - m_tarfile.type = mocker.Mock(return_value=b'99') - m_tarfile.SYMTYPE = mocker.Mock(return_value=b'22') - - return m_tarfile - - -def test_extract_tar_dir_exists(mocker, fake_tar_obj): - mocker.patch('os.makedirs', return_value=None) - m_makedir = mocker.patch('os.mkdir', return_value=None) - mocker.patch('os.path.isdir', return_value=True) - - _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest') - - assert not m_makedir.called - - -def test_extract_tar_dir_does_not_exist(mocker, fake_tar_obj): - mocker.patch('os.makedirs', return_value=None) - m_makedir = mocker.patch('os.mkdir', return_value=None) - mocker.patch('os.path.isdir', return_value=False) - - _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest') - - assert m_makedir.called - assert m_makedir.call_args[0] == (b'/some/dir', 0o0755) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ansible_core-2.21.5/test/units/module_utils/urls/test_mask_url.py new/ansible_core-2.21.6/test/units/module_utils/urls/test_mask_url.py --- old/ansible_core-2.21.5/test/units/module_utils/urls/test_mask_url.py 2026-10-05 17:06:50.000000000 +0200 +++ new/ansible_core-2.21.6/test/units/module_utils/urls/test_mask_url.py 2026-11-02 18:32:18.000000000 +0100 @@ -36,3 +36,45 @@ for notmasked in wanted: assert notmasked in masked + + +_ipv6 = 'Invalid IPv6 URL' + + [email protected]( + 'url, expected', + ( + ('http://secretuser:secretpassword@badunicodeat.com:80/file.html?nothing=something', None), + ('http://secretuser:@badunicodeslash.com:443/file.html', None), + ('http://[email protected]:80', None), + ('http://:secretpassword@badunicodequestion.com:00/file.html?this=breaksparse', None), + ('https://[::1/index.html', _ipv6), + ('https://example.com]:443/index.html', _ipv6), + ('https://[fe80::1:8080/index.html', _ipv6), + ('//[::1/index.html', _ipv6), + ('https://secretuser:secretpass@[::1/index.html', _ipv6), + ('https://secretuser@[::1/index.html', _ipv6), + ('ftp://secretuser:secretpass@[fe80::1:8080/pub/file.txt', _ipv6), + ('https://:secretpass@::1]/index.html', _ipv6), + ('https://secretuser:secretpass@[::1', _ipv6), + # a scheme-relative url has no scheme to anchor on + ('//secretuser:secretpass@[::1/index.html', _ipv6), + # only `/` ends the authority, so a `?` or `#` cannot hide the userinfo + ('https://[::1secretuser:sec?retpass@host/index.html', _ipv6), + ('https://secretuser:secretpass@[::1#fragment', _ipv6), + # the last `@` separates userinfo from host, matching how urlparse splits it + ('https://secretuser:secret@pass@[::1/index.html', _ipv6), + # combine! + ('http://:secretpassword@[::1/:01/badunicodeat/file.html?this=breaksparse', None), + ) +) +def test_mask_url_exceptions(url, expected): + with pytest.raises(ValueError, match="^(?!.*secret).*$") as e: + mask_url(url) + + msg = str(e) + if expected is None: + assert "ValueError('mask_url could not parse the url provided')" in msg + else: + assert 'mask_url could not parse the url provided' in msg + assert expected in msg ++++++ ansible_core-2.21.5.tar.gz.sha256 -> ansible_core-2.21.6.tar.gz.sha256 ++++++ --- /work/SRC/openSUSE:Factory/ansible-core/ansible_core-2.21.5.tar.gz.sha256 2026-10-08 13:28:41.120050075 +0200 +++ /work/SRC/openSUSE:Factory/.ansible-core.new.3759290/ansible_core-2.21.6.tar.gz.sha256 2026-11-06 20:00:45.705176446 +0100 @@ -1 +1 @@ -65bb3148c58ebb843cd5074004c805f404f152114d98476dd823324616c9c6cb ansible_core-2.21.5.tar.gz +3fa05b4b1af69f2fa19b49dde8edd5a23e693c2c1c3bda2d85fce73bd6cfc36d ansible_core-2.21.6.tar.gz
