Skip to content

Commit 72a948b

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 72a948b

5 files changed

Lines changed: 167 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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import pytest
24
import tt_helper
35

@@ -109,3 +111,76 @@ def test_start_multi_inst(tt, tt_app, target):
109111
)
110112
def test_start_cluster(tt, tt_app, target):
111113
check_start(tt, tt_app, target)
114+
115+
116+
################################################################
117+
# Permission denied
118+
119+
120+
@pytest.mark.skipif(skip_cluster_cond, reason=skip_cluster_reason)
121+
@pytest.mark.skipif(
122+
os.getuid() == 0,
123+
reason="Skipping the test, it shouldn't run as root",
124+
)
125+
@pytest.mark.tt_app(**tt_cluster_app)
126+
@pytest.mark.parametrize(
127+
"tt_running_targets",
128+
[
129+
pytest.param([], id="running:none"),
130+
],
131+
)
132+
def test_start_permission_denied(tt, tt_app):
133+
var_dir = tt_app.path("var")
134+
os.makedirs(var_dir, exist_ok=True)
135+
os.chmod(var_dir, 0o444)
136+
137+
try:
138+
rc, out = tt.exec("start")
139+
assert rc != 0
140+
assert "permission denied" in out.lower()
141+
finally:
142+
os.chmod(var_dir, 0o755)
143+
144+
145+
@pytest.mark.skipif(skip_cluster_cond, reason=skip_cluster_reason)
146+
@pytest.mark.skipif(
147+
os.getuid() == 0,
148+
reason="Skipping the test, it shouldn't run as root",
149+
)
150+
@pytest.mark.tt_app(**tt_cluster_app)
151+
@pytest.mark.parametrize(
152+
"tt_running_targets",
153+
[
154+
pytest.param([], id="running:none"),
155+
],
156+
)
157+
def test_start_permission_denied_permissions_denied_files(tt, tt_app):
158+
rc, _ = tt.exec("start")
159+
assert rc == 0
160+
assert utils.wait_files(5, tt_helper.pid_files(tt_app, tt_app.instances))
161+
162+
rc, _ = tt.exec("stop", "-y")
163+
assert rc == 0
164+
165+
log_dir = tt_app.path("var", "log")
166+
lib_dir = tt_app.path("var", "lib")
167+
run_dir = tt_app.path("var", "run")
168+
169+
dirs_to_lock = []
170+
for data_dir in [log_dir, lib_dir, run_dir]:
171+
for root, dirs, files in os.walk(data_dir, topdown=False):
172+
dirs_to_lock.extend(os.path.join(root, f) for f in files)
173+
dirs_to_lock.extend(os.path.join(root, d) for d in dirs)
174+
dirs_to_lock.append(data_dir)
175+
176+
for d in dirs_to_lock:
177+
os.chmod(d, 0o444)
178+
179+
try:
180+
rc, out = tt.exec("start")
181+
assert rc != 0
182+
assert "permission denied" in out.lower()
183+
finally:
184+
for d in dirs_to_lock:
185+
if os.path.exists(d):
186+
os.chmod(d, 0o755)

0 commit comments

Comments
 (0)