Skip to content
34 changes: 26 additions & 8 deletions cmd/tidb-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1199,14 +1199,32 @@ func enablePyroscope() {
func setupSEM() {
cfg := config.GetGlobalConfig()

if cfg.Security.EnableSEM {
if cfg.Security.SEMConfig != "" {
err := semv2.Enable(cfg.Security.SEMConfig)
if err != nil {
logutil.BgLogger().Fatal("failed to enable SEM", zap.Error(err))
}
} else {
sem.Enable()
// Strict SEM is Starter-only and depends on semv2 being initialized
// (the hint allow-list reads sysvar visibility from semv2 state).
strictSEM := deploymode.IsStarter()

if !cfg.Security.EnableSEM {
if strictSEM {
logutil.BgLogger().Warn("Starter deployment mode requires security.enable-sem=true to enable strict SEM; ignoring")
}
return
}

if cfg.Security.SEMConfig != "" {
err := semv2.Enable(cfg.Security.SEMConfig)
if err != nil {
logutil.BgLogger().Fatal("failed to enable SEM", zap.Error(err))
}
} else {
if strictSEM {
logutil.BgLogger().Warn("Starter deployment mode requires security.sem-config to enable strict SEM; ignoring")
strictSEM = false
}
sem.Enable()
}

if strictSEM {
semv2.EnableStrict()
logutil.BgLogger().Info("strict SEM enabled for Starter deployment mode")
}
}
1 change: 1 addition & 0 deletions pkg/planner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ go_library(
"//pkg/util/hint",
"//pkg/util/intest",
"//pkg/util/logutil",
"//pkg/util/sem/v2:sem",
"//pkg/util/topsql",
"//pkg/util/tracing",
"@com_github_pingcap_errors//:errors",
Expand Down
7 changes: 7 additions & 0 deletions pkg/planner/core/planbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6397,6 +6397,13 @@ func GetMaxWriteSpeedFromExpression(opt *AlterDDLJobOpt) (maxWriteSpeed int64, e
}

func (b *PlanBuilder) checkSEMStmt(stmt ast.Node) error {
// Strict SEM applies before the semv2 rule check with no admin bypass.
if semv2.IsStrictEnabled() {
if err := semv2.IsRestrictedStatement(stmt); err != nil {
return err
}
}

if !semv2.IsEnabled() {
return nil
}
Expand Down
30 changes: 29 additions & 1 deletion pkg/planner/optimize.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/pingcap/tidb/pkg/util/hint"
"github.com/pingcap/tidb/pkg/util/intest"
"github.com/pingcap/tidb/pkg/util/logutil"
semv2 "github.com/pingcap/tidb/pkg/util/sem/v2"
"github.com/pingcap/tidb/pkg/util/topsql"
"github.com/pingcap/tidb/pkg/util/tracing"
"go.uber.org/zap"
Expand Down Expand Up @@ -230,6 +231,10 @@ func optimizeNoCache(ctx context.Context, sctx sessionctx.Context, node *resolve
sessVars := sctx.GetSessionVars()

tableHints := hint.ExtractTableHintsFromStmtNode(node.Node, sessVars.StmtCtx)
tableHints, restrictedHintWarns := filterRestrictedHints(tableHints)
for _, warn := range restrictedHintWarns {
sessVars.StmtCtx.AppendWarning(warn)
}
originStmtHints, _, warns := hint.ParseStmtHints(tableHints,
setVarHintChecker, hypoIndexChecker(ctx, is),
sessVars.CurrentDB, byte(kv.ReplicaReadFollower))
Expand Down Expand Up @@ -301,7 +306,11 @@ func optimizeNoCache(ctx context.Context, sctx sessionctx.Context, node *resolve
var warns []error
if binding != nil && binding.IsBindingEnabled() {
hint.BindHint(stmtNode, binding.Hint)
curStmtHints, _, curWarns := hint.ParseStmtHints(binding.Hint.GetStmtHints(),
bindingHints, bindingRestrictedWarns := filterRestrictedHints(binding.Hint.GetStmtHints())
for _, warn := range bindingRestrictedWarns {
sessVars.StmtCtx.AppendWarning(warn)
}
curStmtHints, _, curWarns := hint.ParseStmtHints(bindingHints,
setVarHintChecker, hypoIndexChecker(ctx, is),
sessVars.CurrentDB, byte(kv.ReplicaReadFollower))
sessVars.StmtCtx.StmtHints = curStmtHints
Expand Down Expand Up @@ -820,6 +829,25 @@ func buildLogicalPlan(ctx context.Context, sctx planctx.PlanContext, node *resol
return p, nil
}

// filterRestrictedHints returns hints with strict-SEM-forbidden entries
// stripped and a warning per strip. When strict is off it returns the input
// slice unchanged.
func filterRestrictedHints(hints []*ast.TableOptimizerHint) ([]*ast.TableOptimizerHint, []error) {
if !semv2.IsStrictEnabled() || len(hints) == 0 {
return hints, nil
}
filtered := make([]*ast.TableOptimizerHint, 0, len(hints))
var warns []error
for _, h := range hints {
if err := semv2.IsRestrictedHint(h.HintName.L); err != nil {
warns = append(warns, err)
continue
}
filtered = append(filtered, h)
}
return filtered, warns
}

// setVarHintChecker checks whether the variable name in set_var hint is valid.
func setVarHintChecker(varName, hint string) (ok bool, warning error) {
sysVar := variable.GetSysVar(varName)
Expand Down
1 change: 1 addition & 0 deletions pkg/session/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ go_library(
"//pkg/util/ranger/context",
"//pkg/util/redact",
"//pkg/util/sem/compat",
"//pkg/util/sem/v2:sem",
"//pkg/util/sli",
"//pkg/util/sqlescape",
"//pkg/util/sqlexec",
Expand Down
5 changes: 5 additions & 0 deletions pkg/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import (
rangerctx "github.com/pingcap/tidb/pkg/util/ranger/context"
"github.com/pingcap/tidb/pkg/util/redact"
sem "github.com/pingcap/tidb/pkg/util/sem/compat"
semv2 "github.com/pingcap/tidb/pkg/util/sem/v2"
"github.com/pingcap/tidb/pkg/util/sli"
"github.com/pingcap/tidb/pkg/util/sqlescape"
"github.com/pingcap/tidb/pkg/util/sqlexec"
Expand Down Expand Up @@ -5641,6 +5642,10 @@ func (s *session) usePipelinedDmlOrWarn(ctx context.Context) bool {
if stmtCtx.IsReadOnly {
return false
}
if semv2.IsStrictEnabled() {
stmtCtx.AppendWarning(errors.New("Pipelined DML is not supported in this deployment. Fallback to standard mode"))
return false
}
vars := s.GetSessionVars()
if !vars.TxnCtx.EnableMDL {
stmtCtx.AppendWarning(
Expand Down
8 changes: 8 additions & 0 deletions pkg/util/sem/v2/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ go_library(
name = "sem",
srcs = [
"config.go",
"restricted_hint.go",
"restricted_statement.go",
"sem.go",
"sql_rule.go",
"strict.go",
"testhelper.go",
],
importpath = "github.com/pingcap/tidb/pkg/util/sem/v2",
Expand All @@ -17,9 +20,11 @@ go_library(
"//pkg/parser/mysql",
"//pkg/sessionctx/vardef",
"//pkg/sessionctx/variable",
"//pkg/util/dbterror/plannererrors",
"//pkg/util/intest",
"//pkg/util/logutil",
"@com_github_coreos_go_semver//semver",
"@com_github_pingcap_errors//:errors",
"@org_uber_go_zap//:zap",
],
)
Expand All @@ -29,13 +34,16 @@ go_test(
timeout = "short",
srcs = [
"config_test.go",
"restricted_hint_test.go",
"restricted_statement_test.go",
"sem_test.go",
"sql_rule_test.go",
],
embed = [":sem"],
flaky = True,
deps = [
"//pkg/parser",
"//pkg/parser/ast",
"//pkg/parser/charset",
"//pkg/parser/mysql",
"//pkg/sessionctx/vardef",
Expand Down
46 changes: 46 additions & 0 deletions pkg/util/sem/v2/restricted_hint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2026 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sem

import (
"github.com/pingcap/errors"
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
)

// IsRestrictedHint returns a non-nil error when strict SEM forbids the hint.
// memory_quota/read_consistent_replica/max_execution_time piggyback on sysvar
// visibility; resource_group is rejected outright (unavailable in starter).
func IsRestrictedHint(hintNameLower string) error {
if !IsStrictEnabled() {
return nil
}
switch hintNameLower {
case "memory_quota":
if IsInvisibleSysVar(vardef.TiDBMemQuotaQuery) || IsReadOnlyVariable(vardef.TiDBMemQuotaQuery) {
return errors.New("MEMORY_QUOTA() is not supported when strict SEM is enabled")
}
case "resource_group":
return errors.New("RESOURCE_GROUP() is not supported when strict SEM is enabled")
case "read_consistent_replica":
if IsInvisibleSysVar(vardef.TiDBReplicaRead) || IsReadOnlyVariable(vardef.TiDBReplicaRead) {
return errors.New("READ_CONSISTENT_REPLICA() is not supported when strict SEM is enabled")
}
case "max_execution_time":
if IsInvisibleSysVar(vardef.MaxExecutionTime) || IsReadOnlyVariable(vardef.MaxExecutionTime) {
return errors.New("MAX_EXECUTION_TIME() is not supported when strict SEM is enabled")
}
}
return nil
}
84 changes: 84 additions & 0 deletions pkg/util/sem/v2/restricted_hint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sem

import (
"testing"

"github.com/pingcap/tidb/pkg/sessionctx/vardef"
"github.com/stretchr/testify/require"
)

func TestHint_StrictDisabled(t *testing.T) {
// Without strict SEM, all hints pass — including resource_group, since
// the helper self-gates on IsStrictEnabled.
require.NoError(t, IsRestrictedHint("memory_quota"))
require.NoError(t, IsRestrictedHint("read_consistent_replica"))
require.NoError(t, IsRestrictedHint("max_execution_time"))
require.NoError(t, IsRestrictedHint("resource_group"))
require.NoError(t, IsRestrictedHint("use_index"))
require.NoError(t, IsRestrictedHint(""))
}

func TestHint_StrictWithSEMHidden(t *testing.T) {
EnableStrict()
t.Cleanup(DisableStrict)
t.Cleanup(Disable)
require.NoError(t, EnableBy(&Config{
TiDBVersion: "v0.0.0",
RestrictedVariables: []VariableRestriction{
{Name: vardef.TiDBMemQuotaQuery, Hidden: true},
{Name: vardef.TiDBReplicaRead, Hidden: true},
{Name: vardef.MaxExecutionTime, Hidden: true},
},
}))

require.Error(t, IsRestrictedHint("memory_quota"))
require.Error(t, IsRestrictedHint("read_consistent_replica"))
require.Error(t, IsRestrictedHint("max_execution_time"))
require.Error(t, IsRestrictedHint("resource_group"))
require.NoError(t, IsRestrictedHint("use_index"))
}

func TestHint_StrictWithSEMReadonly(t *testing.T) {
EnableStrict()
t.Cleanup(DisableStrict)
t.Cleanup(Disable)
require.NoError(t, EnableBy(&Config{
TiDBVersion: "v0.0.0",
RestrictedVariables: []VariableRestriction{
{Name: vardef.TiDBMemQuotaQuery, Readonly: true},
{Name: vardef.TiDBReplicaRead, Readonly: true},
{Name: vardef.MaxExecutionTime, Readonly: true},
},
}))

require.Error(t, IsRestrictedHint("memory_quota"))
require.Error(t, IsRestrictedHint("read_consistent_replica"))
require.Error(t, IsRestrictedHint("max_execution_time"))
}

func TestHint_StrictWithSEMUnrestricted(t *testing.T) {
EnableStrict()
t.Cleanup(DisableStrict)
t.Cleanup(Disable)
require.NoError(t, EnableBy(&Config{TiDBVersion: "v0.0.0"}))

// Without sysvar restrictions, only resource_group is rejected.
require.NoError(t, IsRestrictedHint("memory_quota"))
require.NoError(t, IsRestrictedHint("read_consistent_replica"))
require.NoError(t, IsRestrictedHint("max_execution_time"))
require.Error(t, IsRestrictedHint("resource_group"))
}
Loading
Loading