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 <noreply@anthropic.com>
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7ohY7h4RUSNJ2ES14NMmY

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Tao AnandClaude Opus 5.5 authored and GitHub committed 2026-09-27 10:02:42 +02:00
1 parent a1de626512
commit 6467ca9764
1 file changed
+49 -26
+49 -26
View File
@@ -2800,6 +2800,35 @@ _sed_escape_rhs() {
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' 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 "file" "opt" "=" "value" [";"]
_setopt() { _setopt() {
__conf="$1" __conf="$1"
@@ -2826,39 +2855,29 @@ _setopt() {
return 1 return 1
;; ;;
esac esac
if [ -n "$(_tail_c 1 <"$__conf")" ]; then if ! __text="$(cat "$__conf")"; then
echo >>"$__conf" _err "Cannot read $__conf."
return 1
fi 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 if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
_debug3 OK _debug3 OK
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
#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
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
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
else else
_debug3 APP _debug3 APP
#printf, not echo: dash's builtin echo interprets backslash escapes in __text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
#the value and would corrupt it fi
printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf" if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
_err "Cannot save '$__opt' to $__conf."
return 1
fi fi
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")" _debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
} }
@@ -2888,7 +2907,11 @@ _clear_conf() {
_conf_data="$(cat "$_c_c_f")" _conf_data="$(cat "$_c_c_f")"
#printf, not echo: dash's builtin echo interprets backslash escapes and #printf, not echo: dash's builtin echo interprets backslash escapes and
#would corrupt saved values that contain them on every rewrite #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 else
_err "Config file is empty, cannot clear" _err "Config file is empty, cannot clear"
fi fi
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.