protonscr

latest experimental proton fails on ecryptfs due to copy_file_range

protonclosed
ValveSoftware/Proton#5769 · opened 2022-04-15 by kelvie · updated 2022-11-23 · 14 comments · github
1 matching comments, n / p to jump
Kkelvie 2022-04-15 github

This uses Proton Experimental (I tried both the default and bleeding-edge). It seems the new code that tries to save space by making links to the various files in the prefix is broken on my system (possibly because I use btrfs?)

When I try to start up a game using proton experimental (this example uses Elden Ring, but it happens in other games, like Rift Wizard, when I choose to use experimental proton).

Traceback (most recent call last):
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 1463, in <module>
    g_session.init_session(sys.argv[1] != "runinprefix")
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 1265, in init_session
    g_compatdata.setup_prefix()
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 858, in setup_prefix
    self.update_builtin_libs(builtin_dll_copy)
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 703, in update_builtin_libs
    self.pfx_copy(src_file, dst_file, dll_copy)
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 645, in pfx_copy
    try_copyfile(src, dst)
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 247, in try_copyfile
    copyfile(src, dst)
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 227, in copyfile_reflink
    raise e
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 224, in copyfile_reflink
    bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy)
  File "/home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/proton", line 212, in copy_file_range_ctypes
    raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))
OSError: [Errno 0] unknown

Looking at the code, it seems the new copy_file_range_ctypes is failing. I put a print statement to check which file is failing:

def copyfile_reflink(srcname, dstname):
    "Copy srcname to dstname, making reflink if possible"
    global copyfile
    with open(srcname, 'rb', buffering=0) as src:
        bytes_to_copy = os.fstat(src.fileno()).st_size
        try:
            with open(dstname, 'wb', buffering=0) as dst:
                while bytes_to_copy > 0:
                    bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy)
        except OSError as e:
            print(f'Error copying {srcname} to {dstname}')
            if e.errno != errno.EXDEV and e.errno != errno.ENOSYS:
                raise e
            if e.errno == errno.ENOSYS:
                copyfile = shutil.copyfile
            shutil.copyfile(srcname, dstname)

And got:

Error copying /home/kelvie/.local/share/Steam/steamapps/common/Proton - Experimental/files/share/default_pfx/drive_c/windows/system/ddeml.dll to /home/kelvie/.local/share/Steam/steamapps/compatdata/1271280/pfx/drive_c/windows/system/ddeml.dll

When I check the destination file, it becomes a 0-byte empty file:

~ file /home/kelvie/.local/share/Steam/steamapps/compatdata/1271280/pfx/drive_c/windows/system/ddeml.dll                                 0 [13:18:37]
/home/kelvie/.local/share/Steam/steamapps/compatdata/1271280/pfx/drive_c/windows/system/ddeml.dll: empty

Printing the code, it seems that copy_file_range returns -1, but doesn't set an errno, but also creates an empty file at the destination, so the latest proton doesn't seem to work on ecryptfs. Perhaps proton can detect whether or not copy_file_range is supported on the destination before trying? or have a more graceful fallback?

Mmadewokherd 2022-04-17 github

Thanks for reporting this.

I think this may have been a mistake:

        __syscall__copy_file_range = prototype(('syscall', CDLL(None)))

CDLL provides a use_errno argument, so it probably should've been:

        __syscall__copy_file_range = prototype(('syscall', CDLL(None, use_errno=True)))

Hopefully that'll allow us to get a real errno instead of 0.

If it's EINVAL, as that boostorg issue implies, we should probably treat that the same way as EXDEV.

Kkelvie 2022-04-17 github

I just did a quick test of this syscall, and it does seem to return EINVAL on ecryptfs -- strangely on my system (python 3.9.7) when I run proton manually (do you guys use a virtualenv of some kind)? I get the proper errno without having to set use_errno:

OSError: [Errno 22] Invalid argument

Copying from ecryptfs to tmpfs or btrfs works just fine.

Kkelvie 2022-04-17 github

Ah nevermind, on my python version this uses copy_file_range, not copy_file_range_ctypes. I forced it to use copy_file_range_ctypes, and put some print statements:

ret = -1 get_errno() = 0
Traceback (most recent call last):
  File "/home/kelvie/Temporary/./proton", line 1448, in <module>
    copyfile_reflink(sys.argv[1], sys.argv[2])
  File "/home/kelvie/Temporary/./proton", line 228, in copyfile_reflink
    raise e
  File "/home/kelvie/Temporary/./proton", line 225, in copyfile_reflink
    bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy)
  File "/home/kelvie/Temporary/./proton", line 213, in copy_file_range_ctypes
    raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))
OSError: [Errno 0] unknown
def copy_file_range_ctypes(fd_in, fd_out, count):
    "Copy data using the copy_file_range syscall through ctypes, assuming x86_64 Linux"
    global __syscall__copy_file_range
    __NR_copy_file_range = 326

    if __syscall__copy_file_range is None:
        c_int64_p = POINTER(c_int64)
        prototype = CFUNCTYPE(c_ssize_t, c_long, c_int, c_int64_p,
            c_int, c_int64_p, c_size_t, c_uint)
        __syscall__copy_file_range = prototype(('syscall', CDLL(None, use_errno=True)))

    while True:
        ret = __syscall__copy_file_range(__NR_copy_file_range, fd_in, None, fd_out, None, count, 0)
        print(f'{ret = } {get_errno() = }')
        if ret >= 0 or get_errno() != errno.EINTR:
            break

    if ret < 0:
        raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))

    return ret
Kkelvie 2022-04-17 github

Note that I've since moved my steam .local outside of ecryptfs, so I haven't been testing from steam.

Mmadewokherd 2022-04-17 github

OK, so that means we should not be getting an errno of 0, and use_errno didn't fix it.

Mmadewokherd 2022-04-17 github

Maybe use_errno needs to also be passed to CFUNCTYPE.

Kkelvie 2022-04-17 github

Bingo:

ret = -1 get_errno() = 22
Traceback (most recent call last):
  File "/home/kelvie/Temporary/./proton", line 1448, in <module>
    copyfile_reflink(sys.argv[1], sys.argv[2])
  File "/home/kelvie/Temporary/./proton", line 228, in copyfile_reflink
    raise e
  File "/home/kelvie/Temporary/./proton", line 225, in copyfile_reflink
    bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy)
  File "/home/kelvie/Temporary/./proton", line 213, in copy_file_range_ctypes
    raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))
OSError: [Errno 22] EINVAL

I set it on both:

def copy_file_range_ctypes(fd_in, fd_out, count):
    "Copy data using the copy_file_range syscall through ctypes, assuming x86_64 Linux"
    global __syscall__copy_file_range
    __NR_copy_file_range = 326

    if __syscall__copy_file_range is None:
        c_int64_p = POINTER(c_int64)
        prototype = CFUNCTYPE(c_ssize_t, c_long, c_int, c_int64_p,
                              c_int, c_int64_p, c_size_t, c_uint, use_errno=True)
        __syscall__copy_file_range = prototype(('syscall', CDLL(None, use_errno=True)))

    while True:
        ret = __syscall__copy_file_range(__NR_copy_file_range, fd_in, None, fd_out, None, count, 0)
        print(f'{ret = } {get_errno() = }')
        if ret >= 0 or get_errno() != errno.EINTR:
            break

    if ret < 0:
        raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))

    return ret
Kkelvie 2022-04-17 github

btw this also affects python's os.copy_file_range -- it also returns an EINVAL, so this'll have to be fixed in copyfile_reflink or even higher up the stack.

Mmadewokherd 2022-04-17 github

Thanks for checking that. I think the rest of the fix will be to change this check in copyfile_reflink:

            if e.errno != errno.EXDEV and e.errno != errno.ENOSYS:

We should just be able to handle EINVAL the same way:

            if e.errno not in (errno.EXDEV, errno.EINVAL, errno.ENOSYS):
Mmadewokherd 2022-04-18 github

I've pushed those changes, and they should appear shortly in bleeding-edge experimental.

Kkisak-valve maintainer 2022-11-23 github

Hello @kelvie, it looks like the investigation here has concluded. Are you still seeing this issue?

Kkelvie 2022-11-23 github

Hello, I'm pretty sure this is fixed, although I've since moved steam outside the encrypted fs, so I haven't tested it.

Kkisak-valve maintainer 2022-11-23 github

Thanks for the feedback. I think it's safe to close this issue report and if someone else comes across a similar issue, then they can open a new issue report.

Proton versions

DLLs