Skip to content

Commit 4fdbd8b

Browse files
committed
lightningd: catch edits of config files *before* we're committed.
Another report, that we crash if it's edited. We should check that too! Reported-by: daywalker90 Signed-off-by: Rusty Russell <[email protected]>
1 parent 5e79cd4 commit 4fdbd8b

File tree

2 files changed

+107
-20
lines changed

2 files changed

+107
-20
lines changed

lightningd/configs.c

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -416,12 +416,41 @@ static size_t append_to_file(struct lightningd *ld,
416416
return strcount(buffer, "\n") + 1;
417417
}
418418

419+
static const char *grab_and_check(const tal_t *ctx,
420+
const char *fname,
421+
size_t linenum,
422+
const char *expected,
423+
char ***lines)
424+
{
425+
char *contents;
426+
427+
contents = grab_file(tmpctx, fname);
428+
if (!contents)
429+
return tal_fmt(ctx, "Could not load configfile %s: %s",
430+
fname, strerror(errno));
431+
432+
/* These are 1-based! */
433+
assert(linenum > 0);
434+
*lines = tal_strsplit(ctx, contents, "\r\n", STR_EMPTY_OK);
435+
if (linenum >= tal_count(*lines))
436+
return tal_fmt(ctx, "Configfile %s no longer has %zu lines!",
437+
fname, linenum);
438+
439+
if (!streq((*lines)[linenum - 1], expected))
440+
return tal_fmt(ctx, "Configfile %s line %zu changed from %s to %s!",
441+
fname, linenum,
442+
expected,
443+
(*lines)[linenum - 1]);
444+
return NULL;
445+
}
446+
419447
/* This comments out the config file entry or maybe replace one */
420448
static void configfile_replace_var(struct lightningd *ld,
421449
const struct configvar *cv,
422450
const char *replace)
423451
{
424-
char *contents, **lines, *template;
452+
char **lines, *template, *contents;
453+
const char *err;
425454
int outfd;
426455

427456
switch (cv->src) {
@@ -437,21 +466,11 @@ static void configfile_replace_var(struct lightningd *ld,
437466
break;
438467
}
439468

440-
contents = grab_file(tmpctx, cv->file);
441-
if (!contents)
442-
fatal("Could not load configfile %s: %s",
443-
cv->file, strerror(errno));
444-
445-
lines = tal_strsplit(contents, contents, "\r\n", STR_EMPTY_OK);
446-
if (cv->linenum - 1 >= tal_count(lines))
447-
fatal("Configfile %s no longer has %u lines!",
448-
cv->file, cv->linenum);
449-
450-
if (!streq(lines[cv->linenum - 1], cv->configline))
451-
fatal("Configfile %s line %u changed from %s to %s!",
452-
cv->file, cv->linenum,
453-
cv->configline,
454-
lines[cv->linenum - 1]);
469+
/* If it changed *now*, that's fatal: we already set it locally! */
470+
err = grab_and_check(tmpctx, cv->file, cv->linenum, cv->configline,
471+
&lines);
472+
if (err)
473+
fatal("%s", err);
455474

456475
if (replace)
457476
lines[cv->linenum - 1] = cast_const(char *, replace);
@@ -595,14 +614,12 @@ static bool dir_writable(const char *fname)
595614
/* Returns config file name if not writable */
596615
static const char *config_not_writable(const tal_t *ctx,
597616
struct command *cmd,
598-
const struct opt_table *ot)
617+
const struct configvar *oldcv)
599618
{
600619
struct lightningd *ld = cmd->ld;
601-
struct configvar *oldcv;
602620
const char *fname;
603621

604622
/* If it exists before, we will need to replace that file (rename) */
605-
oldcv = configvar_first(ld->configvars, opt_names_arr(tmpctx, ot));
606623
if (oldcv && oldcv->file) {
607624
/* We will rename */
608625
if (!dir_writable(oldcv->file))
@@ -645,11 +662,32 @@ static struct command_result *json_setconfig(struct command *cmd,
645662
assert(!(ot->type & OPT_MULTI));
646663

647664
if (!*transient) {
648-
const char *fname = config_not_writable(cmd, cmd, ot);
665+
const struct configvar *cv;
666+
const char *fname;
667+
668+
cv = configvar_first(cmd->ld->configvars,
669+
opt_names_arr(tmpctx, ot));
670+
671+
fname = config_not_writable(cmd, cmd, cv);
649672
if (fname)
650673
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
651674
"Cannot write to config file %s",
652675
fname);
676+
677+
/* Check if old config has changed (so we couldn't be able
678+
* to comment it out! */
679+
if (cv && cv->file) {
680+
const char *changed;
681+
char **lines;
682+
683+
changed = grab_and_check(tmpctx,
684+
cv->file, cv->linenum,
685+
cv->configline,
686+
&lines);
687+
if (changed)
688+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
689+
"%s", changed);
690+
}
653691
}
654692

655693
/* We use arg = NULL to tell callback it's only for testing */

tests/test_misc.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4278,6 +4278,55 @@ def test_setconfig_access(node_factory, bitcoind):
42784278
os.chmod(includedir, 0o700)
42794279

42804280

4281+
def test_setconfig_changed(node_factory, bitcoind):
4282+
"""Test that we correctly fail (not crash) if config file changed"""
4283+
l1 = node_factory.get_node(start=False)
4284+
4285+
netconfigfile = os.path.join(l1.daemon.opts.get("lightning-dir"), TEST_NETWORK, 'config')
4286+
with open(netconfigfile, 'w') as file:
4287+
file.write("min-capacity-sat=100")
4288+
l1.start()
4289+
4290+
assert l1.rpc.listconfigs(config="min-capacity-sat")['configs']['min-capacity-sat']['value_int'] == 100
4291+
4292+
# Change it underneath
4293+
with open(netconfigfile, 'w') as file:
4294+
file.write("#some comment\nmin-capacity-sat=100")
4295+
4296+
# This will fail.
4297+
with pytest.raises(RpcError, match=f'Configfile {netconfigfile} line 1 changed from min-capacity-sat=100 to #some comment!'):
4298+
l1.rpc.check("setconfig", config="min-capacity-sat", val=9999)
4299+
with pytest.raises(RpcError, match=f'Configfile {netconfigfile} line 1 changed from min-capacity-sat=100 to #some comment!'):
4300+
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
4301+
4302+
# Restore it.
4303+
with open(netconfigfile, 'w') as file:
4304+
file.write("min-capacity-sat=100")
4305+
4306+
# Succeeds
4307+
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
4308+
4309+
# Now mess with config.setconfig...
4310+
setconfigfile = netconfigfile + ".setconfig"
4311+
with open(setconfigfile, 'w') as file:
4312+
pass
4313+
4314+
# Now this will fail (truncated)
4315+
with pytest.raises(RpcError, match=f'Configfile {setconfigfile} no longer has 2 lines'):
4316+
l1.rpc.check("setconfig", config="min-capacity-sat", val=9999)
4317+
with pytest.raises(RpcError, match=f'Configfile {setconfigfile} no longer has 2 lines'):
4318+
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
4319+
4320+
# This will fail (changed)
4321+
with open(setconfigfile, 'w') as file:
4322+
file.write("# Created and update by setconfig, but you can edit this manually when node is stopped.\nmin-capacity-sat=999")
4323+
4324+
with pytest.raises(RpcError, match=f'Configfile {setconfigfile} line 2 changed from min-capacity-sat=9999 to min-capacity-sat=999!'):
4325+
l1.rpc.check("setconfig", config="min-capacity-sat", val=9999)
4326+
with pytest.raises(RpcError, match=f'Configfile {setconfigfile} line 2 changed from min-capacity-sat=9999 to min-capacity-sat=999!'):
4327+
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
4328+
4329+
42814330
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
42824331
def test_recover_command(node_factory, bitcoind):
42834332
l1, l2 = node_factory.get_nodes(2)

0 commit comments

Comments
 (0)