diff --git a/changelogs/fragments/10689-gem-prevent-soundness-issue.yml b/changelogs/fragments/10689-gem-prevent-soundness-issue.yml new file mode 100644 index 00000000000..a55dba1ea1e --- /dev/null +++ b/changelogs/fragments/10689-gem-prevent-soundness-issue.yml @@ -0,0 +1,2 @@ +bugfixes: + - "gem - fix soundness issue when uninstalling default gems on Ubuntu (https://github.com/ansible-collections/community.general/issues/10451, https://github.com/ansible-collections/community.general/pull/10689)." \ No newline at end of file diff --git a/plugins/modules/gem.py b/plugins/modules/gem.py index 1ea9c68a948..2a458116c73 100644 --- a/plugins/modules/gem.py +++ b/plugins/modules/gem.py @@ -243,7 +243,7 @@ def uninstall(module): if module.params['force']: cmd.append('--force') cmd.append(module.params['name']) - module.run_command(cmd, environ_update=environ, check_rc=True) + return module.run_command(cmd, environ_update=environ, check_rc=True) def install(module): @@ -334,9 +334,21 @@ def main(): changed = True elif module.params['state'] == 'absent': if exists(module): - uninstall(module) - changed = True - + command_output = uninstall(module) + if command_output is not None and exists(module): + rc, out, err = command_output + module.fail_json( + msg=( + "Failed to uninstall gem '%s': it is still present after 'gem uninstall'. " + "This usually happens with default or system gems provided by the OS, " + "which cannot be removed with the gem command." + ) % module.params['name'], + rc=rc, + stdout=out, + stderr=err + ) + else: + changed = True result = {} result['name'] = module.params['name'] result['state'] = module.params['state'] diff --git a/tests/integration/targets/gem/tasks/main.yml b/tests/integration/targets/gem/tasks/main.yml index 0c85e564891..73119c20da2 100644 --- a/tests/integration/targets/gem/tasks/main.yml +++ b/tests/integration/targets/gem/tasks/main.yml @@ -212,3 +212,18 @@ that: - install_gem_result is changed - not gem_bindir_stat.stat.exists + + - name: Attempt to uninstall default gem 'json' + community.general.gem: + name: json + state: absent + when: ansible_distribution == "Ubuntu" + register: gem_result + ignore_errors: true + + - name: Assert gem uninstall failed as expected + when: ansible_distribution == "Ubuntu" + assert: + that: + - gem_result is failed + - gem_result.msg.startswith("Failed to uninstall gem 'json'") diff --git a/tests/unit/plugins/modules/test_gem.py b/tests/unit/plugins/modules/test_gem.py index 78c73be5a6b..3e6df91185b 100644 --- a/tests/unit/plugins/modules/test_gem.py +++ b/tests/unit/plugins/modules/test_gem.py @@ -52,7 +52,9 @@ def new(module): def patch_run_command(self): target = 'ansible.module_utils.basic.AnsibleModule.run_command' - return self.mocker.patch(target) + mock = self.mocker.patch(target) + mock.return_value = (0, '', '') + return mock def test_fails_when_user_install_and_install_dir_are_combined(self): with set_module_args({ @@ -107,12 +109,11 @@ def test_passes_install_dir_and_gem_home_when_uninstall_gem(self): run_command = self.patch_run_command() - with pytest.raises(AnsibleExitJson) as exc: + with pytest.raises(AnsibleFailJson) as exc: gem.main() result = exc.value.args[0] - - assert result['changed'] + assert result['failed'] assert run_command.called assert '--install-dir /opt/dummy' in get_command(run_command)