mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-22 22:45:27 +01:00
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: https://github.com/ElementsProject/lightning/issues/7964 Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
ee690665eb
commit
70e28f767a
2 changed files with 147 additions and 2 deletions
|
@ -579,6 +579,47 @@ static struct command_result *setconfig_success(struct command *cmd,
|
||||||
return command_success(cmd, response);
|
return command_success(cmd, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool file_writable(const char *fname)
|
||||||
|
{
|
||||||
|
return access(fname, W_OK) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool dir_writable(const char *fname)
|
||||||
|
{
|
||||||
|
return access(path_dirname(tmpctx, fname), W_OK) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns config file name if not writable */
|
||||||
|
static const char *config_not_writable(const tal_t *ctx,
|
||||||
|
struct command *cmd,
|
||||||
|
const struct opt_table *ot)
|
||||||
|
{
|
||||||
|
struct lightningd *ld = cmd->ld;
|
||||||
|
struct configvar *oldcv;
|
||||||
|
const char *fname;
|
||||||
|
|
||||||
|
/* If it exists before, we will need to replace that file (rename) */
|
||||||
|
oldcv = configvar_first(ld->configvars, opt_names_arr(tmpctx, ot));
|
||||||
|
if (oldcv && oldcv->file) {
|
||||||
|
/* We will rename */
|
||||||
|
if (!dir_writable(oldcv->file))
|
||||||
|
return oldcv->file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we don't have a setconfig file we'll have to create it, and
|
||||||
|
* amend the config file. */
|
||||||
|
if (!ld->setconfig_file) {
|
||||||
|
fname = base_conf_file(tmpctx, ld, NULL);
|
||||||
|
if (!dir_writable(fname))
|
||||||
|
return tal_steal(ctx, fname);
|
||||||
|
} else {
|
||||||
|
/* We will try to append config.setconfig */
|
||||||
|
if (!file_writable(ld->setconfig_file))
|
||||||
|
return tal_strdup(ctx, ld->setconfig_file);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
static struct command_result *json_setconfig(struct command *cmd,
|
static struct command_result *json_setconfig(struct command *cmd,
|
||||||
const char *buffer,
|
const char *buffer,
|
||||||
const jsmntok_t *obj UNNEEDED,
|
const jsmntok_t *obj UNNEEDED,
|
||||||
|
@ -597,11 +638,17 @@ static struct command_result *json_setconfig(struct command *cmd,
|
||||||
NULL))
|
NULL))
|
||||||
return command_param_failed();
|
return command_param_failed();
|
||||||
|
|
||||||
log_debug(cmd->ld->log, "setconfig!");
|
|
||||||
|
|
||||||
/* We don't handle DYNAMIC MULTI, at least yet! */
|
/* We don't handle DYNAMIC MULTI, at least yet! */
|
||||||
assert(!(ot->type & OPT_MULTI));
|
assert(!(ot->type & OPT_MULTI));
|
||||||
|
|
||||||
|
if (!*transient) {
|
||||||
|
const char *fname = config_not_writable(cmd, cmd, ot);
|
||||||
|
if (fname)
|
||||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||||
|
"Cannot write to config file %s",
|
||||||
|
fname);
|
||||||
|
}
|
||||||
|
|
||||||
/* We use arg = NULL to tell callback it's only for testing */
|
/* We use arg = NULL to tell callback it's only for testing */
|
||||||
if (command_check_only(cmd))
|
if (command_check_only(cmd))
|
||||||
arg = NULL;
|
arg = NULL;
|
||||||
|
|
|
@ -4068,6 +4068,104 @@ def test_setconfig(node_factory, bitcoind):
|
||||||
assert lines == ["# Created and update by setconfig, but you can edit this manually", "min-capacity-sat=400000"]
|
assert lines == ["# Created and update by setconfig, but you can edit this manually", "min-capacity-sat=400000"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_setconfig_access(node_factory, bitcoind):
|
||||||
|
"""Test that we correctly fail (not crash) if config file/dir not writable"""
|
||||||
|
l1 = node_factory.get_node()
|
||||||
|
|
||||||
|
netconfigfile = os.path.join(l1.daemon.opts.get("lightning-dir"), TEST_NETWORK, 'config')
|
||||||
|
|
||||||
|
# It's OK if the config file doesn't exist.
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# But not if we can't create it.
|
||||||
|
os.chmod(os.path.dirname(netconfigfile), 0o550)
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
|
||||||
|
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# Empty config file (we need to be able to write dir)
|
||||||
|
os.chmod(os.path.dirname(netconfigfile), 0o750)
|
||||||
|
with open(netconfigfile, 'w') as file:
|
||||||
|
pass
|
||||||
|
l1.restart()
|
||||||
|
|
||||||
|
# check will fail
|
||||||
|
os.chmod(os.path.dirname(netconfigfile), 0o550)
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# real write will definitely fail
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {netconfigfile}'):
|
||||||
|
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# Transient? Don't care that we can't change it.
|
||||||
|
ret = l1.rpc.setconfig(config='min-capacity-sat', val=400001, transient=True)
|
||||||
|
assert ret == {'config':
|
||||||
|
{'config': 'min-capacity-sat',
|
||||||
|
'source': 'setconfig transient',
|
||||||
|
'value_int': 400001,
|
||||||
|
'dynamic': True}}
|
||||||
|
|
||||||
|
# db also needs to write directory!
|
||||||
|
os.chmod(os.path.dirname(netconfigfile), 0o750)
|
||||||
|
|
||||||
|
# Now put a setting in the main config file
|
||||||
|
l1.stop()
|
||||||
|
mainconfigfile = os.path.join(l1.daemon.opts.get("lightning-dir"), 'config')
|
||||||
|
with open(mainconfigfile, 'w') as file:
|
||||||
|
file.write("min-capacity-sat=100")
|
||||||
|
l1.start()
|
||||||
|
|
||||||
|
# We don't actually need to write file, just directoty.
|
||||||
|
os.chmod(mainconfigfile, 0o400)
|
||||||
|
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=9999)
|
||||||
|
l1.rpc.setconfig(config="min-capacity-sat", val=9999)
|
||||||
|
|
||||||
|
# setconfig file exists, and its permissions matter!
|
||||||
|
setconfigfile = netconfigfile + ".setconfig"
|
||||||
|
os.chmod(setconfigfile, 0o400)
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
|
||||||
|
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# Change location of setconfig file in another sub directory.
|
||||||
|
l1.stop()
|
||||||
|
includedir = os.path.join(os.path.dirname(netconfigfile), "include")
|
||||||
|
os.mkdir(includedir)
|
||||||
|
os.unlink(setconfigfile)
|
||||||
|
setconfigfile = os.path.join(includedir, "special.setconfig")
|
||||||
|
with open(netconfigfile, 'w') as file:
|
||||||
|
file.write(f"include {setconfigfile}")
|
||||||
|
with open(setconfigfile, 'w') as file:
|
||||||
|
pass
|
||||||
|
l1.start()
|
||||||
|
|
||||||
|
# Needs to be writable, to append.
|
||||||
|
os.chmod(setconfigfile, 0o400)
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
|
||||||
|
l1.rpc.check("setconfig", config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
with pytest.raises(RpcError, match=f'Cannot write to config file {setconfigfile}'):
|
||||||
|
l1.rpc.setconfig(config="min-capacity-sat", val=1000000)
|
||||||
|
|
||||||
|
# But directory doesn't!
|
||||||
|
os.chmod(includedir, 0o500)
|
||||||
|
os.chmod(setconfigfile, 0o700)
|
||||||
|
assert l1.rpc.setconfig(config="min-capacity-sat", val=1000000) == {'config':
|
||||||
|
{'config': 'min-capacity-sat',
|
||||||
|
'source': f'{setconfigfile}:1',
|
||||||
|
'value_int': 1000000,
|
||||||
|
'dynamic': True}}
|
||||||
|
|
||||||
|
# Don't break pytest cleanup!
|
||||||
|
os.chmod(includedir, 0o700)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
|
@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "deletes database, which is assumed sqlite3")
|
||||||
def test_recover_command(node_factory, bitcoind):
|
def test_recover_command(node_factory, bitcoind):
|
||||||
l1, l2 = node_factory.get_nodes(2)
|
l1, l2 = node_factory.get_nodes(2)
|
||||||
|
|
Loading…
Add table
Reference in a new issue