Skip to content

a file opened for writing could be written and not described - #27

Merged
Sunrisepeak merged 1 commit into
mainfrom
a-write-handle-cannot-answer-fstat
Sep 21, 2026
Merged

Sunrisepeak merged 1 commit into
mainfrom
a-write-handle-cannot-answer-fstat

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

kal_fs_file_info answers through NtQueryInformationFile with
FileBasicInformation, which requires FILE_READ_ATTRIBUTES on the handle.
FILE_GENERIC_WRITE does not carry it:

/* winnt.h:5919 */
#define FILE_GENERIC_WRITE (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA \
                            | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA \
                            | FILE_APPEND_DATA | SYNCHRONIZE)

So a file opened with KAL_OPEN_WRITE and nothing else was granted every
right except the one needed to ask it about itself.

How it surfaces

Through musl, as a descriptor refusing its own fstat a line after it was
opened — measured on a Windows runner:

open('D:/a/.../opened-here', O_WRONLY|O_CREAT|O_TRUNC) -> fd=3 errno=0
fstat(3)                                               -> rc=-1 errno=13

libarchive does exactly that pair when opening an archive for output
(archive_write_open_filename.c:178 and :189) and reports

Couldn't stat '<path>' (errno 13)

a message that names the verb and interpolates the argument.

Four refuted hypotheses, all about the argument

Recorded because the sequence is what makes the diagnosis credible, and
because the message is built to send a reader to the wrong half:

  1. The path lies beneath no preopened directory — EACCES from a capability
    layer ordinarily means that. Refuted by printing the preopen list: the
    program held C: outright.
  2. The two temp-directory helpers disagree about the 8.3 short form
    refuted by the next run returning an identical path and failure.
  3. The 8.3 component fails to resolve — refuted by a path under the checkout
    with no short component failing identically.
  4. The errno for a missing name is wrong — refuted by asking directly: the
    runner answers ENOENT, as Linux and Wine do.

By the time the failure happens the path has already succeeded. Reading
archive_write_open_filename.c ended it in two lines.

The change

FILE_READ_ATTRIBUTES is requested always. It is the minimal right for the
question — metadata and no data — and kal_fs_info already asked for it by
itself, so this file knew the requirement in one place and not the other. The
no-flags case keeps its previous meaning explicitly rather than by falling out
of access == 0.

The criterion

tests/write_handle_answers_info.cpp opens a file write-only and asks it for
its own information.

Its header states that Wine does not enforce the access check and answers
rc=0 either way. That is written down because "it passes under Wine" was
offered as evidence three times while this was open, and an environment that
cannot produce the failing condition is not a second opinion about it. The
test therefore reports on Windows and is a guard elsewhere.

`kal_fs_file_info` answers through NtQueryInformationFile with
FileBasicInformation, which requires FILE_READ_ATTRIBUTES on the handle.
`FILE_GENERIC_WRITE` does not carry it — winnt.h defines it as
STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES |
FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE — so a file opened with
KAL_OPEN_WRITE and nothing else was granted every right except the one needed
to ask it about itself.

Through musl that surfaces as a descriptor refusing its own `fstat` a line
after it was opened:

    open(..., O_WRONLY|O_CREAT|O_TRUNC)  ->  fd=3
    fstat(3)                             ->  -1, EACCES

measured on a Windows runner. libarchive does exactly that pair when opening
an archive for output (archive_write_open_filename.c:178 and :189), and
reports `Couldn't stat '<path>'` — a message that names the verb and
interpolates the argument. Four separate investigations went to the argument:
whether the path lay outside the program's preopened directories, whether two
temp-directory helpers disagreed about its 8.3 short form, whether the 8.3
component failed to resolve, and whether the errno for a missing name was
wrong. Each was refuted, and the path had already succeeded by the time the
failure happened.

FILE_READ_ATTRIBUTES is now requested always. It is the minimal right for the
question — metadata and no data — and `kal_fs_info` already asked for it by
itself, so this file knew the requirement in one place and not the other. The
no-flags case keeps its previous meaning explicitly rather than by falling out
of `access == 0`.

The criterion is `tests/write_handle_answers_info.cpp`. It states in its
header that Wine does not enforce the access check and answers rc=0 either
way, because "it passes under Wine" was offered as evidence three times while
this was open, and an environment that cannot produce the failing condition is
not a second opinion about it.
@Sunrisepeak
Sunrisepeak merged commit b2b1751 into main Sep 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant