Skip to content

save_pretrained (with register_for_auto_class`) propagates read-only permissions from custom-model source files #45684

@nurpax

Description

@nurpax

System Info

  • transformers version: 5.5.3
  • Python: 3.13
  • Platform: Linux

Who can help?

@Cyrilvallez (model loading)

Information

  • The official example scripts
  • My own modified scripts

Tasks

  • An officially supported task in the examples folder (such as GLUE/SQuAD, ...)
  • My own task or dataset (give details below)

Reproduction

When a model is registered via register_for_auto_class, save_pretrained copies the user's custom-model .py file(s) into the output folder using shutil.copy. Since shutil.copy is copyfile + copymode, the destination inherits the source file's permission bits. If the source is read-only -- a common state for files managed by Perforce, which checks files out as r--r--r-- until p4 edit -- the saved copy in the output dir is also read-only.

This:

  1. Breaks any post-save tooling that wants to rewrite the saved module file (in our real workflow we patch the saved file after save_pretrained returns; in the minimal repro below, the patching step fails with PermissionError).
  2. Leaves users with a saved-model directory full of read-only files that are surprising and awkward to operate on.

Repro:

Run:

$ chmod u-w custom_model.py
$ python main.py save --path=pretrained_c --magic="Magic C"
...
PermissionError: [Errno 13] Permission denied: 'pretrained_c/custom_model.py'

main.py
custom_model.py

Expected behavior

Files written into the save directory should be writable by the user (subject to the standard umask), independent of the source file's mode bits.

Root cause

transformers/dynamic_module_utils.py, in custom_object_save() (lines 646–659):

result = []
# Copy module file to the output folder.
object_file = sys.modules[obj.__module__].__file__
dest_file = Path(folder) / (Path(object_file).name)
shutil.copy(object_file, dest_file)
result.append(dest_file)

# Gather all relative imports recursively and make sure they are copied as well.
for needed_file in get_relative_import_files(object_file):
    dest_file = Path(folder) / (Path(needed_file).name)
    shutil.copy(needed_file, dest_file)
    result.append(dest_file)

return result

shutil.copy preserves permission bits. The same applies to the three call sites in get_cached_module_file (lines 423, 431, 445).

Suggested fix

Replace shutil.copy with shutil.copyfile at all five call sites in dynamic_module_utils.py. copyfile copies file contents only; the destination, when newly created, gets standard umask-based permissions, which is what callers of save_pretrained reasonably expect.

-    shutil.copy(object_file, dest_file)
+    shutil.copyfile(object_file, dest_file)

Workaround

This is what we've used to workaround the problem:

copymode_old = shutil.copymode
try:
    shutil.copymode = lambda *_args, **_kwargs: None
    model.save_pretrained(path)
finally:
    shutil.copymode = copymode_old

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions