Skip to content

fix: fsync cache writes and tolerate corrupt network-config.json - #7080

Open
pujitha24 wants to merge 1 commit into
canonical:mainfrom
pujitha24:auto/issue-7078
Open

fix: fsync cache writes and tolerate corrupt network-config.json#7080
pujitha24 wants to merge 1 commit into
canonical:mainfrom
pujitha24:auto/issue-7078

Conversation

@pujitha24

Copy link
Copy Markdown

Proposed Commit Message

fix: fsync cache writes and tolerate corrupt network-config.json

atomic_helper._write_file() wrote a tempfile and renamed it into
place without flushing or fsyncing first. On filesystems with
delayed allocation (e.g. ext4), a sudden crash or power loss right
after cloud-init writes an instance cache file could leave the
renamed file present but empty (0 bytes), since the rename's
directory metadata can reach the journal before the file's data is
written back from the page cache.

On the next boot, Init._write_network_config_json() read that
cached network-config.json with no exception handling around the
JSON parse, so a 0-byte or otherwise corrupt cache raised an
unhandled JSONDecodeError/TypeError and cloud-init failed to
initialize networking.

This adds tf.flush() + os.fsync() (best-effort; a failing fsync is
not treated as fatal, matching this function's existing handling of
other non-critical OSErrors) before the rename in
atomic_helper._write_file(), and wraps the network-config.json read
in stages.py in a try/except for JSONDecodeError, TypeError, and
ValueError, logging a warning and rewriting the cache instead of
crashing. The except tuple mirrors the same
util.load_json(util.load_text_file(...)) pattern already used
elsewhere in stages.py.

Validation: added tests/unittests/test_atomic_helper.py cases
asserting os.fsync() is called before rename and that write_file()
still succeeds if fsync() raises OSError, and a parametrized
tests/unittests/test_stages.py case covering an empty cache file,
truncated JSON, and syntactically-valid-but-wrong-type JSON (e.g.
"null"). All three stages.py cases were confirmed to fail with an
unhandled exception on the pre-fix code and pass after the fix.
Ran `tox -e py3`-equivalent (pytest) across tests/unittests/: no
new failures versus main; the only failures present (5, in
test_all_stages.py, test_azure.py, and test_dump.py) are
macOS-specific environment issues (AF_UNIX path length, a
passlib/crypt platform quirk, and GNU-date detection) that
reproduce identically on an unmodified checkout and are unrelated
to this change. Also ran ruff, black --check, isort --check-only,
pylint, and mypy against the changed files with no findings. This
was not validated against an actual crash/power-loss on real
hardware, since that cannot be reproduced in this environment; the
fix is a standard flush+fsync-before-rename durability pattern and
the JSON-handling fix is proven by the failing-then-passing tests
above.

Additional Context

Report: #7078

Test Steps

  1. tox -e py3 (or pytest tests/unittests/test_atomic_helper.py tests/unittests/test_stages.py -v)
  2. To reproduce the original crash locally: write an empty string to an
    instance's network-config.json cache
    (/var/lib/cloud/instance/network-config.json), then call
    Init()._write_network_config_json(some_netcfg_dict) — before this fix,
    this raises an unhandled JSONDecodeError; after the fix it logs a
    warning and rewrites the cache.

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

Fixes #7078

atomic_helper._write_file() wrote a tempfile and renamed it into place
without flushing or fsyncing first. On filesystems with delayed
allocation (e.g. ext4), a sudden crash or power loss right after
cloud-init writes an instance cache file could leave the renamed file
present but empty (0 bytes), since the rename's directory metadata can
reach the journal before the file's data is written back from the
page cache.

On the next boot, Init._write_network_config_json() read that cached
network-config.json with no exception handling around the JSON parse,
so a 0-byte or otherwise corrupt cache raised an unhandled
JSONDecodeError/TypeError and cloud-init failed to initialize
networking.

This adds tf.flush() + os.fsync() (best-effort; a failing fsync is
not treated as fatal, matching this function's existing handling of
other non-critical OSErrors) before the rename in
atomic_helper._write_file(), and wraps the network-config.json read
in stages.py in a try/except for JSONDecodeError, TypeError, and
ValueError, logging a warning and rewriting the cache instead of
crashing. The except tuple mirrors the same
util.load_json(util.load_text_file(...)) pattern already used
elsewhere in stages.py.

Validation: added tests/unittests/test_atomic_helper.py cases
asserting os.fsync() is called before rename and that write_file()
still succeeds if fsync() raises OSError, and a parametrized
tests/unittests/test_stages.py case covering an empty cache file,
truncated JSON, and syntactically-valid-but-wrong-type JSON (e.g.
"null"). All three stages.py cases were confirmed to fail with an
unhandled exception on the pre-fix code and pass after the fix. Ran
pytest across tests/unittests/ (tox's py3 target): no new failures
versus main; the only failures present (5, in test_all_stages.py,
test_azure.py, and test_dump.py) are macOS-specific environment
issues (AF_UNIX path length, a passlib/crypt platform quirk, and
GNU-date detection) that reproduce identically on an unmodified
checkout and are unrelated to this change. Also ran ruff, black
--check, isort --check-only, pylint, and mypy against the changed
files with no findings. This was not validated against an actual
crash/power-loss on real hardware, since that cannot be reproduced in
this environment; the fix is a standard flush+fsync-before-rename
durability pattern and the JSON-handling fix is proven by the
failing-then-passing tests above.

Report: canonical#7078
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Assisted-by: claude-sonnet-5 (via Claude Code)
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.

atomic_helper.write_file missing os.fsync() leaves 0-byte files on sudden reboot/power-loss, causing fatal JSONDecodeError in stages.py

1 participant