{
 "number": 36210,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36210",
 "title": "streams: Include the OS error when AutoFile I/O fails",
 "author": "pablomartin4btc",
 "author_association": "MEMBER",
 "created_at": "2026-09-09T20:08:10Z",
 "updated_at": "2026-09-10T04:28:30Z",
 "age_days": 7,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "9e939b7207651b7dc4aecfc361e40faa6dd5f906",
 "head_ref": "streams/autofile-io-error-detail",
 "head_repo": "pablomartin4btc/bitcoin",
 "head_history": [
  {
   "t": "2026-09-10T03:13:37Z",
   "sha": "9e939b7207651b7dc4aecfc361e40faa6dd5f906"
  }
 ],
 "additions": 110,
 "deletions": 5,
 "changed_files": 3,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": []
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-10T04:28:30Z",
  "last_reviewer_activity": null,
  "last_reviewer": null,
  "author_silent_days": 7,
  "waiting_on_author_days": 0,
  "days_since_update": 7
 },
 "refs": {
  "mentioned": [
   35492
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35492,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: fail dump on incomplete writes"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "This came up during review of #35492 (wallet: fail dump on incomplete writes), which switches `DumpWallet` from `std::ofstream` to `AutoFile` so a failed `write`/`close`/`fsync` properly aborts the dump. While reviewing that switch, I noticed the underlying `AutoFile`/`BufferedFile` exception messages themselves don't carry the actual reason a `fread()`/`fwrite()` call failed \u2014 only a fixed string like \"AutoFile::write: write failed\" \u2014 the errno that would explain why the call failed is discarded. Any caller that surfaces `e.what()` to a log or an error message (including the dump path in #35492, once that's wired up) loses that detail.\n\nThis PR captures errno right after the failing call and folds SysErrorString() into the thrown message for:\n- `AutoFile::write()` / `write_buffer()`\n- `AutoFile::read()` (via `detail_fread`())\n- `AutoFile::ignore()`\n- `BufferedFile::Fill()`\n\n`AutoFile::seek()` is intentionally left unchanged \u2014 forcing a real, portable `fseek()` failure (as opposed to a null handle or EOF) across this project's CI platforms isn't practical, so it has no equivalent test coverage.\n\nTwo commits: the first adds `streams_tests.cpp` coverage that asserts the current behavior \u2014 the OS error is discarded on a genuine (non-EOF) I/O failure \u2014 with a TODO marking the assertion that gets flipped; the second is the fix itself, flipping that same assertion to confirm the OS error is now present.\n\nOut of scope for this PR: wiring `DumpWallet` in #35492 to actually surface this detail in its own error message \u2014 that PR's `dump.cpp` doesn't exist in its AutoFile-based form on master yet, so that wiring has to wait for whichever of the two merges first.",
 "commits": [
  {
   "sha": "2b3a3e2123aa6016a23ede4bd72e1b0ffbd7e9fa",
   "date": "2026-09-10T03:00:03Z",
   "message": "test: AutoFile/BufferedFile I/O failures discard the OS error\n\nAutoFile::write(), write_buffer(), read() (via detail_fread), ignore(),\nand BufferedFile::Fill() all throw a fixed, generic message when the\nunderlying fread()/fwrite() fails for a real OS reason (as opposed to\nreaching end-of-file), discarding the errno that explains why.\n\nForce such a failure with a genuine kernel-level I/O error rather than\nrelying on a libc-internal check: on POSIX, open the file in the\ncorrect direction and close its underlying fd out from under the\nFILE*, so the next read/write hits a real EBADF from the kernel; musl's\nown __towrite()/__toread() never touch errno for a same-direction\nmismatch, unlike glibc's, so a libc-internal check isn't reliable here.\nOn Windows, opening in the wrong direction already produces a genuine,\nerrno-bearing failure.\n\nTODO: flip check_io_failure() to assert the OS error IS present once\nAutoFile/BufferedFile are fixed to include it."
  },
  {
   "sha": "9e939b7207651b7dc4aecfc361e40faa6dd5f906",
   "date": "2026-09-10T03:01:20Z",
   "message": "streams: include the OS error when AutoFile I/O fails\n\nAutoFile::write(), write_buffer(), read() (via detail_fread), ignore(),\nand BufferedFile::Fill() throw a fixed message on a genuine fread()/\nfwrite() failure (not end-of-file), discarding the errno that explains\nwhy the call actually failed. Callers surfacing e.what() to logs or\nusers lose that detail.\n\nCapture errno right after the failing call and fold SysErrorString()\ninto the thrown message, consistent with the rest of the codebase's\nerror reporting (lint-locale-dependence.py restricts raw strerror use\nto SysErrorString's own implementation).\n\nFlips check_io_failure()'s assertion from the previous commit to\nconfirm the OS error is now present.\n\nAutoFile::seek() is intentionally left unchanged: forcing a real,\nportable fseek() failure (as opposed to a null handle or EOF) across\nthis project's CI platforms isn't practical, so it has no equivalent\ntest coverage."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-10T03:00:31Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "Fixing the Alpine (musl) CI [failure](https://github.com/bitcoin/bitcoin/actions/runs/34399219812/job/102626611981?pr=36210). Root cause: the test forced a \"genuine\" I/O failure by opening the file in the wrong direction (write to a read-only-opened stream, etc.), which works on every platform in CI except musl. Confirmed from musl's own source (`__towrite.c`/`__toread.c`): when it detects a direction mismatch, it sets an internal error flag and returns immediately, without ever touching `errno` \u2014 it never reaches the actual `write(2)`/`read(2)` syscall. glibc happens to set `errno` in that same internal check as a courtesy, which is why this worked everywhere else; musl isn't required to and doesn't.\n\nFix: force a genuine kernel-level failure instead of relying on that libc-internal precheck. On POSIX, the file is now opened in the correct direction and its underlying fd is closed out from under the `FILE*` before the read/write attempt \u2014 since the direction is correct, musl's precheck never triggers, so the call reaches the real syscall, which the kernel fails with `EBADF` and sets `errno` for, reliably, on every libc. Windows keeps the original wrong-direction technique, since CI already confirmed that produces a genuine, errno-bearing failure there."
  },
  {
   "t": "2026-09-10T03:13:37Z",
   "kind": "force_push",
   "who": "pablomartin4btc",
   "commit": "9e939b7207651b7dc4aecfc361e40faa6dd5f906"
  },
  {
   "t": "2026-09-10T04:28:30Z",
   "kind": "comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "text": "-<ins>_**Updates**_</ins>:\n- Fixed Alpine (musl) CI failure as described [above](https://github.com/bitcoin/bitcoin/pull/36210#issuecomment-5611987438)."
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-09T21:20:22Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-10T04:19:51Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-10T03:00:39Z",
   "kind": "convert_to_draft",
   "who": "pablomartin4btc"
  },
  {
   "t": "2026-09-10T04:28:27Z",
   "kind": "ready_for_review",
   "who": "pablomartin4btc"
  }
 ],
 "text_chars": 5051,
 "text_tokens_estimate": 1262,
 "changed_paths": [
  "src/streams.cpp",
  "src/streams.h",
  "src/test/streams_tests.cpp"
 ],
 "files": [
  {
   "path": "src/streams.cpp",
   "add": 9,
   "del": 4
  },
  {
   "path": "src/streams.h",
   "add": 3,
   "del": 1
  },
  {
   "path": "src/test/streams_tests.cpp",
   "add": 98,
   "del": 0
  }
 ],
 "test_lines": 98,
 "git": {
  "head": "9e939b7207651b7dc4aecfc361e40faa6dd5f906",
  "head_matches_backup": true,
  "base": "7e75b3b922f9ba740240c2204e3e9389524eafdb",
  "commits": [
   {
    "sha": "2b3a3e2123",
    "subject": "test: AutoFile/BufferedFile I/O failures discard the OS error",
    "files": 1,
    "add": 101,
    "del": 0
   },
   {
    "sha": "9e939b7207",
    "subject": "streams: include the OS error when AutoFile I/O fails",
    "files": 3,
    "add": 14,
    "del": 10
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "30b2ec6e66ebe6bc",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}