Skip to main content
Unofficial Guild Wars Reforged App — a fan project, not affiliated with ArenaNet or NCSoft

UR-06: File::Open mode 1 creates the file it tests for

Mode 1 is the open-an-existing-file mode, but it carries O_CREAT. The rename probe therefore creates the destination file and reports the name as taken.

File::Open mode 1 is supposed to open a file that exists and fail when it's absent. Today it carries O_CREAT, so it creates the file instead.

Build used for the measurements below: b0319704f3072d6948a66026a35af5eb0af12b48d70986783c293e7c77e98483.

Function 771, File::Open(path, mode, err) -> handle.

Fix: low effort, medium blast radius — the change is one flag in the mode table, but every mode 1 caller changes behaviour. Code that depends on the current create-on-open breaks; that's the correct outcome, but the mode 1 callers need a check. We identified two: function 9753 (template read) and function 9757 (rename probe).

Mode 1 is the open-an-existing-file mode. Function 9753 uses it to read a template, and function 9757 uses it to ask whether a rename destination is already taken:

c
if (!allowOverwrite) {
    handle = File::Open(path, 1, NULL);
    if (handle) return false;          // the name is already in use
}
handle = File::Open(path, 2, NULL);    // create and write

What the client does now

Both modes reach openat with identical flags. We captured a save and then a rename on a live client:

save    openat flags:32834 access:2 create:true errno:0 -> fd_pwrite 24/24 -> fd_close
rename  openat flags:32834 access:2 create:true errno:0 -> fd_close

The value 32834 is O_RDWR | O_CREAT | O_LARGEFILE.

Mode 1 therefore creates the very file it's testing for — the probe then sees that the file exists and reports the name as taken.

What the player sees

  • Every rename fails, for every destination name.
  • The client shows "The attempt to rename the Skills Template named … failed".
  • A file of zero length stays at the destination path.
  • A load of an absent template creates an empty file instead of a failure.

Expected behaviour

Mode 1 opens an existing file, without O_CREAT, and fails when the file is absent. Mode 2 keeps O_CREAT.

Proposed mode table

c
// File::Open(path, mode, err)
//
// mode 1  open existing    O_RDONLY  (or O_RDWR)      no O_CREAT
// mode 2  create and write O_RDWR | O_CREAT | O_TRUNC

We don't know the full mode table. We report modes 1 and 2 only, because those are the two the template paths use.

Check for a read-only mount
If a mode 1 open uses O_RDWR today, switching to O_RDONLY also changes behaviour on a read-only file. Either choice works with our acceptance criteria — just decide which one you want.

Acceptance criteria

  • File::Open(L"Templates/Skills/Absent.txt", 1, NULL) returns a null handle.
  • After that call, Templates/Skills/Absent.txt does not exist.
  • File::Open mode 2 on the same path creates the file, as it does today.
  • A rename to an unused name completes.
  • A rename to a used name fails, and the existing file stays unchanged.
  • A load of an absent template fails and creates no file.

How to reproduce

Prepare a template

Use a build in which the save path works. Save one template.

Rename it

Rename the template to any unused name. The rename fails.

Look at the directory

A file of zero length is now at the destination name.

Our workaround today

The host adds a forwarder for the probe call site only. It asks the host whether the file exists and calls the real File::Open only when it does; the write call site and the load path keep the real function. A corrected mode table retires the forwarder.

GWonMac

Play Guild Wars on your Mac. GWonMac runs ArenaNet's official client natively on Apple Silicon — free, open source, signed and notarized.

© GWonMac. All rights reserved.