[PATCH] use TemporaryDirectory.cleanup() to avoid PermissionError

19 views
Skip to first unread message

Akihiro Yamazaki

unread,
Mar 8, 2026, 6:55:15 AMMar 8
to kas-...@googlegroups.com, Akihiro Yamazaki
rmtree() fails if there is a directory without write permission,
even if you are the owner. This can be avoided by using
TemporaryDirectory.cleanup().

However, there are still cases where it may fail, so in the
future it may be better to specify ignore_cleanup_errors=True
(requires Python 3.10 or later).
---
kas/libcmds.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/kas/libcmds.py b/kas/libcmds.py
index 3a651bf..bfe320c 100644
--- a/kas/libcmds.py
+++ b/kas/libcmds.py
@@ -191,11 +191,12 @@ class SetupHome(Command):

def __init__(self):
super().__init__()
+ self.tmpdir = None
self.tmpdirname = None

def __del__(self):
- if self.tmpdirname:
- shutil.rmtree(self.tmpdirname)
+ if self.tmpdir:
+ self.tmpdir.cleanup()

def __str__(self):
return 'setup_home'
@@ -382,8 +383,9 @@ class SetupHome(Command):
managed_env = get_context().managed_env
if managed_env:
logging.info(f'Running on {managed_env}')
- if not self.tmpdirname:
- self.tmpdirname = tempfile.mkdtemp()
+ if not self.tmpdir:
+ self.tmpdir = tempfile.TemporaryDirectory()
+ self.tmpdirname = self.tmpdir.name
def_umask = os.umask(0o077)
self._setup_netrc()
self._setup_npmrc()
--
2.53.0

Akihiro Yamazaki

unread,
Mar 8, 2026, 7:34:32 AMMar 8
to kas-...@googlegroups.com

> 2026/03/08 19:55、Akihiro Yamazaki <aki...@tinkermode.com>:
>
> rmtree() fails if there is a directory without write permission,
> even if you are the owner. This can be avoided by using
> TemporaryDirectory.cleanup().
>
> However, there are still cases where it may fail, so in the
> future it may be better to specify ignore_cleanup_errors=True
> (requires Python 3.10 or later).

We use kas to develop projects that include Go code.

An error occurs when exiting kas shell. It can be reproduced with the following steps:
```
kas shell kas-project.yml

# inside the kas shell:
go install github.com/spf13/cobra-cli@latest <http://github.com/spf13/cobra-cli@latest>
exit
(snip)
Exception ignored in: <function SetupHome.__del__ at 0x74f6c6254040>
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/kas/libcmds.py", line 165, in __del__
shutil.rmtree(self.tmpdirname)
(snip)
PermissionError: [Errno 13] Permission denied: ‘go.mod'
```

This happens because `go install` creates read-only directories/files under the HOME directory, which causes rmtree() to fail during cleanup.

As a result, several hundred megabytes of data remain in /tmp after each kas execution, gradually consuming disk space.

Thanks,

--
Yamazaki Akihiro
aki...@tinkermode.com
MODE, Inc.

Jan Kiszka

unread,
Mar 8, 2026, 1:56:18 PMMar 8
to Akihiro Yamazaki, kas-...@googlegroups.com
On 08.03.26 11:55, Akihiro Yamazaki wrote:
> rmtree() fails if there is a directory without write permission,
> even if you are the owner. This can be avoided by using
> TemporaryDirectory.cleanup().
>
> However, there are still cases where it may fail, so in the
> future it may be better to specify ignore_cleanup_errors=True
> (requires Python 3.10 or later).

Can you provide some examples for those remaining cases? And if we do
that, we would also leave some files and dirs behind, just less, right?

Missing signed-off here. Please see CONTRIBUTING.md for its meaning.
How about a global conversion of self.tmpdirname -> self.tmpdir.name?
Would allow us to drop this extra variable.

> def_umask = os.umask(0o077)
> self._setup_netrc()
> self._setup_npmrc()

Thanks for addressing this!

Jan

--
Siemens AG, Foundational Technologies
Linux Expert Center

Jan Kiszka

unread,
Mar 8, 2026, 1:58:01 PMMar 8
to Akihiro Yamazaki, kas-...@googlegroups.com
Ah, here is the example of the current case. Please enrich the commit
message with it, at least with a summary.

But the case above is fully addressed by your patch, right?

Akihiro Yamazaki

unread,
Mar 9, 2026, 11:43:17 AMMar 9
to kas-...@googlegroups.com, jan.k...@siemens.com, Akihiro Yamazaki
shutil.rmtree() can fail when removing directories that contain
non-writable files or directories, even if they are owned by the
current user. Using TemporaryDirectory.cleanup() avoids this issue.

This problem can be reproduced with:

kas shell kas-project.yml -c 'go install github.com/spf13/cobra-cli@latest'

When kas exits, cleanup of the temporary HOME directory may fail with
PermissionError:

Exception ignored in: <function SetupHome.__del__ at 0x74f6c6254040>
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/kas/libcmds.py", line 165, in __del__
shutil.rmtree(self.tmpdirname)
(snip)
PermissionError: [Errno 13] Permission denied: ‘go.mod'

This happens because `go install` creates read-only directories and
files under the HOME directory, which causes rmtree() to fail during
cleanup.

As a result, temporary data remains in /tmp after each kas execution,
potentially accumulating hundreds of megabytes over time.

Signed-off-by: Akihiro Yamazaki <aki...@tinkermode.com>
---
kas/libcmds.py | 32 ++++++++++++++++----------------
1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/kas/libcmds.py b/kas/libcmds.py
index 3a651bf..2c268fa 100644
--- a/kas/libcmds.py
+++ b/kas/libcmds.py
@@ -191,11 +191,11 @@ class SetupHome(Command):

def __init__(self):
super().__init__()
- self.tmpdirname = None
+ self.tmpdir = None

def __del__(self):
- if self.tmpdirname:
- shutil.rmtree(self.tmpdirname)
+ if self.tmpdir:
+ self.tmpdir.cleanup()

def __str__(self):
return 'setup_home'
@@ -232,10 +232,10 @@ class SetupHome(Command):
def _setup_netrc(self):
netrc_file = self._path_from_env('NETRC_FILE')
if netrc_file:
- shutil.copy(netrc_file, self.tmpdirname + "/.netrc")
+ shutil.copy(netrc_file, self.tmpdir.name + "/.netrc")
if os.environ.get('CI_SERVER_HOST', False) \
and os.environ.get('CI_JOB_TOKEN', False):
- with open(self.tmpdirname + '/.netrc', 'a') as fds:
+ with open(self.tmpdir.name + '/.netrc', 'a') as fds:
fds.write('machine ' + os.environ['CI_SERVER_HOST'] + '\n'
'login gitlab-ci-token\n'
'password ' + os.environ['CI_JOB_TOKEN'] + '\n')
@@ -244,22 +244,22 @@ class SetupHome(Command):
npmrc_file = self._path_from_env('NPMRC_FILE')
if not npmrc_file:
return
- shutil.copy(npmrc_file, self.tmpdirname + "/.npmrc")
+ shutil.copy(npmrc_file, self.tmpdir.name + "/.npmrc")

def _setup_registry_auth(self):
- os.makedirs(self.tmpdirname + "/.docker")
+ os.makedirs(self.tmpdir.name + "/.docker")
reg_auth_file = self._path_from_env('REGISTRY_AUTH_FILE')
if reg_auth_file:
shutil.copy(reg_auth_file,
- self.tmpdirname + "/.docker/config.json")
- elif not os.path.exists(self.tmpdirname + '/.docker/config.json'):
- with open(self.tmpdirname + '/.docker/config.json', 'w') as fds:
+ self.tmpdir.name + "/.docker/config.json")
+ elif not os.path.exists(self.tmpdir.name + '/.docker/config.json'):
+ with open(self.tmpdir.name + '/.docker/config.json', 'w') as fds:
fds.write("{}")

if os.environ.get('CI_REGISTRY', False) \
and os.environ.get('CI_JOB_TOKEN', False) \
and os.environ.get('CI_REGISTRY_USER', False):
- with open(self.tmpdirname + '/.docker/config.json', 'r+') as fds:
+ with open(self.tmpdir.name + '/.docker/config.json', 'r+') as fds:
data = json.loads(fds.read())
token = os.environ['CI_JOB_TOKEN']
base64_token = base64.b64encode(token.encode()).decode()
@@ -272,7 +272,7 @@ class SetupHome(Command):
fds.truncate()

def _setup_aws_creds(self):
- aws_dir = self.tmpdirname + "/.aws"
+ aws_dir = self.tmpdir.name + "/.aws"
conf_file = aws_dir + "/config"
shared_creds_file = aws_dir + "/credentials"
sso_cache_dir = aws_dir + "/sso/cache"
@@ -343,7 +343,7 @@ class SetupHome(Command):

def _setup_gitconfig(self):
gitconfig_host = self._path_from_env('GITCONFIG_FILE')
- gitconfig_kas = self.tmpdirname + '/.gitconfig'
+ gitconfig_kas = self.tmpdir.name + '/.gitconfig'

# when running in a externally managed environment,
# always try to read the gitconfig
@@ -382,8 +382,8 @@ class SetupHome(Command):
managed_env = get_context().managed_env
if managed_env:
logging.info(f'Running on {managed_env}')
- if not self.tmpdirname:
- self.tmpdirname = tempfile.mkdtemp()
+ if not self.tmpdir:
+ self.tmpdir = tempfile.TemporaryDirectory()
def_umask = os.umask(0o077)
self._setup_netrc()
self._setup_npmrc()
@@ -392,7 +392,7 @@ class SetupHome(Command):
self._setup_aws_creds()
os.umask(def_umask)

- ctx.environ['HOME'] = self.tmpdirname
+ ctx.environ['HOME'] = self.tmpdir.name


class MakeNonInteractive(Command):

Akihiro Yamazaki

unread,
Mar 9, 2026, 11:56:52 AMMar 9
to Jan Kiszka, kas-...@googlegroups.com

Thanks for the review!
I updated the patch, but I made a mistake in the process, and it ended up in a separate thread. Sorry.

This is the message ID of the v2 patch:
Message-ID: <20260309154244....@tinkermode.com>

> 2026/03/09 2:56、Jan Kiszka <jan.k...@siemens.com>のメール:
>
> On 08.03.26 11:55, Akihiro Yamazaki wrote:
>> rmtree() fails if there is a directory without write permission,
>> even if you are the owner. This can be avoided by using
>> TemporaryDirectory.cleanup().
>>
>> However, there are still cases where it may fail, so in the
>> future it may be better to specify ignore_cleanup_errors=True
>> (requires Python 3.10 or later).
>
> Can you provide some examples for those remaining cases? And if we do
> that, we would also leave some files and dirs behind, just less, right?

On Linux, such remaining cases could involve things like `chattr +i somedir`
or the existence of a mount point. However, both require root privileges and
seem highly unlikely to occur in practice. Because of this, I found the original
comment not very helpful and have removed it.

Yes, in this case, if we use `ignore_cleanup_errors=True` we can delete more files,
but some files will continue to remain.

> Missing signed-off here. Please see CONTRIBUTING.md for its meaning.

Oops, sorry, I forgot it.
I was initially hesitant to replace self.tmpdirname due to the size of the diff,
but I have gone ahead and made the change to use self.tmpdir.name as you suggested,
as it looks clean.

>> def_umask = os.umask(0o077)
>> self._setup_netrc()
>> self._setup_npmrc()
>
> Thanks for addressing this!
>
> Jan
>

--

Akihiro Yamazaki

unread,
Mar 9, 2026, 11:58:25 AMMar 9
to Jan Kiszka, kas-...@googlegroups.com

> 2026/03/09 2:57、Jan Kiszka <jan.k...@siemens.com>のメール:
Yes, the case is fully addressed by this patch. I have also enriched the new commit message with a summary and the reproduction steps.

| v2 patch Message-ID: <20260309154244....@tinkermode.com>
Reply all
Reply to author
Forward
0 new messages