Skip to content

Commit 4392e94

Browse files
author
r.inyakin
committed
start: insufficient permissions to directories
Fixed a bug where an error did not appear when access rights to the var directive were insufficient. Closes #1238
1 parent bbc33e6 commit 4392e94

5 files changed

Lines changed: 218 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1313

1414
### Fixed
1515

16+
- `tt start`: fixed a bug where an error did not appear when access rights
17+
to the var directive were insufficient.
18+
1619
## [2.11.4] - 2026-03-04
1720

1821
The auxilary release just to keep version matching with `tt-ee`

cli/cmd/start.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sync"
1111
"syscall"
1212

13+
"github.com/apex/log"
1314
"github.com/spf13/cobra"
1415
"github.com/tarantool/tt/cli/cmd/internal"
1516
"github.com/tarantool/tt/cli/cmdcontext"
@@ -92,9 +93,12 @@ func startInstancesInteractive(cmdCtx *cmdcontext.CmdCtx, instances []running.In
9293
prefix := running.GetAppInstanceName(instCtx) + " "
9394
wg.Add(1)
9495
go func(inst running.InstanceCtx) {
95-
running.RunInstance(ctx, cmdCtx, inst,
96+
err := running.RunInstance(ctx, cmdCtx, inst,
9697
running.NewColorizedPrefixWriter(os.Stdout, clr, prefix),
9798
running.NewColorizedPrefixWriter(os.Stderr, clr, prefix))
99+
if err != nil {
100+
log.Error(err.Error())
101+
}
98102
wg.Done()
99103
}(instCtx)
100104
}

cli/running/running.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,12 @@ func RunInstance(ctx context.Context, cmdCtx *cmdcontext.CmdCtx, inst InstanceCt
753753
) error {
754754
for _, dataDir := range [...]string{inst.WalDir, inst.VinylDir, inst.MemtxDir, inst.RunDir} {
755755
if err := util.CreateDirectory(dataDir, defaultDirPerms); err != nil {
756-
return err
756+
return fmt.Errorf("failed to run instance %q: %s",
757+
GetAppInstanceName(inst)+" ", err)
758+
}
759+
if err := util.IsDirContentWritable(dataDir); err != nil {
760+
return fmt.Errorf("failed to run instance %q: %s",
761+
GetAppInstanceName(inst)+" ", err)
757762
}
758763
}
759764

@@ -966,6 +971,20 @@ Cluster config path: %q`, tntVersion.Str, inst.ClusterConfigPath)
966971
func StartWatchdog(cmdCtx *cmdcontext.CmdCtx, ttExecutable string, instance InstanceCtx,
967972
args []string,
968973
) error {
974+
for _, dataDir := range [...]string{
975+
instance.WalDir, instance.VinylDir,
976+
instance.MemtxDir, instance.RunDir,
977+
} {
978+
if err := util.CreateDirectory(dataDir, defaultDirPerms); err != nil {
979+
return fmt.Errorf("failed to run instance %q: %s",
980+
GetAppInstanceName(instance)+" ", err)
981+
}
982+
if err := util.IsDirContentWritable(dataDir); err != nil {
983+
return fmt.Errorf("failed to run instance %q: %s",
984+
GetAppInstanceName(instance)+" ", err)
985+
}
986+
}
987+
969988
appName := GetAppInstanceName(instance)
970989
// If an instance is already running don't try to start it again.
971990
// To restart an instance use tt restart command.

cli/util/util.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,70 @@ func CreateDirectory(dirName string, fileMode os.FileMode) error {
752752
return nil
753753
}
754754

755+
// IsDirWritable checks if the directory is writable.
756+
func IsDirWritable(dirName string) error {
757+
testFile, err := os.CreateTemp(dirName, ".tt_write_test_*")
758+
if err != nil {
759+
return fmt.Errorf("directory %q is not writable: %w", dirName, err)
760+
}
761+
762+
defer os.Remove(testFile.Name())
763+
764+
if err = testFile.Close(); err != nil {
765+
return fmt.Errorf("failed to close test file %q: %w", testFile.Name(), err)
766+
}
767+
768+
testWriteFile, err := os.OpenFile(testFile.Name(), os.O_RDWR, 0)
769+
if err != nil {
770+
return fmt.Errorf("failed to open test file %q: %w", testFile.Name(), err)
771+
}
772+
773+
if err = testWriteFile.Close(); err != nil {
774+
return fmt.Errorf("failed to close test file %q: %w", testFile.Name(), err)
775+
}
776+
777+
return nil
778+
}
779+
780+
// IsDirContentWritable checks if all files and subdirectories inside the directory
781+
// are writable by the current user.
782+
func IsDirContentWritable(dirName string) error {
783+
err := filepath.WalkDir(dirName, func(path string, d fs.DirEntry, err error) error {
784+
if err != nil {
785+
return fmt.Errorf("directory %q is not accessible: %w", path, err)
786+
}
787+
788+
info, err := d.Info()
789+
if err != nil {
790+
return fmt.Errorf("cannot get info for %q: %w", path, err)
791+
}
792+
793+
if info.IsDir() {
794+
if err := IsDirWritable(path); err != nil {
795+
return fmt.Errorf("directory %q is not writable: %w", path, err)
796+
}
797+
} else {
798+
if filepath.Ext(path) == ".control" {
799+
return nil
800+
}
801+
802+
f, err := os.OpenFile(path, os.O_RDWR, 0)
803+
if err != nil {
804+
return fmt.Errorf("file %q is not writable: %w", path, err)
805+
}
806+
if err = f.Close(); err != nil {
807+
return fmt.Errorf("failed to close file %q: %w", path, err)
808+
}
809+
}
810+
return nil
811+
})
812+
if err != nil {
813+
return fmt.Errorf("failed to check directory content writability for %q: %w",
814+
dirName, err)
815+
}
816+
return nil
817+
}
818+
755819
// writeYaml writes YAML encoding of object o to fileName.
756820
func WriteYaml(fileName string, o interface{}) error {
757821
file, err := os.Create(fileName)

test/integration/start/test_start.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
import subprocess
3+
14
import pytest
25
import tt_helper
36

@@ -109,3 +112,126 @@ def test_start_multi_inst(tt, tt_app, target):
109112
)
110113
def test_start_cluster(tt, tt_app, target):
111114
check_start(tt, tt_app, target)
115+
116+
117+
################################################################
118+
# Permission denied
119+
120+
121+
@pytest.mark.skipif(skip_cluster_cond, reason=skip_cluster_reason)
122+
@pytest.mark.skipif(
123+
os.getuid() == 0,
124+
reason="Skipping the test, it shouldn't run as root",
125+
)
126+
@pytest.mark.tt_app(**tt_cluster_app)
127+
@pytest.mark.parametrize(
128+
"tt_running_targets",
129+
[
130+
pytest.param([], id="running:none"),
131+
],
132+
)
133+
def test_start_permission_denied(tt, tt_app):
134+
var_dir = tt_app.path("var")
135+
os.makedirs(var_dir, exist_ok=True)
136+
os.chmod(var_dir, 0o444)
137+
138+
try:
139+
rc, out = tt.exec("start")
140+
assert rc != 0
141+
assert "permission denied" in out.lower()
142+
finally:
143+
os.chmod(var_dir, 0o755)
144+
145+
146+
@pytest.mark.skipif(skip_cluster_cond, reason=skip_cluster_reason)
147+
@pytest.mark.skipif(
148+
os.getuid() == 0,
149+
reason="Skipping the test, it shouldn't run as root",
150+
)
151+
@pytest.mark.tt_app(**tt_cluster_app)
152+
@pytest.mark.parametrize(
153+
"tt_running_targets",
154+
[
155+
pytest.param([], id="running:none"),
156+
],
157+
)
158+
def test_start_permission_denied_permissions_denied_files(tt, tt_app):
159+
rc, _ = tt.exec("start")
160+
assert rc == 0
161+
assert utils.wait_files(5, tt_helper.pid_files(tt_app, tt_app.instances))
162+
163+
rc, _ = tt.exec("stop", "-y")
164+
assert rc == 0
165+
166+
log_dir = tt_app.path("var", "log")
167+
lib_dir = tt_app.path("var", "lib")
168+
run_dir = tt_app.path("var", "run")
169+
170+
dirs_to_lock = []
171+
for data_dir in [log_dir, lib_dir, run_dir]:
172+
for root, dirs, files in os.walk(data_dir, topdown=False):
173+
dirs_to_lock.extend(os.path.join(root, f) for f in files)
174+
dirs_to_lock.extend(os.path.join(root, d) for d in dirs)
175+
dirs_to_lock.append(data_dir)
176+
177+
for d in dirs_to_lock:
178+
os.chmod(d, 0o444)
179+
180+
try:
181+
rc, out = tt.exec("start")
182+
assert rc != 0
183+
assert "permission denied" in out.lower()
184+
finally:
185+
for d in dirs_to_lock:
186+
if os.path.exists(d):
187+
os.chmod(d, 0o755)
188+
189+
190+
@pytest.mark.skipif(skip_cluster_cond, reason=skip_cluster_reason)
191+
@pytest.mark.skipif(
192+
os.getuid() == 0,
193+
reason="Skipping the test, it shouldn't run as root",
194+
)
195+
@pytest.mark.tt_app(**tt_cluster_app)
196+
@pytest.mark.parametrize(
197+
"tt_running_targets",
198+
[
199+
pytest.param([], id="running:none"),
200+
],
201+
)
202+
def test_start_permission_denied_rewrite_error(tt, tt_app):
203+
data_dirs = [
204+
tt_app.path("var", "lib"),
205+
tt_app.path("var", "log"),
206+
tt_app.path("var", "run"),
207+
]
208+
209+
for data_dir in data_dirs:
210+
os.makedirs(data_dir, exist_ok=True)
211+
for instance in tt_app.instances:
212+
os.makedirs(os.path.join(data_dir, instance), exist_ok=True)
213+
214+
for data_dir in data_dirs:
215+
if os.path.exists(data_dir):
216+
for root, dirs, _ in os.walk(data_dir):
217+
for d in dirs:
218+
dpath = os.path.join(root, d)
219+
subprocess.run(
220+
["setfacl", "-d", "-m", "u::r,g::r,o::r", dpath],
221+
check=True,
222+
)
223+
224+
try:
225+
rc, out = tt.exec("start")
226+
assert rc != 0
227+
assert "permission denied" in out
228+
finally:
229+
for data_dir in data_dirs:
230+
if os.path.exists(data_dir):
231+
for root, dirs, _ in os.walk(data_dir):
232+
for d in dirs:
233+
dpath = os.path.join(root, d)
234+
subprocess.run(
235+
["setfacl", "-d", "-m", "u::rwx,g::rx,o::rx", dpath],
236+
check=False,
237+
)

0 commit comments

Comments
 (0)