diff --git a/CVE-2024-22034.patch b/CVE-2024-22034.patch new file mode 100644 index 0000000000000000000000000000000000000000..14fca7b78868896259c44b1039b5a679e1fffc90 --- /dev/null +++ b/CVE-2024-22034.patch @@ -0,0 +1,761 @@ +From 56c08df3d632a462a805be687336a8dfbbb1a4ab Mon Sep 17 00:00:00 2001 +From: Daniel Mach +Date: Thu, 18 Jul 2024 10:36:20 +0200 +Subject: [PATCH] Fix possibility to overwrite special files in .osc + (CVE-2024-22034 boo#1225911) + +Origin: https://github.com/openSUSE/osc/commit/56c08df3d632a462a805be687336a8dfbbb1a4ab + +Source files are now stored in the 'sources' subdirectory which prevents +name collisons. This requires changing version of '.osc' store to 2.0. +--- + osc/commandline.py | 10 +-- + osc/core.py | 160 ++++++++++++++++++++++++++++++-------- + tests/common.py | 4 +- + tests/test_addfiles.py | 12 +-- + tests/test_commit.py | 16 ++-- + tests/test_deletefiles.py | 20 ++--- + tests/test_repairwc.py | 16 ++-- + tests/test_revertfiles.py | 2 +- + tests/test_update.py | 22 +++--- + 9 files changed, 176 insertions(+), 86 deletions(-) + +diff --git a/osc/commandline.py b/osc/commandline.py +index 09a72a5f9c..9958174bca 100644 +--- a/osc/commandline.py ++++ b/osc/commandline.py +@@ -8868,14 +8868,12 @@ def do_repairlink(self, subcmd, opts, *args): + store_write_string(destdir, '_linkrepair', '') + pac = Package(destdir) + +- storedir = os.path.join(destdir, store) +- + for name in sorted(entries.keys()): + md5_old = entries_old.get(name, '') + md5_new = entries_new.get(name, '') + md5_oldpatched = entries_oldpatched.get(name, '') + if md5_new != '': +- self.download(name, md5_new, dir_new, os.path.join(storedir, name)) ++ self.download(name, md5_new, dir_new, store_sources_get_path(destdir, name)) + if md5_old == md5_new: + if md5_oldpatched == '': + pac.put_on_deletelist(name) +@@ -8887,17 +8885,17 @@ def do_repairlink(self, subcmd, opts, *args): + if md5_new == '': + continue + print(statfrmt('U', name)) +- shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name)) ++ shutil.copy2(store_sources_get_path(destdir, name), os.path.join(destdir, name)) + continue + if md5_new == md5_oldpatched: + if md5_new == '': + continue + print(statfrmt('G', name)) +- shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name)) ++ shutil.copy2(store_sources_get_path(destdir, name), os.path.join(destdir, name)) + continue + self.download(name, md5_oldpatched, dir_oldpatched, os.path.join(destdir, name + '.mine')) + if md5_new != '': +- shutil.copy2(os.path.join(storedir, name), os.path.join(destdir, name + '.new')) ++ shutil.copy2(store_sources_get_path(destdir, name), os.path.join(destdir, name + '.new')) + else: + self.download(name, md5_new, dir_new, os.path.join(destdir, name + '.new')) + self.download(name, md5_old, dir_old, os.path.join(destdir, name + '.old')) +diff --git a/osc/core.py b/osc/core.py +index 5c0a21ac21..15b2a1e0a4 100644 +--- a/osc/core.py ++++ b/osc/core.py +@@ -13,7 +13,7 @@ + # __store_version__ is to be incremented when the format of the working copy + # "store" changes in an incompatible way. Please add any needed migration + # functionality to check_store_version(). +-__store_version__ = '1.0' ++__store_version__ = '2.0' + + import locale + import os +@@ -1213,16 +1213,13 @@ def wc_check(self): + def wc_check(self): + dirty_files = [] + for fname in self.filenamelist: +- if not os.path.exists(os.path.join(self.storedir, fname)) and not fname in self.skipped: ++ if not store_sources_is_file(self.absdir, fname) and fname not in self.skipped: + dirty_files.append(fname) + for fname in Package.REQ_STOREFILES: + if not os.path.isfile(os.path.join(self.storedir, fname)): + dirty_files.append(fname) +- for fname in os.listdir(self.storedir): +- if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \ +- fname.startswith('_build'): +- continue +- elif fname in self.filenamelist and fname in self.skipped: ++ for fname in store_sources_list_files(self.absdir): ++ if fname in self.filenamelist and fname in self.skipped: + dirty_files.append(fname) + elif not fname in self.filenamelist: + dirty_files.append(fname) +@@ -1248,18 +1245,20 @@ def wc_repair(self, apiurl=None): + # all files which are present in the filelist have to exist in the storedir + for f in self.filelist: + # XXX: should we also check the md5? +- if not os.path.exists(os.path.join(self.storedir, f.name)) and not f.name in self.skipped: ++ if not store_sources_is_file(self.absdir, f.name) and f.name not in self.skipped: + # if get_source_file fails we're screwed up... + get_source_file(self.apiurl, self.prjname, self.name, f.name, +- targetfilename=os.path.join(self.storedir, f.name), revision=self.rev, ++ targetfilename=store_sources_get_path(self.absdir, f.name), revision=self.rev, + mtime=f.mtime) + for fname in os.listdir(self.storedir): + if fname in Package.REQ_STOREFILES or fname in Package.OPT_STOREFILES or \ + fname.startswith('_build'): + continue +- elif not fname in self.filenamelist or fname in self.skipped: ++ ++ for fname in store_sources_list_files(self.absdir): ++ if fname not in self.filenamelist or fname in self.skipped: + # this file does not belong to the storedir so remove it +- os.unlink(os.path.join(self.storedir, fname)) ++ os.unlink(store_sources_get_path(self.absdir, fname)) + for fname in self.to_be_deleted[:]: + if not fname in self.filenamelist: + self.to_be_deleted.remove(fname) +@@ -1279,11 +1278,9 @@ def addfile(self, n): + raise oscerr.OscIOError(None, 'error: file \'%s\' does not exist' % n) + if n in self.to_be_deleted: + self.to_be_deleted.remove(n) +-# self.delete_storefile(n) + self.write_deletelist() + elif n in self.filenamelist or n in self.to_be_added: + raise oscerr.PackageFileConflict(self.prjname, self.name, n, 'osc: warning: \'%s\' is already under version control' % n) +-# shutil.copyfile(os.path.join(self.dir, n), os.path.join(self.storedir, n)) + if self.dir != '.': + pathname = os.path.join(self.dir, n) + else: +@@ -1348,7 +1345,7 @@ def clear_from_conflictlist(self, n): + if n in self.in_conflict: + + filename = os.path.join(self.dir, n) +- storefilename = os.path.join(self.storedir, n) ++ storefilename = store_sources_get_path(self.absdir, n) + myfilename = os.path.join(self.dir, n + '.mine') + upfilename = os.path.join(self.dir, n + '.new') + +@@ -1392,7 +1389,7 @@ def write_deletelist(self): + def delete_source_file(self, n): + """delete local a source file""" + self.delete_localfile(n) +- self.delete_storefile(n) ++ store_sources_delete_file(self.absdir, n) + + def delete_remote_source_file(self, n): + """delete a remote source file (e.g. from the server)""" +@@ -1415,7 +1412,7 @@ def put_source_file(self, n, tdir, copy_only=False): + def __commit_update_store(self, tdir): + """move files from transaction directory into the store""" + for filename in os.listdir(tdir): +- os.rename(os.path.join(tdir, filename), os.path.join(self.storedir, filename)) ++ os.rename(os.path.join(tdir, filename), store_sources_get_path(self.absdir, filename)) + + def __generate_commitlist(self, todo_send): + root = ET.Element('directory') +@@ -1549,7 +1546,7 @@ def commit(self, msg='', verbose=False, skip_local_service_run=False, can_branch + # in sha256sums. + # The storefile is guaranteed to exist (since we have a + # pulled/linkrepair wc, the file cannot have state 'S') +- storefile = os.path.join(self.storedir, filename) ++ storefile = store_sources_get_path(self.absdir, filename) + sha256sums[filename] = sha256_dgst(storefile) + + if not force and not real_send and not todo_delete and not self.islinkrepair() and not self.ispulled(): +@@ -1629,7 +1626,7 @@ def commit(self, msg='', verbose=False, skip_local_service_run=False, can_branch + store_write_string(self.absdir, '_files', ET.tostring(sfilelist, encoding=ET_ENCODING) + '\n') + for filename in todo_delete: + self.to_be_deleted.remove(filename) +- self.delete_storefile(filename) ++ store_sources_delete_file(self.absdir, filename) + self.write_deletelist() + self.write_addlist() + self.update_datastructs() +@@ -1670,7 +1667,7 @@ def write_conflictlist(self): + + def updatefile(self, n, revision, mtime=None): + filename = os.path.join(self.dir, n) +- storefilename = os.path.join(self.storedir, n) ++ storefilename = store_sources_get_path(self.absdir, n) + origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n) + origfile = os.path.join(self.storedir, '_in_update', n) + if os.path.isfile(filename): +@@ -1690,7 +1687,7 @@ def updatefile(self, n, revision, mtime=None): + + def mergefile(self, n, revision, mtime=None): + filename = os.path.join(self.dir, n) +- storefilename = os.path.join(self.storedir, n) ++ storefilename = store_sources_get_path(self.absdir, n) + myfilename = os.path.join(self.dir, n + '.mine') + upfilename = os.path.join(self.dir, n + '.new') + origfile_tmp = os.path.join(self.storedir, '_in_update', '%s.copy' % n) +@@ -1966,7 +1963,7 @@ def status(self, n): + known_by_meta = True + if os.path.exists(localfile): + exists = True +- if os.path.exists(os.path.join(self.storedir, n)): ++ if store_sources_is_file(self.absdir, n): + exists_in_store = True + + if n in self.to_be_deleted: +@@ -2040,7 +2037,7 @@ def diff_add_delete(fname, add, revision): + b_revision = self.rev.encode() + diff.append(b'--- %s\t(revision %s)\n' % (fname.encode(), b_revision)) + diff.append(b'+++ %s\t(working copy)\n' % fname.encode()) +- fname = os.path.join(self.storedir, fname) ++ fname = store_sources_get_path(self.absdir, fname) + + try: + if revision is not None and not add: +@@ -2376,8 +2373,8 @@ def update(self, rev = None, service_files = False, size_limit = None): + raise oscerr.PackageInternalError(self.prjname, self.name, 'too many files in \'_in_update\' dir') + tmp = rfiles[:] + for f in tmp: +- if os.path.exists(os.path.join(self.storedir, f.name)): +- if dgst(os.path.join(self.storedir, f.name)) == f.md5: ++ if store_sources_is_file(self.absdir, f.name): ++ if dgst(store_sources_get_path(self.absdir, f.name)) == f.md5: + if f in kept: + kept.remove(f) + elif f in added: +@@ -2419,10 +2416,10 @@ def __update(self, kept, added, deleted, services, fm, rev): + # if the storefile doesn't exist we're resuming an aborted update: + # the file was already deleted but we cannot know this + # OR we're processing a _service: file (simply keep the file) +- if os.path.isfile(os.path.join(self.storedir, f.name)) and self.status(f.name) not in ('M', 'C'): ++ if store_sources_is_file(self.absdir, f.name) and self.status(f.name) not in ('M', 'C'): + # if self.status(f.name) != 'M': + self.delete_localfile(f.name) +- self.delete_storefile(f.name) ++ store_sources_delete_file(self.absdir, f.name) + print(statfrmt('D', os.path.join(pathn, f.name))) + if f.name in self.to_be_deleted: + self.to_be_deleted.remove(f.name) +@@ -2446,7 +2443,7 @@ def __update(self, kept, added, deleted, services, fm, rev): + print('Restored \'%s\'' % os.path.join(pathn, f.name)) + elif state == 'C': + get_source_file(self.apiurl, self.prjname, self.name, f.name, +- targetfilename=os.path.join(self.storedir, f.name), revision=rev, ++ targetfilename=store_sources_get_path(self.absdir, f.name), revision=rev, + progress_obj=self.progress_obj, mtime=f.mtime, meta=self.meta) + print('skipping \'%s\' (this is due to conflicts)' % f.name) + elif state == 'D' and self.findfilebyname(f.name).md5 != f.md5: +@@ -2506,11 +2503,11 @@ def revert(self, filename): + raise oscerr.OscIOError(None, 'file \'%s\' is not under version control' % filename) + elif filename in self.skipped: + raise oscerr.OscIOError(None, 'file \'%s\' is marked as skipped and cannot be reverted' % filename) +- if filename in self.filenamelist and not os.path.exists(os.path.join(self.storedir, filename)): ++ if filename in self.filenamelist and not store_sources_is_file(self.absdir, filename): + raise oscerr.PackageInternalError('file \'%s\' is listed in filenamelist but no storefile exists' % filename) + state = self.status(filename) + if not (state == 'A' or state == '!' and filename in self.to_be_added): +- shutil.copyfile(os.path.join(self.storedir, filename), os.path.join(self.absdir, filename)) ++ shutil.copyfile(store_sources_get_path(self.absdir, filename), os.path.join(self.absdir, filename)) + if state == 'D': + self.to_be_deleted.remove(filename) + self.write_deletelist() +@@ -3483,12 +3480,43 @@ def check_store_version(dir): + raise oscerr.NoWorkingCopy(msg) + + if v != __store_version__: ++ migrated = False ++ + if v in ['0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '0.95', '0.96', '0.97', '0.98', '0.99']: +- # version is fine, no migration needed +- f = open(versionfile, 'w') +- f.write(__store_version__ + '\n') +- f.close() ++ # no migration needed, only change metadata version to 1.0 ++ v = "1.0" ++ with open(versionfile, 'w') as f: ++ f.write(v + '\n') ++ migrated = True ++ ++ if v == "1.0": ++ store_dir = os.path.join(dir, store) ++ sources_dir = os.path.join(dir, store, "sources") ++ os.makedirs(sources_dir, exist_ok=True) ++ ++ if is_package_dir(dir) and not store_read_scmurl(dir): ++ scm_files = [i.name for i in store_read_files(dir)] ++ ++ for fn in os.listdir(store_dir): ++ old_path = os.path.join(store_dir, fn) ++ new_path = os.path.join(sources_dir, fn) ++ if not os.path.isfile(old_path): ++ continue ++ if fn in Package.REQ_STOREFILES or fn in Package.OPT_STOREFILES: ++ continue ++ if fn.startswith("_") and fn not in scm_files: ++ continue ++ if os.path.isfile(old_path): ++ os.rename(old_path, new_path) ++ ++ v = "2.0" ++ with open(versionfile, 'w') as f: ++ f.write(v + '\n') ++ migrated = True ++ ++ if migrated: + return ++ + msg = 'The osc metadata of your working copy "%s"' % dir + msg += '\nhas __store_version__ = %s, but it should be %s' % (v, __store_version__) + msg += '\nPlease do a fresh checkout or update your client. Sorry about the inconvenience.' +@@ -4889,7 +4917,7 @@ def get_source_file_diff(dir, filename, rev, oldfilename = None, olddir = None, + oldfilename = filename + + if not olddir: +- olddir = os.path.join(dir, store) ++ olddir = os.path.join(dir, store, "sources") + + if not origfilename: + origfilename = filename +@@ -6796,6 +6824,70 @@ def store_write_initial_packages(dir, project, subelements): + root.append(elem) + ET.ElementTree(root).write(fname) + ++ ++def store_read_files(dir): ++ files_tree = read_filemeta(dir) ++ files_tree_root = files_tree.getroot() ++ result = [] ++ for node in files_tree_root.findall('entry'): ++ f = File( ++ node.get('name'), ++ node.get('md5'), ++ int(node.get('size')), ++ int(node.get('mtime')), ++ ) ++ if node.get('skipped'): ++ f.skipped = True ++ result.append(f) ++ return result ++ ++ ++def store_sources_get_path(dir, file_name): ++ if "/" in file_name: ++ raise ValueError("Plain file name expected: %s" % file_name) ++ result = os.path.join(dir, store, "sources", file_name) ++ try: ++ os.makedirs(os.path.dirname(result)) ++ except FileExistsError: ++ pass ++ return result ++ ++ ++def store_sources_list_files(dir): ++ result = [] ++ invalid = [] ++ ++ topdir = os.path.join(dir, store, "sources") ++ ++ if not os.path.isdir(topdir): ++ return [] ++ ++ for fn in os.listdir(topdir): ++ if store_sources_is_file(dir, fn): ++ result.append(fn) ++ else: ++ invalid.append(fn) ++ ++ if invalid: ++ msg = ".osc/sources contains entries other than regular files" ++ project = store_read_project(dir) ++ package = store_read_package(dir) ++ raise oscerr.WorkingCopyInconsistent(project, package, invalid, msg) ++ ++ return result ++ ++ ++def store_sources_is_file(dir, file_name): ++ return os.path.isfile(store_sources_get_path(dir, file_name)) ++ ++ ++def store_sources_delete_file(dir, file_name): ++ try: ++ os.unlink(store_sources_get_path(dir, file_name)) ++ except: ++ pass ++ ++ + def get_osc_version(): + return __version__ + +diff --git a/tests/common.py b/tests/common.py +index f97655cbd1..ce15c7ab99 100644 +--- a/tests/common.py ++++ b/tests/common.py +@@ -239,8 +239,8 @@ def _check_digests(self, fname, *skipfiles): + for i in root.findall('entry'): + if i.get('name') in skipfiles: + continue +- self.assertTrue(os.path.exists(os.path.join('.osc', i.get('name')))) +- self.assertEqual(osc.core.dgst(os.path.join('.osc', i.get('name'))), i.get('md5')) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', i.get('name')))) ++ self.assertEqual(osc.core.dgst(os.path.join('.osc', 'sources', i.get('name'))), i.get('md5')) + + def assertXMLEqual(self, act, exp): + if xml_equal(act, exp): +diff --git a/tests/test_addfiles.py b/tests/test_addfiles.py +index 75d3842031..369a941b07 100644 +--- a/tests/test_addfiles.py ++++ b/tests/test_addfiles.py +@@ -21,7 +21,7 @@ def testSimpleAdd(self): + p.addfile('toadd1') + exp = 'A toadd1\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd1'))) + self._check_status(p, 'toadd1', 'A') + self._check_addlist('toadd1\n') + +@@ -33,8 +33,8 @@ def testSimpleMultipleAdd(self): + p.addfile('toadd2') + exp = 'A toadd1\nA toadd2\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1'))) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd2'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd1'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd2'))) + self._check_status(p, 'toadd1', 'A') + self._check_status(p, 'toadd2', 'A') + self._check_addlist('toadd1\ntoadd2\n') +@@ -55,7 +55,7 @@ def testAddUnversionedFileTwice(self): + self.assertRaises(osc.oscerr.PackageFileConflict, p.addfile, 'toadd1') + exp = 'A toadd1\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd1'))) + self._check_status(p, 'toadd1', 'A') + self._check_addlist('toadd1\n') + +@@ -67,8 +67,8 @@ def testReplace(self): + p.addfile('foo') + exp = 'A foo\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) +- self.assertNotEqual(open(os.path.join('.osc', 'foo'), 'r').read(), 'replaced file\n') ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) ++ self.assertNotEqual(open(os.path.join('.osc', 'sources', 'foo'), 'r').read(), 'replaced file\n') + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) + self._check_status(p, 'foo', 'R') + self._check_addlist('foo\n') +diff --git a/tests/test_commit.py b/tests/test_commit.py +index d2ba8c2ea0..54357211e9 100644 +--- a/tests/test_commit.py ++++ b/tests/test_commit.py +@@ -47,7 +47,7 @@ def test_simple(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testSimple_cfilesremote') + self.assertTrue(os.path.exists('nochange')) +- self.assertEqual(open('nochange', 'r').read(), open(os.path.join('.osc', 'nochange'), 'r').read()) ++ self.assertEqual(open('nochange', 'r').read(), open(os.path.join('.osc', 'sources', 'nochange'), 'r').read()) + self._check_status(p, 'nochange', ' ') + self._check_status(p, 'foo', ' ') + self._check_status(p, 'merge', ' ') +@@ -71,7 +71,7 @@ def test_addfile(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testAddfile_cfilesremote') + self.assertTrue(os.path.exists('add')) +- self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'add'), 'r').read()) ++ self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'sources', 'add'), 'r').read()) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added'))) + self._check_status(p, 'add', ' ') + self._check_status(p, 'foo', ' ') +@@ -93,7 +93,7 @@ def test_deletefile(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testDeletefile_cfilesremote') + self.assertFalse(os.path.exists('nochange')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'nochange'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'nochange'))) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) + self._check_status(p, 'foo', ' ') + self._check_status(p, 'merge', ' ') +@@ -148,8 +148,8 @@ def test_multiple(self): + self._check_digests('testMultiple_cfilesremote') + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added'))) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'foo'))) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'merge'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'merge'))) + self.assertRaises(osc.oscerr.OscIOError, p.status, 'foo') + self.assertRaises(osc.oscerr.OscIOError, p.status, 'merge') + self._check_status(p, 'add', ' ') +@@ -225,7 +225,7 @@ def test_allstates(self): + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added'))) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) + self.assertFalse(os.path.exists('foo')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_status(p, 'add', ' ') + self._check_status(p, 'nochange', ' ') + self._check_status(p, 'merge', '!') +@@ -248,7 +248,7 @@ def test_remoteexists(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testAddfile_cfilesremote') + self.assertTrue(os.path.exists('add')) +- self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'add'), 'r').read()) ++ self.assertEqual(open('add', 'r').read(), open(os.path.join('.osc', 'sources', 'add'), 'r').read()) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added'))) + self._check_status(p, 'add', ' ') + self._check_status(p, 'foo', ' ') +@@ -348,7 +348,7 @@ def test_simple_sha256(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testSimple_cfilesremote') + self.assertTrue(os.path.exists('nochange')) +- self.assertEqual(open('nochange', 'r').read(), open(os.path.join('.osc', 'nochange'), 'r').read()) ++ self.assertEqual(open('nochange', 'r').read(), open(os.path.join('.osc', 'sources', 'nochange'), 'r').read()) + self._check_status(p, 'nochange', ' ') + self._check_status(p, 'foo', ' ') + self._check_status(p, 'merge', ' ') +diff --git a/tests/test_deletefiles.py b/tests/test_deletefiles.py +index 566ddf4784..611aa2d022 100644 +--- a/tests/test_deletefiles.py ++++ b/tests/test_deletefiles.py +@@ -20,7 +20,7 @@ def testSimpleRemove(self): + ret = p.delete_file('foo') + self.__check_ret(ret, True, ' ') + self.assertFalse(os.path.exists('foo')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + +@@ -31,7 +31,7 @@ def testDeleteModified(self): + ret = p.delete_file('nochange') + self.__check_ret(ret, False, 'M') + self.assertTrue(os.path.exists('nochange')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'nochange'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'nochange'))) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) + self._check_status(p, 'nochange', 'M') + +@@ -73,7 +73,7 @@ def testDeleteConflict(self): + ret = p.delete_file('foo') + self.__check_ret(ret, False, 'C') + self.assertTrue(os.path.exists('foo')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_deleted'))) + self._check_conflictlist('foo\n') + self._check_status(p, 'foo', 'C') +@@ -85,7 +85,7 @@ def testDeleteModifiedForce(self): + ret = p.delete_file('nochange', force=True) + self.__check_ret(ret, True, 'M') + self.assertFalse(os.path.exists('nochange')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'nochange'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'nochange'))) + self._check_deletelist('nochange\n') + self._check_status(p, 'nochange', 'D') + +@@ -117,7 +117,7 @@ def testDeleteReplacedForce(self): + ret = p.delete_file('merge', force=True) + self.__check_ret(ret, True, 'R') + self.assertFalse(os.path.exists('merge')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'merge'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'merge'))) + self._check_deletelist('merge\n') + self._check_addlist('toadd1\n') + self._check_status(p, 'merge', 'D') +@@ -131,7 +131,7 @@ def testDeleteConflictForce(self): + self.assertFalse(os.path.exists('foo')) + self.assertTrue(os.path.exists('foo.r2')) + self.assertTrue(os.path.exists('foo.mine')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_deletelist('foo\n') + self.assertFalse(os.path.exists(os.path.join('.osc', '_in_conflict'))) + self._check_status(p, 'foo', 'D') +@@ -146,8 +146,8 @@ def testDeleteMultiple(self): + self.__check_ret(ret, True, ' ') + self.assertFalse(os.path.exists('foo')) + self.assertFalse(os.path.exists('merge')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'merge'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'merge'))) + self._check_deletelist('foo\nmerge\n') + + def testDeleteAlreadyDeleted(self): +@@ -157,7 +157,7 @@ def testDeleteAlreadyDeleted(self): + ret = p.delete_file('foo') + self.__check_ret(ret, True, 'D') + self.assertFalse(os.path.exists('foo')) +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + +@@ -171,7 +171,7 @@ def testDeleteAddedMissing(self): + ret = p.delete_file('toadd1') + self.__check_ret(ret, True, '!') + self.assertFalse(os.path.exists('toadd1')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd1'))) + self._check_deletelist('foo\n') + self.assertFalse(os.path.exists(os.path.join('.osc', '_to_be_added'))) + +diff --git a/tests/test_repairwc.py b/tests/test_repairwc.py +index 0a4d506a71..bb2d42c8b5 100644 +--- a/tests/test_repairwc.py ++++ b/tests/test_repairwc.py +@@ -53,7 +53,7 @@ def test_simple1(self): + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'M') +@@ -68,7 +68,7 @@ def test_simple2(self): + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertFalse(os.path.exists(os.path.join('.osc', 'somefile'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'somefile'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'M') +@@ -78,12 +78,12 @@ def test_simple2(self): + self.__assertNotRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + + def test_simple3(self): +- """toadd1 has state 'A' and a file .osc/toadd1 exists""" ++ """toadd1 has state 'A' and a file .osc/sources/toadd1 exists""" + self._change_to_pkg('simple3') + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertFalse(os.path.exists(os.path.join('.osc', 'toadd1'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'toadd1'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'M') +@@ -132,7 +132,7 @@ def test_simple6(self): + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'M') +@@ -154,7 +154,7 @@ def test_simple8(self): + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertFalse(os.path.exists(os.path.join('.osc', 'skipped'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'skipped'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'M') +@@ -177,8 +177,8 @@ def test_multiple(self): + self.assertRaises(osc.oscerr.WorkingCopyInconsistent, osc.core.Package, '.') + p = osc.core.Package('.', wc_check=False) + p.wc_repair() +- self.assertTrue(os.path.exists(os.path.join('.osc', 'foo'))) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'unknown_file'))) ++ self.assertTrue(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'unknown_file'))) + self._check_deletelist('foo\n') + self._check_status(p, 'foo', 'D') + self._check_status(p, 'nochange', 'C') +diff --git a/tests/test_revertfiles.py b/tests/test_revertfiles.py +index 121fea39da..42a6cc04c6 100644 +--- a/tests/test_revertfiles.py ++++ b/tests/test_revertfiles.py +@@ -87,7 +87,7 @@ def testRevertSkipped(self): + self.assertRaises(osc.oscerr.OscIOError, p.revert, 'skipped') + + def __check_file(self, fname): +- storefile = os.path.join('.osc', fname) ++ storefile = os.path.join('.osc', 'sources', fname) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.exists(storefile)) + self.assertEqual(open(fname, 'r').read(), open(storefile, 'r').read()) +diff --git a/tests/test_update.py b/tests/test_update.py +index 251268e8a7..d7678a72d0 100644 +--- a/tests/test_update.py ++++ b/tests/test_update.py +@@ -51,7 +51,7 @@ def testUpdateDeletedFile(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_digests('testUpdateDeletedFile_files') + self.assertFalse(os.path.exists('foo')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + + @GET('http://localhost/source/osctest/simple?rev=2', file='testUpdateUpstreamModifiedFile_files') + @GET('http://localhost/source/osctest/simple/foo?rev=2', file='testUpdateUpstreamModifiedFile_foo') +@@ -111,7 +111,7 @@ def testUpdateLocalDeletions(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self._check_deletelist('foo\n') + self._check_conflictlist('merge\n') +- self.assertEqual(open('foo', 'r').read(), open(os.path.join('.osc', 'foo'), 'r').read()) ++ self.assertEqual(open('foo', 'r').read(), open(os.path.join('.osc', 'sources', 'foo'), 'r').read()) + self._check_digests('testUpdateLocalDeletions_files') + + @GET('http://localhost/source/osctest/restore?rev=latest', file='testUpdateRestore_files') +@@ -136,7 +136,7 @@ def testUpdateLimitSizeNoChange(self): + osc.core.Package('.').update(size_limit=50) + exp = 'D bigfile\nAt revision 2.\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'bigfile'))) + self.assertFalse(os.path.exists('bigfile')) + self._check_digests('testUpdateLimitSizeNoChange_files', 'bigfile') + +@@ -152,8 +152,8 @@ def testUpdateLocalLimitSizeNoChange(self): + p.update() + exp = 'D bigfile\nD merge\nAt revision 2.\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile'))) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'merge'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'bigfile'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'merge'))) + self.assertFalse(os.path.exists('bigfile')) + self._check_digests('testUpdateLocalLimitSizeNoChange_files', 'bigfile', 'merge') + self._check_status(p, 'bigfile', 'S') +@@ -174,11 +174,11 @@ def testUpdateLimitSizeAddDelete(self): + osc.core.Package('.').update(size_limit=10) + exp = 'A exists\nD bigfile\nD foo\nD merge\nD nochange\nAt revision 2.\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'bigfile'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'bigfile'))) + self.assertFalse(os.path.exists('bigfile')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'foo'))) + self.assertFalse(os.path.exists('foo')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'merge'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'merge'))) + self.assertFalse(os.path.exists('merge')) + # exists because local version is modified + self.assertTrue(os.path.exists('nochange')) +@@ -214,9 +214,9 @@ def testUpdateDisableAddDeleteServiceFiles(self): + osc.core.Package('.').update() + exp = 'A bigfile\nD _service:exists\nAt revision 2.\n' + self.assertEqual(sys.stdout.getvalue(), exp) +- self.assertFalse(os.path.exists(os.path.join('.osc', '_service:bar'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', '_service:bar'))) + self.assertFalse(os.path.exists('_service:bar')) +- self.assertFalse(os.path.exists(os.path.join('.osc', '_service:foo'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', '_service:foo'))) + self.assertFalse(os.path.exists('_service:foo')) + self.assertTrue(os.path.exists('_service:exists')) + self._check_digests('testUpdateServiceFilesAddDelete_files', '_service:foo', '_service:bar') +@@ -280,7 +280,7 @@ def testUpdateResumeDeletedFile(self): + self.assertEqual(sys.stdout.getvalue(), exp) + self.assertFalse(os.path.exists(os.path.join('.osc', '_in_update'))) + self.assertFalse(os.path.exists('added')) +- self.assertFalse(os.path.exists(os.path.join('.osc', 'added'))) ++ self.assertFalse(os.path.exists(os.path.join('.osc', 'sources', 'added'))) + self._check_digests('testUpdateResumeDeletedFile_files') + + if __name__ == '__main__': diff --git a/osc.spec b/osc.spec index c825df1b9167c9764b1d23ab800935cf45381117..61a921cc2b8b480307e3729131891562eb9a2940 100644 --- a/osc.spec +++ b/osc.spec @@ -1,10 +1,11 @@ Name: osc Version: 0.177.0 -Release: 1 +Release: 2 Summary: The Command Line Interface to work with an Open Build Service License: GPLv2+ Url: https://github.com/openSUSE/osc Source: https://github.com/openSUSE/osc/archive/refs/tags/%{version}.tar.gz +Patch0: CVE-2024-22034.patch BuildArch: noarch BuildRequires: python3-devel python3-distro python3-rpm python3-progressbar2 python3-setuptools diffstat Requires: python3-distro python3-rpm python3-m2crypto python3-lxml python3-progressbar2 vim @@ -73,6 +74,9 @@ EOF %{_mandir}/man1/osc.* %changelog +* Tue Sep 10 2024 wangkai <13474090681@163.com> - 0.177.0-2 +- Fix CVE-2024-22034 + * Tue May 10 2022 Wei, Qiang - 0.177.0-1 - Upgrade to 0.177.0