mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Aggregate arguments for loading executables into a single struct.
This change simplifies the function signatures of functions related to loading executables, such as LoadTaskImage, Load, loadBinary. PiperOrigin-RevId: 276821187
This commit is contained in:
@@ -804,8 +804,20 @@ func (k *Kernel) CreateProcess(args CreateProcessArgs) (*ThreadGroup, ThreadID,
|
||||
|
||||
// Create a fresh task context.
|
||||
remainingTraversals = uint(args.MaxSymlinkTraversals)
|
||||
loadArgs := loader.LoadArgs{
|
||||
Mounts: mounts,
|
||||
Root: root,
|
||||
WorkingDirectory: wd,
|
||||
RemainingTraversals: &remainingTraversals,
|
||||
ResolveFinal: true,
|
||||
Filename: args.Filename,
|
||||
File: args.File,
|
||||
Argv: args.Argv,
|
||||
Envv: args.Envv,
|
||||
Features: k.featureSet,
|
||||
}
|
||||
|
||||
tc, se := k.LoadTaskImage(ctx, mounts, root, wd, &remainingTraversals, args.Filename, args.File, args.Argv, args.Envv, true /*resolveFinal*/, k.featureSet)
|
||||
tc, se := k.LoadTaskImage(ctx, loadArgs)
|
||||
if se != nil {
|
||||
return nil, 0, errors.New(se.String())
|
||||
}
|
||||
|
||||
@@ -18,10 +18,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/abi/linux"
|
||||
"gvisor.dev/gvisor/pkg/cpuid"
|
||||
"gvisor.dev/gvisor/pkg/sentry/arch"
|
||||
"gvisor.dev/gvisor/pkg/sentry/context"
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/futex"
|
||||
"gvisor.dev/gvisor/pkg/sentry/loader"
|
||||
"gvisor.dev/gvisor/pkg/sentry/mm"
|
||||
@@ -132,30 +130,21 @@ func (t *Task) Stack() *arch.Stack {
|
||||
return &arch.Stack{t.Arch(), t.MemoryManager(), usermem.Addr(t.Arch().Stack())}
|
||||
}
|
||||
|
||||
// LoadTaskImage loads filename into a new TaskContext.
|
||||
// LoadTaskImage loads a specified file into a new TaskContext.
|
||||
//
|
||||
// It takes several arguments:
|
||||
// * mounts: MountNamespace to lookup filename in
|
||||
// * root: Root to lookup filename under
|
||||
// * wd: Working directory to lookup filename under
|
||||
// * maxTraversals: maximum number of symlinks to follow
|
||||
// * filename: path to binary to load
|
||||
// * file: an open fs.File object of the binary to load. If set,
|
||||
// file will be loaded and not filename.
|
||||
// * argv: Binary argv
|
||||
// * envv: Binary envv
|
||||
// * fs: Binary FeatureSet
|
||||
func (k *Kernel) LoadTaskImage(ctx context.Context, mounts *fs.MountNamespace, root, wd *fs.Dirent, maxTraversals *uint, filename string, file *fs.File, argv, envv []string, resolveFinal bool, fs *cpuid.FeatureSet) (*TaskContext, *syserr.Error) {
|
||||
// If File is not nil, we should load that instead of resolving filename.
|
||||
if file != nil {
|
||||
filename = file.MappedName(ctx)
|
||||
// args.MemoryManager does not need to be set by the caller.
|
||||
func (k *Kernel) LoadTaskImage(ctx context.Context, args loader.LoadArgs) (*TaskContext, *syserr.Error) {
|
||||
// If File is not nil, we should load that instead of resolving Filename.
|
||||
if args.File != nil {
|
||||
args.Filename = args.File.MappedName(ctx)
|
||||
}
|
||||
|
||||
// Prepare a new user address space to load into.
|
||||
m := mm.NewMemoryManager(k, k)
|
||||
defer m.DecUsers(ctx)
|
||||
args.MemoryManager = m
|
||||
|
||||
os, ac, name, err := loader.Load(ctx, m, mounts, root, wd, maxTraversals, fs, filename, file, argv, envv, resolveFinal, k.extraAuxv, k.vdso)
|
||||
os, ac, name, err := loader.Load(ctx, args, k.extraAuxv, k.vdso)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -624,15 +624,15 @@ func loadInterpreterELF(ctx context.Context, m *mm.MemoryManager, f *fs.File, in
|
||||
return loadParsedELF(ctx, m, f, info, 0)
|
||||
}
|
||||
|
||||
// loadELF loads f into the Task address space.
|
||||
// loadELF loads args.File into the Task address space.
|
||||
//
|
||||
// If loadELF returns ErrSwitchFile it should be called again with the returned
|
||||
// path and argv.
|
||||
//
|
||||
// Preconditions:
|
||||
// * f is an ELF file
|
||||
func loadELF(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace, root, wd *fs.Dirent, maxTraversals *uint, fs *cpuid.FeatureSet, f *fs.File) (loadedELF, arch.Context, error) {
|
||||
bin, ac, err := loadInitialELF(ctx, m, fs, f)
|
||||
// * args.File is an ELF file
|
||||
func loadELF(ctx context.Context, args LoadArgs) (loadedELF, arch.Context, error) {
|
||||
bin, ac, err := loadInitialELF(ctx, args.MemoryManager, args.Features, args.File)
|
||||
if err != nil {
|
||||
ctx.Infof("Error loading binary: %v", err)
|
||||
return loadedELF{}, nil, err
|
||||
@@ -640,7 +640,12 @@ func loadELF(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace
|
||||
|
||||
var interp loadedELF
|
||||
if bin.interpreter != "" {
|
||||
d, i, err := openPath(ctx, mounts, root, wd, maxTraversals, bin.interpreter, true /*resolveFinal*/)
|
||||
// Even if we do not allow the final link of the script to be
|
||||
// resolved, the interpreter should still be resolved if it is
|
||||
// a symlink.
|
||||
args.ResolveFinal = true
|
||||
args.Filename = bin.interpreter
|
||||
d, i, err := openPath(ctx, args)
|
||||
if err != nil {
|
||||
ctx.Infof("Error opening interpreter %s: %v", bin.interpreter, err)
|
||||
return loadedELF{}, nil, err
|
||||
@@ -649,7 +654,7 @@ func loadELF(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace
|
||||
// We don't need the Dirent.
|
||||
d.DecRef()
|
||||
|
||||
interp, err = loadInterpreterELF(ctx, m, i, bin)
|
||||
interp, err = loadInterpreterELF(ctx, args.MemoryManager, i, bin)
|
||||
if err != nil {
|
||||
ctx.Infof("Error loading interpreter: %v", err)
|
||||
return loadedELF{}, nil, err
|
||||
|
||||
+87
-45
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package loader loads a binary into a MemoryManager.
|
||||
// Package loader loads an executable file into a MemoryManager.
|
||||
package loader
|
||||
|
||||
import (
|
||||
@@ -35,6 +35,48 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
)
|
||||
|
||||
// LoadArgs holds specifications for an executable file to be loaded.
|
||||
type LoadArgs struct {
|
||||
// MemoryManager is the memory manager to load the executable into.
|
||||
MemoryManager *mm.MemoryManager
|
||||
|
||||
// Mounts is the mount namespace in which to look up Filename.
|
||||
Mounts *fs.MountNamespace
|
||||
|
||||
// Root is the root directory under which to look up Filename.
|
||||
Root *fs.Dirent
|
||||
|
||||
// WorkingDirectory is the working directory under which to look up
|
||||
// Filename.
|
||||
WorkingDirectory *fs.Dirent
|
||||
|
||||
// RemainingTraversals is the maximum number of symlinks to follow to
|
||||
// resolve Filename. This counter is passed by reference to keep it
|
||||
// updated throughout the call stack.
|
||||
RemainingTraversals *uint
|
||||
|
||||
// ResolveFinal indicates whether the final link of Filename should be
|
||||
// resolved, if it is a symlink.
|
||||
ResolveFinal bool
|
||||
|
||||
// Filename is the path for the executable.
|
||||
Filename string
|
||||
|
||||
// File is an open fs.File object of the executable. If File is not
|
||||
// nil, then File will be loaded and Filename will be ignored.
|
||||
File *fs.File
|
||||
|
||||
// Argv is the vector of arguments to pass to the executable.
|
||||
Argv []string
|
||||
|
||||
// Envv is the vector of environment variables to pass to the
|
||||
// executable.
|
||||
Envv []string
|
||||
|
||||
// Features specifies the CPU feature set for the executable.
|
||||
Features *cpuid.FeatureSet
|
||||
}
|
||||
|
||||
// readFull behaves like io.ReadFull for an *fs.File.
|
||||
func readFull(ctx context.Context, f *fs.File, dst usermem.IOSequence, offset int64) (int64, error) {
|
||||
var total int64
|
||||
@@ -51,24 +93,24 @@ func readFull(ctx context.Context, f *fs.File, dst usermem.IOSequence, offset in
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// openPath opens name for loading.
|
||||
// openPath opens args.Filename for loading.
|
||||
//
|
||||
// openPath returns the fs.Dirent and an *fs.File for name, which is not
|
||||
// installed in the Task FDTable. The caller takes ownership of both.
|
||||
// openPath returns the fs.Dirent and an *fs.File for args.Filename, which is
|
||||
// not installed in the Task FDTable. The caller takes ownership of both.
|
||||
//
|
||||
// name must be a readable, executable, regular file.
|
||||
func openPath(ctx context.Context, mounts *fs.MountNamespace, root, wd *fs.Dirent, maxTraversals *uint, name string, resolveFinal bool) (*fs.Dirent, *fs.File, error) {
|
||||
// args.Filename must be a readable, executable, regular file.
|
||||
func openPath(ctx context.Context, args LoadArgs) (*fs.Dirent, *fs.File, error) {
|
||||
var err error
|
||||
if name == "" {
|
||||
if args.Filename == "" {
|
||||
ctx.Infof("cannot open empty name")
|
||||
return nil, nil, syserror.ENOENT
|
||||
}
|
||||
|
||||
var d *fs.Dirent
|
||||
if resolveFinal {
|
||||
d, err = mounts.FindInode(ctx, root, wd, name, maxTraversals)
|
||||
if args.ResolveFinal {
|
||||
d, err = args.Mounts.FindInode(ctx, args.Root, args.WorkingDirectory, args.Filename, args.RemainingTraversals)
|
||||
} else {
|
||||
d, err = mounts.FindLink(ctx, root, wd, name, maxTraversals)
|
||||
d, err = args.Mounts.FindLink(ctx, args.Root, args.WorkingDirectory, args.Filename, args.RemainingTraversals)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -77,11 +119,11 @@ func openPath(ctx context.Context, mounts *fs.MountNamespace, root, wd *fs.Diren
|
||||
// Open file will take a reference to Dirent, so destroy this one.
|
||||
defer d.DecRef()
|
||||
|
||||
if !resolveFinal && fs.IsSymlink(d.Inode.StableAttr) {
|
||||
if !args.ResolveFinal && fs.IsSymlink(d.Inode.StableAttr) {
|
||||
return nil, nil, syserror.ELOOP
|
||||
}
|
||||
|
||||
return openFile(ctx, nil, d, name)
|
||||
return openFile(ctx, nil, d, args.Filename)
|
||||
}
|
||||
|
||||
// openFile takes that file's Dirent and performs checks on it. If provided a
|
||||
@@ -182,34 +224,33 @@ const (
|
||||
maxLoaderAttempts = 6
|
||||
)
|
||||
|
||||
// loadBinary loads a binary that is pointed to by "file". If nil, the path
|
||||
// "filename" is resolved and loaded.
|
||||
// loadExecutable loads an executable that is pointed to by args.File. If nil,
|
||||
// the path args.Filename is resolved and loaded. If the executable is an
|
||||
// interpreter script rather than an ELF, the binary of the corresponding
|
||||
// interpreter will be loaded.
|
||||
//
|
||||
// It returns:
|
||||
// * loadedELF, description of the loaded binary
|
||||
// * arch.Context matching the binary arch
|
||||
// * fs.Dirent of the binary file
|
||||
// * Possibly updated argv
|
||||
func loadBinary(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace, root, wd *fs.Dirent, remainingTraversals *uint, features *cpuid.FeatureSet, filename string, passedFile *fs.File, argv []string, resolveFinal bool) (loadedELF, arch.Context, *fs.Dirent, []string, error) {
|
||||
// * Possibly updated args.Argv
|
||||
func loadExecutable(ctx context.Context, args LoadArgs) (loadedELF, arch.Context, *fs.Dirent, []string, error) {
|
||||
for i := 0; i < maxLoaderAttempts; i++ {
|
||||
var (
|
||||
d *fs.Dirent
|
||||
f *fs.File
|
||||
err error
|
||||
)
|
||||
if passedFile == nil {
|
||||
d, f, err = openPath(ctx, mounts, root, wd, remainingTraversals, filename, resolveFinal)
|
||||
if args.File == nil {
|
||||
d, args.File, err = openPath(ctx, args)
|
||||
} else {
|
||||
d, f, err = openFile(ctx, passedFile, nil, "")
|
||||
// Set to nil in case we loop on a Interpreter Script.
|
||||
passedFile = nil
|
||||
d, args.File, err = openFile(ctx, args.File, nil, "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.Infof("Error opening %s: %v", filename, err)
|
||||
ctx.Infof("Error opening %s: %v", args.Filename, err)
|
||||
return loadedELF{}, nil, nil, nil, err
|
||||
}
|
||||
defer f.DecRef()
|
||||
defer args.File.DecRef()
|
||||
// We will return d in the successful case, but defer a DecRef
|
||||
// for intermediate loops and failure cases.
|
||||
defer d.DecRef()
|
||||
@@ -217,9 +258,9 @@ func loadBinary(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamesp
|
||||
// Check the header. Is this an ELF or interpreter script?
|
||||
var hdr [4]uint8
|
||||
// N.B. We assume that reading from a regular file cannot block.
|
||||
_, err = readFull(ctx, f, usermem.BytesIOSequence(hdr[:]), 0)
|
||||
// Allow unexpected EOF, as a valid executable could be only three
|
||||
// bytes (e.g., #!a).
|
||||
_, err = readFull(ctx, args.File, usermem.BytesIOSequence(hdr[:]), 0)
|
||||
// Allow unexpected EOF, as a valid executable could be only
|
||||
// three bytes (e.g., #!a).
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
if err == io.EOF {
|
||||
err = syserror.ENOEXEC
|
||||
@@ -229,33 +270,33 @@ func loadBinary(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamesp
|
||||
|
||||
switch {
|
||||
case bytes.Equal(hdr[:], []byte(elfMagic)):
|
||||
loaded, ac, err := loadELF(ctx, m, mounts, root, wd, remainingTraversals, features, f)
|
||||
loaded, ac, err := loadELF(ctx, args)
|
||||
if err != nil {
|
||||
ctx.Infof("Error loading ELF: %v", err)
|
||||
return loadedELF{}, nil, nil, nil, err
|
||||
}
|
||||
// An ELF is always terminal. Hold on to d.
|
||||
d.IncRef()
|
||||
return loaded, ac, d, argv, err
|
||||
return loaded, ac, d, args.Argv, err
|
||||
case bytes.Equal(hdr[:2], []byte(interpreterScriptMagic)):
|
||||
newpath, newargv, err := parseInterpreterScript(ctx, filename, f, argv)
|
||||
args.Filename, args.Argv, err = parseInterpreterScript(ctx, args.Filename, args.File, args.Argv)
|
||||
if err != nil {
|
||||
ctx.Infof("Error loading interpreter script: %v", err)
|
||||
return loadedELF{}, nil, nil, nil, err
|
||||
}
|
||||
filename = newpath
|
||||
argv = newargv
|
||||
default:
|
||||
ctx.Infof("Unknown magic: %v", hdr)
|
||||
return loadedELF{}, nil, nil, nil, syserror.ENOEXEC
|
||||
}
|
||||
// Set to nil in case we loop on a Interpreter Script.
|
||||
args.File = nil
|
||||
}
|
||||
|
||||
return loadedELF{}, nil, nil, nil, syserror.ELOOP
|
||||
}
|
||||
|
||||
// Load loads "file" into a MemoryManager. If file is nil, the path "filename"
|
||||
// is resolved and loaded instead.
|
||||
// Load loads args.File into a MemoryManager. If args.File is nil, the path
|
||||
// args.Filename is resolved and loaded instead.
|
||||
//
|
||||
// If Load returns ErrSwitchFile it should be called again with the returned
|
||||
// path and argv.
|
||||
@@ -263,37 +304,37 @@ func loadBinary(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamesp
|
||||
// Preconditions:
|
||||
// * The Task MemoryManager is empty.
|
||||
// * Load is called on the Task goroutine.
|
||||
func Load(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace, root, wd *fs.Dirent, maxTraversals *uint, fs *cpuid.FeatureSet, filename string, file *fs.File, argv, envv []string, resolveFinal bool, extraAuxv []arch.AuxEntry, vdso *VDSO) (abi.OS, arch.Context, string, *syserr.Error) {
|
||||
// Load the binary itself.
|
||||
loaded, ac, d, argv, err := loadBinary(ctx, m, mounts, root, wd, maxTraversals, fs, filename, file, argv, resolveFinal)
|
||||
func Load(ctx context.Context, args LoadArgs, extraAuxv []arch.AuxEntry, vdso *VDSO) (abi.OS, arch.Context, string, *syserr.Error) {
|
||||
// Load the executable itself.
|
||||
loaded, ac, d, newArgv, err := loadExecutable(ctx, args)
|
||||
if err != nil {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Failed to load %s: %v", filename, err), syserr.FromError(err).ToLinux())
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Failed to load %s: %v", args.Filename, err), syserr.FromError(err).ToLinux())
|
||||
}
|
||||
defer d.DecRef()
|
||||
|
||||
// Load the VDSO.
|
||||
vdsoAddr, err := loadVDSO(ctx, m, vdso, loaded)
|
||||
vdsoAddr, err := loadVDSO(ctx, args.MemoryManager, vdso, loaded)
|
||||
if err != nil {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Error loading VDSO: %v", err), syserr.FromError(err).ToLinux())
|
||||
}
|
||||
|
||||
// Setup the heap. brk starts at the next page after the end of the
|
||||
// binary. Userspace can assume that the remainer of the page after
|
||||
// executable. Userspace can assume that the remainer of the page after
|
||||
// loaded.end is available for its use.
|
||||
e, ok := loaded.end.RoundUp()
|
||||
if !ok {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("brk overflows: %#x", loaded.end), linux.ENOEXEC)
|
||||
}
|
||||
m.BrkSetup(ctx, e)
|
||||
args.MemoryManager.BrkSetup(ctx, e)
|
||||
|
||||
// Allocate our stack.
|
||||
stack, err := allocStack(ctx, m, ac)
|
||||
stack, err := allocStack(ctx, args.MemoryManager, ac)
|
||||
if err != nil {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Failed to allocate stack: %v", err), syserr.FromError(err).ToLinux())
|
||||
}
|
||||
|
||||
// Push the original filename to the stack, for AT_EXECFN.
|
||||
execfn, err := stack.Push(filename)
|
||||
execfn, err := stack.Push(args.Filename)
|
||||
if err != nil {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Failed to push exec filename: %v", err), syserr.FromError(err).ToLinux())
|
||||
}
|
||||
@@ -327,11 +368,12 @@ func Load(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace, r
|
||||
}...)
|
||||
auxv = append(auxv, extraAuxv...)
|
||||
|
||||
sl, err := stack.Load(argv, envv, auxv)
|
||||
sl, err := stack.Load(newArgv, args.Envv, auxv)
|
||||
if err != nil {
|
||||
return 0, nil, "", syserr.NewDynamic(fmt.Sprintf("Failed to load stack: %v", err), syserr.FromError(err).ToLinux())
|
||||
}
|
||||
|
||||
m := args.MemoryManager
|
||||
m.SetArgvStart(sl.ArgvStart)
|
||||
m.SetArgvEnd(sl.ArgvEnd)
|
||||
m.SetEnvvStart(sl.EnvvStart)
|
||||
@@ -342,7 +384,7 @@ func Load(ctx context.Context, m *mm.MemoryManager, mounts *fs.MountNamespace, r
|
||||
ac.SetIP(uintptr(loaded.entry))
|
||||
ac.SetStack(uintptr(stack.Bottom))
|
||||
|
||||
name := path.Base(filename)
|
||||
name := path.Base(args.Filename)
|
||||
if len(name) > linux.TASK_COMM_LEN-1 {
|
||||
name = name[:linux.TASK_COMM_LEN-1]
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ go_library(
|
||||
"//pkg/sentry/kernel/signalfd",
|
||||
"//pkg/sentry/kernel/time",
|
||||
"//pkg/sentry/limits",
|
||||
"//pkg/sentry/loader",
|
||||
"//pkg/sentry/memmap",
|
||||
"//pkg/sentry/mm",
|
||||
"//pkg/sentry/safemem",
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/sentry/fs"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
||||
"gvisor.dev/gvisor/pkg/sentry/kernel/sched"
|
||||
"gvisor.dev/gvisor/pkg/sentry/loader"
|
||||
"gvisor.dev/gvisor/pkg/sentry/usermem"
|
||||
"gvisor.dev/gvisor/pkg/syserror"
|
||||
)
|
||||
@@ -147,8 +148,21 @@ func execveat(t *kernel.Task, dirFD int32, pathnameAddr, argvAddr, envvAddr user
|
||||
}
|
||||
|
||||
// Load the new TaskContext.
|
||||
maxTraversals := uint(linux.MaxSymlinkTraversals)
|
||||
tc, se := t.Kernel().LoadTaskImage(t, t.MountNamespace(), root, wd, &maxTraversals, pathname, executable, argv, envv, resolveFinal, t.Arch().FeatureSet())
|
||||
remainingTraversals := uint(linux.MaxSymlinkTraversals)
|
||||
loadArgs := loader.LoadArgs{
|
||||
Mounts: t.MountNamespace(),
|
||||
Root: root,
|
||||
WorkingDirectory: wd,
|
||||
RemainingTraversals: &remainingTraversals,
|
||||
ResolveFinal: resolveFinal,
|
||||
Filename: pathname,
|
||||
File: executable,
|
||||
Argv: argv,
|
||||
Envv: envv,
|
||||
Features: t.Arch().FeatureSet(),
|
||||
}
|
||||
|
||||
tc, se := t.Kernel().LoadTaskImage(t, loadArgs)
|
||||
if se != nil {
|
||||
return 0, nil, se.ToError()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user