From 6467ca9764f89b4a887142ee18b05219d1cf5f8a Mon Sep 17 00:00:00 2001 From: Tao An <1250043+tao-hpu@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:02:42 +0800 Subject: [PATCH] fix: keep the conf intact when writing it fails (#7247) (#7278) * fix: keep the conf intact when writing it fails _setopt() and _clear_conf() redirected the new content straight into the conf file. The redirect truncates the file before anything is written, so a failed write (no space left on device, quota, I/O error) left a 0-byte conf, _save_conf still returned 0, and the cert could not be renewed any more even after space was freed. Write the new content to a temp file next to the conf, verify it, then rename it over the conf. The temp file is created with cp -p so the mode and owner are kept. A symlinked or bind mounted conf cannot be renamed over, so it is written in place once the temp copy is known to be good. A failed write now returns 1 with an error and the conf untouched. Fixes #7247 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RL9G1DHE5yNVtkm3FFPPNX * fix: route the append through _write_conf and keep the temp file on a failed in-place write The append branch of _setopt() still wrote into the conf with >>, so a write cut short (full disk, quota) left a half written line while _setopt returned 0, and sourcing the conf failed afterwards. Build the new content in all three branches and write it once through _write_conf. This also drops the separate trailing newline append. In the symlink / bind mount fallback, a failed in-place write leaves the conf truncated. Keep the temp file in that case, since it is the only complete copy, and name it in the error. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01G7ohY7h4RUSNJ2ES14NMmY --------- Co-authored-by: Claude Fable 5.1 --- acme.sh | 75 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/acme.sh b/acme.sh index 1f2454b4..4bb5acb2 100755 --- a/acme.sh +++ b/acme.sh @@ -2800,6 +2800,35 @@ _sed_escape_rhs() { sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' } +#_write_conf file content +#Replace the conf file with the content. +#Redirecting straight into the conf truncates it before anything is written, so +#a failed write (e.g. no space left on device) left an empty conf and the cert +#could not be renewed any more (#7247). Write a temp file next to the conf and +#rename it over the conf only after the content is verified. +_write_conf() { + __w_conf="$1" + __w_text="$2" + __w_tmp="$__w_conf.$$.tmp" + #cp -p, so the temp file carries the mode and owner of the conf + if ! cp -p "$__w_conf" "$__w_tmp" 2>/dev/null || + ! printf -- "%s\n" "$__w_text" 2>/dev/null >"$__w_tmp" || + [ "$(cat "$__w_tmp")" != "$__w_text" ]; then + rm -f "$__w_tmp" + return 1 + fi + if [ ! -L "$__w_conf" ] && mv -f "$__w_tmp" "$__w_conf" 2>/dev/null; then + return 0 + fi + #a symlink or a bind mounted file cannot be renamed over, write in place + if ! cat "$__w_tmp" 2>/dev/null >"$__w_conf"; then + #the conf may be truncated now, the temp file is the only complete copy + _err "Cannot write $__w_conf, the new content is kept in $__w_tmp" + return 1 + fi + rm -f "$__w_tmp" +} + #setopt "file" "opt" "=" "value" [";"] _setopt() { __conf="$1" @@ -2826,39 +2855,29 @@ _setopt() { return 1 ;; esac - if [ -n "$(_tail_c 1 <"$__conf")" ]; then - echo >>"$__conf" + if ! __text="$(cat "$__conf")"; then + _err "Cannot read $__conf." + return 1 fi - + #build the new content first and write it once through _write_conf: + #redirecting straight into the conf truncates it before anything is + #written, so a failing sed (#2426) or a failing write (#7247) left a + #truncated conf, and a short append left a half written line + __sed_err="" if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then _debug3 OK __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" - text="$(cat "$__conf")" - #capture first, write only on success: redirecting sed straight into the - #conf file truncates it before sed runs, so a failing sed (e.g. on an - #unescaped special character in the value) wiped the whole conf (#2426) - if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then - printf -- "%s\n" "$__text" >"$__conf" - else - _err "Cannot save '$__opt' to $__conf." - return 1 - fi - + __text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1 elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" - text="$(cat "$__conf")" - if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then - printf -- "%s\n" "$__text" >"$__conf" - else - _err "Cannot save '$__opt' to $__conf." - return 1 - fi - + __text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1 else _debug3 APP - #printf, not echo: dash's builtin echo interprets backslash escapes in - #the value and would corrupt it - printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf" + __text="$__text${__text:+$__nl}$__opt$__sep$__val$__end" + fi + if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then + _err "Cannot save '$__opt' to $__conf." + return 1 fi _debug3 "$(grep -n "^$__opt$__sep" "$__conf")" } @@ -2888,7 +2907,11 @@ _clear_conf() { _conf_data="$(cat "$_c_c_f")" #printf, not echo: dash's builtin echo interprets backslash escapes and #would corrupt saved values that contain them on every rewrite - printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f" + if ! _conf_data="$(printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" || + ! _write_conf "$_c_c_f" "$_conf_data"; then + _err "Cannot clear '$_sdkey' in $_c_c_f." + return 1 + fi else _err "Config file is empty, cannot clear" fi