xref: /freebsd/lib/libbsdconf/bsdconf_put.3 (revision dece5b05f577fc4c6b89af7ad48dfbdf7cc4945c)
1.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
2.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
3.\"
4.\" SPDX-License-Identifier: BSD-2-Clause
5.\"
6.Dd September 16, 2026
7.Dt BSDCONF_PUT 3
8.Os
9.Sh NAME
10.Nm bsdconf_put ,
11.Nm bsdconf_set_option
12.Nd resilient configuration file writing
13.Sh LIBRARY
14.Lb libbsdconf
15.Sh SYNOPSIS
16.In bsdconf.h
17.Ft int
18.Fo bsdconf_put
19.Fa "struct bsdconf_option options[]"
20.Fa "const char *path"
21.Fa "uint16_t processing_options"
22.Fa "uint16_t put_options"
23.Fc
24.Ft int
25.Fo bsdconf_set_option
26.Fa "struct bsdconf_option options[]"
27.Fa "const char *directive"
28.Fa "union bsdconf_value *value"
29.Fc
30.Sh DESCRIPTION
31.Fn bsdconf_put
32rewrites the configuration file at
33.Fa path ,
34applying the per-directive action of each option in the array:
35.Bl -tag -width BSDCONF_ACTION_SET_VALUE
36.It Dv BSDCONF_ACTION_SET_VALUE
37Set the directive to
38.Va value.str ,
39editing it in place if present or appending it to the file if absent.
40.It Dv BSDCONF_ACTION_CHECK
41Report
42.Pq via Va result
43whether the current value differs from
44.Va value.str ,
45without modifying the file.
46.It Dv BSDCONF_ACTION_REMOVE
47Delete the directive from the file.
48.El
49.Pp
50Unlike
51.Fn bsdconf_parse ,
52directives are matched exactly
53.Pq a pattern is not a writable target .
54Comments,
55blank lines,
56statement ordering,
57and the formatting of untouched statements are preserved.
58For each processed option,
59.Va result
60is set to a bitmask of
61.Dv BSDCONF_DIRECTIVE_FOUND ,
62.Dv BSDCONF_VALUE_CHANGED ,
63.Dv BSDCONF_DIRECTIVE_ADDED ,
64and
65.Dv BSDCONF_DIRECTIVE_REMOVED ,
66and
67.Va line
68is set to the line of the first match
69.Pq if found .
70A non-zero
71.Va match_line
72on input restricts the put to the statement on that physical line
73.Pq zero matches any statement, as before ;
74this allows a single
75.Ql name+=value
76among several to be rewritten or removed without appending a new line
77or touching the others
78.Pq make(1) list-strike edits in Xr sysconf 8 .
79.Pp
80The
81.Fa path
82argument to
83.Fn bsdconf_put
84must name a regular file
85.Pq only a regular file can be atomically replaced ;
86anything else is rejected with
87.Er EINVAL .
88When every option is
89.Dv BSDCONF_ACTION_CHECK ,
90or every
91.Dv BSDCONF_ACTION_SET_VALUE
92and
93.Dv BSDCONF_ACTION_REMOVE
94would leave the file unchanged,
95the original is left untouched:
96no temporary is created,
97.Va mtime
98is not bumped,
99and hard links are not severed.
100The original is read in full, subject to the
101.Ev BSDCONF_MAX_BYTES
102cap documented in
103.Xr bsdconf 3 .
104Otherwise the file is replaced atomically:
105output is streamed to a temporary file created with
106.Xr mkstemp 3
107in the target's own directory,
108flushed to stable storage with
109.Xr fsync 2 ,
110given the mode
111.Pq and, if permitted, the ownership
112of the original,
113and then moved over the original with
114.Xr rename 2 .
115An unexpected system failure or power loss mid-transaction leaves the
116original untouched
117.Pq see also Sx SECURITY CONSIDERATIONS .
118.Pp
119.Fn bsdconf_put
120operates on exactly one file.
121For a format backed by more than one file
122.Pq see Xr bsdconf_format 3 ,
123the caller decides which file to hand it,
124and the deterministic sourcing order makes that decision mechanical:
125because the last file to list a directive dictates its effective value,
126a directive should be rewritten in the last file of the format's ordered
127list that currently lists it
128.Pq found by parsing the files in order with Fn bsdconf_parse ,
129or appended to the format's default file when no file lists it.
130A removal,
131by contrast,
132must be applied to every file listing the directive,
133lest deleting the authoritative definition merely unmask an earlier one.
134This is the policy implemented by
135.Xr sysconf 8 .
136.Pp
137The
138.Fa put_options
139argument is a mask of the following bit fields:
140.Bl -tag -width BSDCONF_PUT_NO_DUPLICATES
141.It Dv BSDCONF_PUT_NO_DUPLICATES
142Fail with
143.Er EEXIST
144if a directive to be written appears more than once in the file.
145.It Dv BSDCONF_PUT_ALLOW_EMPTY
146Permit setting an empty value.
147.It Dv BSDCONF_PUT_BACKUP
148Save a copy of the original file with a
149.Ql .bak
150suffix before replacing it.
151.It Dv BSDCONF_PUT_UNQUOTED
152Emit
153.Va value.str
154as literal file text:
155no quotes are added and no characters are backslash-escaped
156.Pq the caller supplies the bytes that should appear on disk .
157Embedded whitespace is fine
158.Pq the value runs to the end of the line .
159A value that could not round-trip under the parser
160.Pq an embedded newline, unescaped comment marker, or trailing unescaped backslash
161is rejected with
162.Er EINVAL
163rather than silently corrupting the file.
164The reader still runs
165.Fn bsdconf_strunexpand
166on input, so escape sequences present in the file become the logical value
167on read; this flag does not re-encode them on write.
168.It Dv BSDCONF_PUT_QUOTE_ALWAYS
169Enclose every value in double-quotes,
170escaping embedded quotes and backslashes.
171.El
172.Pp
173.Fn bsdconf_set_option
174traverses the options-array and stages
175.Fa value
176into the option whose directive matches
177.Fa directive
178via
179.Xr strcmp 3 .
180.Sh RETURN VALUES
181.Fn bsdconf_put
182returns zero on success;
183otherwise -1 is returned and the global variable
184.Va errno
185is set to indicate the error.
186.Fn bsdconf_set_option
187returns 1 if a matching directive was staged,
188otherwise 0.
189.Sh SEE ALSO
190.Xr bsdconf 3 ,
191.Xr bsdconf_format 3 ,
192.Xr sysconf 8
193.Sh HISTORY
194The
195.Fn bsdconf_put
196and
197.Fn bsdconf_set_option
198functions first appeared in
199.Fx 16.0
200as part of
201.Xr bsdconf 3 .
202.Sh AUTHORS
203.An Devin Teske Aq Mt dteske@FreeBSD.org
204.An Faraz Vahedi Aq Mt kfv@FreeBSD.org
205.Sh LIMITATIONS
206.Fn bsdconf_put
207matches directives by exact name and, by default, treats each name as
208single-valued: the first matching statement is rewritten, or a new line
209is appended when none matches.
210That suits the built-in formats when the last assignment wins.
211.Pp
212Make(1)-style cumulative
213.Ql +=
214chains are supported only in part.
215With
216.Va match_line
217unset, a
218.Ql +=
219.Dv BSDCONF_ACTION_SET_VALUE
220appends a new statement rather than collapsing earlier ones; with
221.Va match_line
222set, one physical line can be rewritten or removed
223.Pq as Xr sysconf 8 does for make/src list strikes .
224Word-level
225.Ql -=
226policy and effective-value accumulation remain the caller's
227responsibility;
228the library does not implement
229.Ql -=
230itself.
231.Pp
232Formats whose directives legitimately repeat without make(1) operators
233.Pq for example Apache-style cumulative keywords; see Xr bsdconf 3
234likewise parse with caller-supplied callbacks, but
235.Fn bsdconf_put
236offers no first-class insert, remove, or reorder of occurrence
237.Em N
238among equals—only exact-name match or
239.Va match_line
240selection.
241.Sh SECURITY CONSIDERATIONS
242The write transaction is designed to be safe against both interruption and
243interference.
244The temporary file is created by
245.Xr mkstemp 3
246.Pq Dv O_EXCL ; never predictable, never followed
247in the target's own directory,
248so the data never crosses a filesystem boundary and never transits a
249world-writable directory such as
250.Pa /tmp .
251The mode and ownership propagated onto it are taken by
252.Xr fstat 2
253from the descriptor actually read,
254not by re-looking up the path,
255and are applied with
256.Xr fchmod 2
257.Pq the original mode masked to 0666, so setuid, setgid, and execute bits are not copied
258and
259.Xr fchown 2
260on the open descriptor,
261so a concurrently swapped file cannot influence them.
262A backup requested with
263.Dv BSDCONF_PUT_BACKUP
264is created mode 0600 then
265.Xr fchmod 2 Ns 'd
266to the original mode masked to 0777
267.Pq setuid and setgid bits are not restored
268and refuses to follow a symbolic link planted at the
269.Ql .bak
270name
271.Pq Dv O_NOFOLLOW .
272.Pp
273Symbolic links in the
274.Fa path
275argument to
276.Fn bsdconf_put
277are resolved
278.Pq with Xr realpath 3
279before work begins:
280writing through a symbolic link rewrites the file it points at and
281preserves the link itself,
282rather than replacing the link with a regular file.
283The resolution is not re-verified at
284.Xr open 2
285time;
286as with any path-based interface,
287an actor with write access to a directory along the path can redirect it,
288so the containing directories must be trustworthy
289.Pq as those of system configuration files are .
290Because replacement is by
291.Xr rename 2 ,
292a target with multiple hard links is severed from its other names,
293which afterwards continue to reference the old content;
294this is inherent to atomic replacement.
295