Skip to content

Commit 5e79cd4

Browse files
committed
lightningd: check for writability before allowing setconfig.
If we actually can't write it, we crash (to avoid an inconsistent state), so sanity check FIRST. Fixes: #7964 Signed-off-by: Rusty Russell <[email protected]>
1 parent 8127fcf commit 5e79cd4

File tree

2 files changed

+149
-2
lines changed

2 files changed

+149
-2
lines changed

lightningd/configs.c

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,47 @@ static struct command_result *setconfig_success(struct command *cmd,
582582
return command_success(cmd, response);
583583
}
584584

585+
static bool file_writable(const char *fname)
586+
{
587+
return access(fname, W_OK) == 0;
588+
}
589+
590+
static bool dir_writable(const char *fname)
591+
{
592+
return access(path_dirname(tmpctx, fname), W_OK) == 0;
593+
}
594+
595+
/* Returns config file name if not writable */
596+
static const char *config_not_writable(const tal_t *ctx,
597+
struct command *cmd,
598+
const struct opt_table *ot)
599+
{
600+
struct lightningd *ld = cmd->ld;
601+
struct configvar *oldcv;
602+
const char *fname;
603+
604+
/* If it exists before, we will need to replace that file (rename) */
605+
oldcv = configvar_first(ld->configvars, opt_names_arr(tmpctx, ot));
606+
if (oldcv && oldcv->file) {
607+
/* We will rename */
608+
if (!dir_writable(oldcv->file))
609+
return oldcv->file;
610+
}
611+
612+
/* If we don't have a setconfig file we'll have to create it, and
613+
* amend the config file. */
614+
if (!ld->setconfig_file) {
615+
fname = base_conf_file(tmpctx, ld, NULL);
616+
if (!dir_writable(fname))
617+
return tal_steal(ctx, fname);
618+
} else {
619+
/* We will try to append config.setconfig */
620+
if (!file_writable(ld->setconfig_file))
621+
return tal_strdup(ctx, ld->setconfig_file);
622+
}
623+
return NULL;
624+
}
625+
585626
static struct command_result *json_setconfig(struct command *cmd,
586627
const char *buffer,
587628
const jsmntok_t *obj UNNEEDED,
@@ -600,11 +641,17 @@ static struct command_result *json_setconfig(struct command *cmd,
600641
NULL))
601642
return command_param_failed();
602643

603-
log_debug(cmd->ld->log, "setconfig!");
604-
605644
/* We don't handle DYNAMIC MULTI, at least yet! */
606645
assert(!(ot->type & OPT_MULTI));
607646

647+
if (!*transient) {
648+
const char *fname = config_not_writable(cmd, cmd, ot);
649+
if (fname)
650+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
651+
"Cannot write to config file %s",
652+
fname);
653+
}
654+
608655
/* We use arg = NULL to tell callback it's only for testing */
609656
if (command_check_only(cmd))
610657
arg = NULL;

tests/test_misc.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4178,6 +4178,106 @@ def test_setconfig(node_factory, bitcoind):
41784178
assert lines == ["# Created and update by setconfig, but you can edit this manually when node is stopped.", "min-capacity-sat=400000"]
41794179

41804180

4181+
def test_setconfig_access(node_factory, bitcoind):
4182+
"""Test that we correctly fail (not crash) if config file/dir not writable"""
4183+
4184+
# Disable bookkeeper, with its separate db which gets upset under CI.
4185+
l1 = node_factory.get_node(options={'disable-plugin': 'bookkeeper'})
4186+
4187+
netconfigfile = os.path.join(l1.daemon.opts.get("lightning-dir"), TEST_NETWORK, 'config')
4188+
4189+
# It's OK if the config file doesn't exist.
4190+
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
4191+
4192+
# But not if we can't create it.
4193+
os.chmod(os.path.dirname(netconfigfile), 0o550)
4194+
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
4195+
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
4196+
4197+
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
4198+
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
4199+
4200+
# Empty config file (we need to be able to write dir)
4201+
os.chmod(os.path.dirname(netconfigfile), 0o750)
4202+
with open(netconfigfile, 'w') as file:
4203+
pass
4204+
l1.restart()
4205+
4206+
# check will fail
4207+
os.chmod(os.path.dirname(netconfigfile), 0o550)
4208+
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
4209+
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
4210+
4211+
# real write will definitely fail
4212+
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
4213+
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
4214+
4215+
# Transient? Don't care that we can't change it.
4216+
ret = l1.rpc.setconfig(config='min-capacity-sat', val=400001, transient=True)
4217+
assert ret == {'config':
4218+
{'config': 'min-capacity-sat',
4219+
'source': 'setconfig transient',
4220+
'value_int': 400001,
4221+
'dynamic': True}}
4222+
4223+
# db also needs to write directory!
4224+
os.chmod(os.path.dirname(netconfigfile), 0o750)
4225+
4226+
# Now put a setting in the main config file
4227+
l1.stop()
4228+
mainconfigfile = os.path.join(l1.daemon.opts.get("lightning-dir"), 'config')
4229+
with open(mainconfigfile, 'w') as file:
4230+
file.write("min-capacity-sat=100")
4231+
l1.start()
4232+
4233+
# We don't actually need to write file, just directoty.
4234+
os.chmod(mainconfigfile, 0o400)
4235+
4236+
l1.rpc.check("setconfig", config="min-capacity-sat", val=9999)
4237+
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
4238+
4239+
# setconfig file exists, and its permissions matter!
4240+
setconfigfile = netconfigfile + ".setconfig"
4241+
os.chmod(setconfigfile, 0o400)
4242+
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
4243+
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
4244+
4245+
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
4246+
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
4247+
4248+
# Change location of setconfig file in another sub directory.
4249+
l1.stop()
4250+
includedir = os.path.join(os.path.dirname(netconfigfile), "include")
4251+
os.mkdir(includedir)
4252+
os.unlink(setconfigfile)
4253+
setconfigfile = os.path.join(includedir, "special.setconfig")
4254+
with open(netconfigfile, 'w') as file:
4255+
file.write(f"include {setconfigfile}")
4256+
with open(setconfigfile, 'w') as file:
4257+
pass
4258+
l1.start()
4259+
4260+
# Needs to be writable, to append.
4261+
os.chmod(setconfigfile, 0o400)
4262+
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
4263+
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
4264+
4265+
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
4266+
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
4267+
4268+
# But directory doesn't!
4269+
os.chmod(includedir, 0o500)
4270+
os.chmod(setconfigfile, 0o700)
4271+
assert l1.rpc.setconfig(config="min-capacity-sat", val=1000000) == {'config':
4272+
{'config': 'min-capacity-sat',
4273+
'source': f'{setconfigfile}:1',
4274+
'value_int': 1000000,
4275+
'dynamic': True}}
4276+
4277+
# Don't break pytest cleanup!
4278+
os.chmod(includedir, 0o700)
4279+
4280+
41814281
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
41824282
def test_recover_command(node_factory, bitcoind):
41834283
l1, l2 = node_factory.get_nodes(2)

0 commit comments

Comments
 (0)