From 9788d563aaca26eee6997d916e8f5be8880e03e3 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 4 Aug 2026 04:24:30 +0200 Subject: [PATCH] Apply chown before chmod in syncfileclient permission handling chown() clears the setuid bit of a file on Linux (and its setgid bit, if the file is group-executable), even when run by root and even when the owner/group are unchanged. Since the owner/group chown ran after the permissions chmod, any syncfiles entry combining owner=/group= with a setuid/setgid permissions= value silently lost the special bits. --- .../common/profile/scripts/syncfileclient | 10 ++++++++-- .../profiles/default/scripts/syncfileclient | 8 +++++++- .../el7/profiles/default/scripts/syncfileclient | 8 +++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/confluent_osdeploy/common/profile/scripts/syncfileclient b/confluent_osdeploy/common/profile/scripts/syncfileclient index 878dc36e..99687df2 100644 --- a/confluent_osdeploy/common/profile/scripts/syncfileclient +++ b/confluent_osdeploy/common/profile/scripts/syncfileclient @@ -301,6 +301,7 @@ def synchronize(): for fname in opts: uid = -1 gid = -1 + perms = None for opt in opts[fname]: if opt == 'owner': try: @@ -309,16 +310,21 @@ def synchronize(): try: uid = opts[fname][opt]['id'] except KeyError: - raise Exception(f"Unable to map owner of {fname}") + raise Exception(f"Unable to map owner of {fname}") elif opt == 'group': try: gid = grp.getgrnam(opts[fname][opt]['name']).gr_gid except KeyError: gid = opts[fname][opt]['id'] elif opt == 'permissions': - os.chmod(fname, int(opts[fname][opt], 8)) + perms = int(opts[fname][opt], 8) if uid != -1 or gid != -1: + # chown clears setuid (and setgid on a group-executable + # file), even for root, so it must happen before the + # chmod that applies them os.chown(fname, uid, gid) + if perms is not None: + os.chmod(fname, perms) return status finally: shutil.rmtree(tmpdir) diff --git a/confluent_osdeploy/el7-diskless/profiles/default/scripts/syncfileclient b/confluent_osdeploy/el7-diskless/profiles/default/scripts/syncfileclient index 37c7bed1..69283a13 100644 --- a/confluent_osdeploy/el7-diskless/profiles/default/scripts/syncfileclient +++ b/confluent_osdeploy/el7-diskless/profiles/default/scripts/syncfileclient @@ -287,6 +287,7 @@ def synchronize(): for fname in opts: uid = -1 gid = -1 + perms = None for opt in opts[fname]: if opt == 'owner': try: @@ -299,9 +300,14 @@ def synchronize(): except KeyError: gid = opts[fname][opt]['id'] elif opt == 'permissions': - os.chmod(fname, int(opts[fname][opt], 8)) + perms = int(opts[fname][opt], 8) if uid != -1 or gid != -1: + # chown clears setuid (and setgid on a group-executable + # file), even for root, so it must happen before the + # chmod that applies them os.chown(fname, uid, gid) + if perms is not None: + os.chmod(fname, perms) finally: shutil.rmtree(tmpdir) shutil.rmtree(appendoncedir) diff --git a/confluent_osdeploy/el7/profiles/default/scripts/syncfileclient b/confluent_osdeploy/el7/profiles/default/scripts/syncfileclient index 17565570..c17cf52e 100644 --- a/confluent_osdeploy/el7/profiles/default/scripts/syncfileclient +++ b/confluent_osdeploy/el7/profiles/default/scripts/syncfileclient @@ -287,6 +287,7 @@ def synchronize(): for fname in opts: uid = -1 gid = -1 + perms = None for opt in opts[fname]: if opt == 'owner': try: @@ -299,9 +300,14 @@ def synchronize(): except KeyError: gid = opts[fname][opt]['id'] elif opt == 'permissions': - os.chmod(fname, int(opts[fname][opt], 8)) + perms = int(opts[fname][opt], 8) if uid != -1 or gid != -1: + # chown clears setuid (and setgid on a group-executable + # file), even for root, so it must happen before the + # chmod that applies them os.chown(fname, uid, gid) + if perms is not None: + os.chmod(fname, perms) finally: shutil.rmtree(tmpdir) shutil.rmtree(appendoncedir)