diff --git a/cmd/attack/attack.go b/cmd/attack/attack.go index 398c3f70..9c2d2a69 100644 --- a/cmd/attack/attack.go +++ b/cmd/attack/attack.go @@ -35,6 +35,7 @@ func NewAttackCommand() *cobra.Command { NewDiskAttackCommand(&uid), NewHostAttackCommand(&uid), NewJVMAttackCommand(&uid), + NewClockAttackCommand(&uid), ) return cmd diff --git a/cmd/attack/clock.go b/cmd/attack/clock.go new file mode 100644 index 00000000..8d88742d --- /dev/null +++ b/cmd/attack/clock.go @@ -0,0 +1,68 @@ +// Copyright 2021 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package attack + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.uber.org/fx" + + "github.com/chaos-mesh/chaosd/cmd/server" + "github.com/chaos-mesh/chaosd/pkg/core" + "github.com/chaos-mesh/chaosd/pkg/server/chaosd" + "github.com/chaos-mesh/chaosd/pkg/utils" +) + +func NewClockAttackCommand(uid *string) *cobra.Command { + options := core.NewClockOption() + dep := fx.Options( + server.Module, + fx.Provide(func() *core.ClockOption { + options.UID = *uid + return options + }), + ) + + cmd := &cobra.Command{ + Use: "clock attack", + Short: "clock skew", + Run: func(*cobra.Command, []string) { + options.Action = "Attack" + utils.FxNewAppWithoutLog(dep, fx.Invoke(processClockAttack)).Run() + }, + } + + cmd.Flags().IntVarP(&options.Pid, "pid", "p", 0, "Pid of target program.") + cmd.Flags().StringVarP(&options.TimeOffset, "time-offset", "t", "", "Specifies the length of time offset.") + cmd.Flags().StringVarP(&options.ClockIdsSlice, "clock-ids-slice", "c", "CLOCK_REALTIME", + "The identifier of the particular clock on which to act."+ + "More clock description in linux kernel can be found in man page of clock_getres, clock_gettime, clock_settime."+ + "Muti clock ids should be split with \",\"") + return cmd +} + +func processClockAttack(options *core.ClockOption, chaos *chaosd.Server) { + err := options.PreProcess() + if err != nil { + utils.ExitWithError(utils.ExitBadArgs, err) + } + + uid, err := chaos.ExecuteAttack(chaosd.ClockAttack, options, core.CommandMode) + if err != nil { + utils.ExitWithError(utils.ExitError, err) + } + + utils.NormalExit(fmt.Sprintf("Clock attack %v successfully, uid: %s", options, uid)) +} diff --git a/pkg/core/clock.go b/pkg/core/clock.go new file mode 100644 index 00000000..856f7537 --- /dev/null +++ b/pkg/core/clock.go @@ -0,0 +1,112 @@ +// Copyright 2021 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "syscall" + "time" + + "github.com/pingcap/log" + "go.uber.org/zap" + + "github.com/chaos-mesh/chaos-mesh/pkg/time/utils" +) + +type ClockOption struct { + CommonAttackConfig + + Pid int + + TimeOffset string + SecDelta int64 + NsecDelta int64 + + ClockIdsSlice string + + Store ClockFuncStore + + ClockIdsMask uint64 +} + +type ClockFuncStore struct { + CodeOfGetClockFunc []byte + OriginAddress uint64 +} + +func NewClockOption() *ClockOption { + return &ClockOption{ + CommonAttackConfig: CommonAttackConfig{ + Kind: ClockAttack, + }, + } +} + +func (opt *ClockOption) PreProcess() error { + clkIds := strings.Split(opt.ClockIdsSlice, ",") + + offset, err := time.ParseDuration(opt.TimeOffset) + if err != nil { + return err + } + opt.SecDelta = int64(offset / time.Second) + opt.NsecDelta = int64(offset % time.Second) + + clockIdsMask, err := utils.EncodeClkIds(clkIds) + if err != nil { + log.Error("error while converting clock ids to mask", zap.Error(err)) + return err + } + if clockIdsMask == 0 { + log.Error("clock ids must not be empty") + return fmt.Errorf("clock ids must not be empty") + } + opt.ClockIdsMask = clockIdsMask + + if uint64(opt.SecDelta) > 1<<31 { + log.Warn("Monotonic clock will be broken when sec delta is too large or too small.") + if uint64(opt.SecDelta) > 1<<56 { + log.Warn("Time zone info will be broken when sec delta is too large or too small.") + } + } + + if uint64(opt.NsecDelta) > 1<<56 { + log.Warn("Time will be broken when nanosecond delta is too large or too small") + } + + // Since os.FindProcess in unix systems will always succeed + // regardless of whether the process exists (https://pkg.go.dev/os#FindProcess), + // we need to use process.Signal to check if pid is accessible. + process, err := os.FindProcess(opt.Pid) + if err != nil { + log.Error("failed to find process", zap.Error(err)) + return err + } + + err = process.Signal(syscall.Signal(0)) + if err != nil { + log.Error("pid may not be accessible", zap.Error(err)) + return err + } + return nil +} + +func (opt ClockOption) RecoverData() string { + data, _ := json.Marshal(opt) + + return string(data) +} diff --git a/pkg/core/experiment.go b/pkg/core/experiment.go index ddef1fcb..fdfcd54d 100644 --- a/pkg/core/experiment.go +++ b/pkg/core/experiment.go @@ -35,6 +35,7 @@ const ( NetworkAttack = "network" StressAttack = "stress" DiskAttack = "disk" + ClockAttack = "clock" HostAttack = "host" JVMAttack = "jvm" ) @@ -104,6 +105,8 @@ func GetAttackByKind(kind string) *AttackConfig { attackConfig = &DiskAttackConfig{} case JVMAttack: attackConfig = &JVMCommand{} + case ClockAttack: + attackConfig = &ClockOption{} default: return nil } diff --git a/pkg/server/chaosd/attack.go b/pkg/server/chaosd/attack.go index 839e6916..98e9536d 100644 --- a/pkg/server/chaosd/attack.go +++ b/pkg/server/chaosd/attack.go @@ -30,7 +30,14 @@ type Environment struct { } type AttackType interface { + // Attack execute attack with options and env. + // ExecuteAttack will store the options ahead of Attack be executed + // and will store options again after Attack be executed. + // We can also use env.Chaos.expStore to touch the storage of chaosd. + // But do not update it with your own uid , + // because it will be covered after Attack executed with options. Attack(options core.AttackConfig, env Environment) error + // Recover can get marshaled options data from experiment and recover it. Recover(experiment core.Experiment, env Environment) error } @@ -59,25 +66,26 @@ func (s *Server) ExecuteAttack(attackType AttackType, options core.AttackConfig, RecoverCommand: options.RecoverData(), LaunchMode: launchMode, } - if err = s.exp.Set(context.Background(), exp); err != nil { + if err = s.expStore.Set(context.Background(), exp); err != nil { err = perr.WithStack(err) return } defer func() { if err != nil { - if err := s.exp.Update(context.Background(), uid, core.Error, err.Error(), options.RecoverData()); err != nil { + if err := s.expStore.Update(context.Background(), uid, core.Error, err.Error(), options.RecoverData()); err != nil { log.Error("failed to update experiment", zap.Error(err)) } return } + var newStatus string if len(options.Cron()) > 0 { newStatus = core.Scheduled } else { newStatus = core.Success } - if err := s.exp.Update(context.Background(), uid, newStatus, "", options.RecoverData()); err != nil { + if err := s.expStore.Update(context.Background(), uid, newStatus, "", options.RecoverData()); err != nil { log.Error("failed to update experiment", zap.Error(err)) } }() diff --git a/pkg/server/chaosd/clock.go b/pkg/server/chaosd/clock.go new file mode 100644 index 00000000..432a8cc1 --- /dev/null +++ b/pkg/server/chaosd/clock.go @@ -0,0 +1,273 @@ +// Copyright 2021 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package chaosd + +import ( + "bytes" + "debug/elf" + "fmt" + "runtime" + + "github.com/chaos-mesh/chaos-mesh/pkg/mapreader" + "github.com/chaos-mesh/chaos-mesh/pkg/ptrace" + "github.com/pingcap/log" + "go.uber.org/zap" + + "github.com/chaos-mesh/chaosd/pkg/core" +) + +type clockAttack struct{} + +var ClockAttack AttackType = clockAttack{} + +// Copied from chaos-mesh/pkg/time/time_linux_amd64, +// I will move the recover part into it just future. +var fakeImage = []byte{ + 0xb8, 0xe4, 0x00, 0x00, 0x00, //mov $0xe4,%eax + 0x0f, 0x05, //syscall + 0xba, 0x01, 0x00, 0x00, 0x00, //mov $0x1,%edx + 0x89, 0xf9, //mov %edi,%ecx + 0xd3, 0xe2, //shl %cl,%edx + 0x48, 0x8d, 0x0d, 0x74, 0x00, 0x00, 0x00, //lea 0x74(%rip),%rcx # + 0x48, 0x63, 0xd2, //movslq %edx,%rdx + 0x48, 0x85, 0x11, //test %rdx,(%rcx) + 0x74, 0x6b, //je 108a + 0x48, 0x8d, 0x15, 0x6d, 0x00, 0x00, 0x00, //lea 0x6d(%rip),%rdx # + 0x4c, 0x8b, 0x46, 0x08, //mov 0x8(%rsi),%r8 + 0x48, 0x8b, 0x0a, //mov (%rdx),%rcx + 0x48, 0x8d, 0x15, 0x67, 0x00, 0x00, 0x00, //lea 0x67(%rip),%rdx # + 0x48, 0x8b, 0x3a, //mov (%rdx),%rdi + 0x4a, 0x8d, 0x14, 0x07, //lea (%rdi,%r8,1),%rdx + 0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp $0x3b9aca00,%rdx + 0x7e, 0x1c, //jle + 0x0f, 0x1f, 0x40, 0x00, //nopl 0x0(%rax) + 0x48, 0x81, 0xef, 0x00, 0xca, 0x9a, 0x3b, //sub $0x3b9aca00,%rdi + 0x48, 0x83, 0xc1, 0x01, //add $0x1,%rcx + 0x49, 0x8d, 0x14, 0x38, //lea (%r8,%rdi,1),%rdx + 0x48, 0x81, 0xfa, 0x00, 0xca, 0x9a, 0x3b, //cmp $0x3b9aca00,%rdx + 0x7f, 0xe8, //jg + 0x48, 0x85, 0xd2, //test %rdx,%rdx + 0x79, 0x1e, //jns + 0x4a, 0x8d, 0xbc, 0x07, 0x00, 0xca, 0x9a, //lea 0x3b9aca00(%rdi,%r8,1),%rdi + 0x3b, // + 0x0f, 0x1f, 0x00, //nopl (%rax) + 0x48, 0x89, 0xfa, //mov %rdi,%rdx + 0x48, 0x83, 0xe9, 0x01, //sub $0x1,%rcx + 0x48, 0x81, 0xc7, 0x00, 0xca, 0x9a, 0x3b, //add $0x3b9aca00,%rdi + 0x48, 0x85, 0xd2, //test %rdx,%rdx + 0x78, 0xed, //js + 0x48, 0x01, 0x0e, //add %rcx,(%rsi) + 0x48, 0x89, 0x56, 0x08, //mov %rdx,0x8(%rsi) + 0xc3, //retq + // constant + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //CLOCK_IDS_MASK + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_SEC_DELTA + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //TV_NSEC_DELTA +} + +func (c clockAttack) Attack(options core.AttackConfig, env Environment) error { + var opt *core.ClockOption + var ok bool + if opt, ok = options.(*core.ClockOption); !ok { + return fmt.Errorf("AttackConfig -> *ClockOption meet error") + } + + runtime.LockOSThread() + defer func() { + runtime.UnlockOSThread() + }() + program, err := ptrace.Trace(opt.Pid) + if err != nil { + return err + } + defer func() { + err = program.Detach() + if err != nil { + log.Error("fail to detach program", zap.Error(err), zap.Int("pid", opt.Pid)) + } + }() + + var vdsoEntry *mapreader.Entry + for index := range program.Entries { + // reverse loop is faster + e := program.Entries[len(program.Entries)-index-1] + if e.Path == "[vdso]" { + vdsoEntry = &e + break + } + } + if vdsoEntry == nil { + return fmt.Errorf("cannot find [vdso] entry") + } + + // minus tailing variable part + // 24 = 3 * 8 because we have three variables + constImageLen := len(fakeImage) - 24 + var fakeEntry *mapreader.Entry + + // find injected image to avoid redundant inject (which will lead to memory leak) + for _, e := range program.Entries { + e := e + + image, err := program.ReadSlice(e.StartAddress, uint64(constImageLen)) + if err != nil { + continue + } + + if bytes.Equal(*image, fakeImage[0:constImageLen]) { + fakeEntry = &e + log.Warn("found injected image", zap.Uint64("addr", fakeEntry.StartAddress)) + } + } + + if fakeEntry == nil { + fakeEntry, err = program.MmapSlice(fakeImage) + if err != nil { + return err + } + } + fakeAddr := fakeEntry.StartAddress + + // 139 is the index of CLOCK_IDS_MASK in fakeImage + err = program.WriteUint64ToAddr(fakeAddr+139, opt.ClockIdsMask) + if err != nil { + return err + } + + // 147 is the index of TV_SEC_DELTA in fakeImage + err = program.WriteUint64ToAddr(fakeAddr+147, uint64(opt.SecDelta)) + if err != nil { + return err + } + + // 155 is the index of TV_NSEC_DELTA in fakeImage + err = program.WriteUint64ToAddr(fakeAddr+155, uint64(opt.NsecDelta)) + if err != nil { + return err + } + + originAddr, size, err := FindSymbolInEntry(*program, "clock_gettime", vdsoEntry) + if err != nil { + return err + } + + funcBytes, err := program.ReadSlice(originAddr, size) + + exps, err := env.Chaos.Search(&core.SearchCommand{ + Status: core.Success, + Kind: core.ClockAttack, + }) + if err != nil { + return err + } + + for _, exp := range exps { + if exp.Kind == core.ClockAttack { + lastOptions, err := exp.GetRequestCommand() + if err != nil { + return err + } + + var lastOpt *core.ClockOption + var ok bool + if lastOpt, ok = lastOptions.(*core.ClockOption); !ok { + log.Warn("AttackConfig -> *ClockOption meet error") + continue + } + if lastOpt.Pid == opt.Pid { + return fmt.Errorf("plz recover the last clock attack on pid : %d first \n"+ + "chaosd recover %s", opt.Pid, exp.Uid) + } + } + } + + opt.Store = core.ClockFuncStore{ + CodeOfGetClockFunc: *funcBytes, + OriginAddress: originAddr, + } + if err != nil { + return err + } + + err = program.JumpToFakeFunc(originAddr, fakeAddr) + return err +} + +func (c clockAttack) Recover(exp core.Experiment, env Environment) error { + options, err := exp.GetRequestCommand() + if err != nil { + return err + } + + var opt *core.ClockOption + var ok bool + if opt, ok = options.(*core.ClockOption); !ok { + return fmt.Errorf("AttackConfig -> *ClockOption meet error") + } + + runtime.LockOSThread() + defer func() { + runtime.UnlockOSThread() + }() + program, err := ptrace.Trace(opt.Pid) + if err != nil { + return err + } + defer func() { + err = program.Detach() + if err != nil { + log.Error("fail to detach program", zap.Error(err), zap.Int("pid", opt.Pid)) + } + }() + + err = program.PtraceWriteSlice(opt.Store.OriginAddress, opt.Store.CodeOfGetClockFunc) + return err +} + +// FindSymbolInEntry finds symbol in entry through parsing elf +func FindSymbolInEntry(p ptrace.TracedProgram, symbolName string, entry *mapreader.Entry) (uint64, uint64, error) { + libBuffer, err := p.GetLibBuffer(entry) + if err != nil { + return 0, 0, err + } + + reader := bytes.NewReader(*libBuffer) + vdsoElf, err := elf.NewFile(reader) + if err != nil { + return 0, 0, err + } + + loadOffset := uint64(0) + + for _, prog := range vdsoElf.Progs { + if prog.Type == elf.PT_LOAD { + loadOffset = prog.Vaddr - prog.Off + + // break here is enough for vdso + break + } + } + + symbols, err := vdsoElf.DynamicSymbols() + if err != nil { + return 0, 0, err + } + for _, symbol := range symbols { + if symbol.Name == symbolName { + offset := symbol.Value + return entry.StartAddress + (offset - loadOffset), symbol.Size, nil + } + } + return 0, 0, fmt.Errorf("cannot find symbol") +} diff --git a/pkg/server/chaosd/recover.go b/pkg/server/chaosd/recover.go index a465a4e7..dda61b35 100644 --- a/pkg/server/chaosd/recover.go +++ b/pkg/server/chaosd/recover.go @@ -25,7 +25,7 @@ import ( ) func (s *Server) RecoverAttack(uid string) error { - exp, err := s.exp.FindByUid(context.Background(), uid) + exp, err := s.expStore.FindByUid(context.Background(), uid) if err != nil { return err } @@ -63,6 +63,8 @@ func (s *Server) RecoverAttack(uid string) error { attackType = DiskAttack case core.JVMAttack: attackType = JVMAttack + case core.ClockAttack: + attackType = ClockAttack default: return perr.Errorf("chaos experiment kind %s not found", exp.Kind) } @@ -77,7 +79,7 @@ func (s *Server) RecoverAttack(uid string) error { } } - if err := s.exp.Update(context.Background(), uid, core.Destroyed, "", exp.RecoverCommand); err != nil { + if err := s.expStore.Update(context.Background(), uid, core.Destroyed, "", exp.RecoverCommand); err != nil { return perr.WithStack(err) } return nil diff --git a/pkg/server/chaosd/search.go b/pkg/server/chaosd/search.go index 6d6466c2..f0c5ff14 100644 --- a/pkg/server/chaosd/search.go +++ b/pkg/server/chaosd/search.go @@ -23,7 +23,7 @@ import ( func (s *Server) Search(conds *core.SearchCommand) ([]*core.Experiment, error) { if len(conds.UID) > 0 { - exp, err := s.exp.FindByUid(context.Background(), conds.UID) + exp, err := s.expStore.FindByUid(context.Background(), conds.UID) if err != nil { return nil, errors.WithStack(err) } @@ -31,7 +31,7 @@ func (s *Server) Search(conds *core.SearchCommand) ([]*core.Experiment, error) { return []*core.Experiment{exp}, nil } - exps, err := s.exp.ListByConditions(context.Background(), conds) + exps, err := s.expStore.ListByConditions(context.Background(), conds) if err != nil { return nil, errors.WithStack(err) } diff --git a/pkg/server/chaosd/server.go b/pkg/server/chaosd/server.go index 6e3d1a04..4b127fef 100644 --- a/pkg/server/chaosd/server.go +++ b/pkg/server/chaosd/server.go @@ -22,7 +22,7 @@ import ( ) type Server struct { - exp core.ExperimentStore + expStore core.ExperimentStore ExpRun core.ExperimentRunStore Cron scheduler.Scheduler ipsetRule core.IPSetRuleStore @@ -44,7 +44,7 @@ func NewServer( ) *Server { return &Server{ conf: conf, - exp: exp, + expStore: exp, Cron: cron, ExpRun: expRun, ipsetRule: ipset, diff --git a/pkg/server/httpserver/server.go b/pkg/server/httpserver/server.go index 66d376e6..4c87edbf 100644 --- a/pkg/server/httpserver/server.go +++ b/pkg/server/httpserver/server.go @@ -83,6 +83,7 @@ func handler(s *httpServer) { attack.POST("/stress", s.createStressAttack) attack.POST("/network", s.createNetworkAttack) attack.POST("/disk", s.createDiskAttack) + attack.POST("/clock", s.createClockAttack) attack.DELETE("/:uid", s.recoverAttack) } @@ -225,6 +226,37 @@ func (s *httpServer) createDiskAttack(c *gin.Context) { c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid)) } +// @Summary Create clock attack. +// @Description Create clock attack. +// @Tags attack +// @Produce json +// @Param request body core.ClockOption true "Request body" +// @Success 200 {object} utils.Response +// @Failure 400 {object} utils.APIError +// @Failure 500 {object} utils.APIError +// @Router /api/attack/clock [post] +func (s *httpServer) createClockAttack(c *gin.Context) { + options := core.NewClockOption() + if err := c.ShouldBindJSON(options); err != nil { + c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err)) + return + } + + err := options.PreProcess() + if err != nil { + err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed") + handleError(c, err) + return + } + uid, err := s.chaos.ExecuteAttack(chaosd.ClockAttack, options, core.CommandMode) + if err != nil { + handleError(c, err) + return + } + + c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid)) +} + // @Summary Create recover attack. // @Description Create recover attack. // @Tags attack diff --git a/pkg/store/experiment/experiment.go b/pkg/store/experiment/experiment.go index cab04d4b..5d79e204 100644 --- a/pkg/store/experiment/experiment.go +++ b/pkg/store/experiment/experiment.go @@ -17,9 +17,8 @@ import ( "context" "errors" - "gorm.io/gorm" - perr "github.com/pkg/errors" + "gorm.io/gorm" "github.com/chaos-mesh/chaosd/pkg/core" "github.com/chaos-mesh/chaosd/pkg/store/dbstore"