Skip to content

Commit

Permalink
Implementing a limitation for the open()
Browse files Browse the repository at this point in the history
Implementing a limitation for the open() that limits opening files
by the list of the files that were opened during the initialization
step (__VU == 0).

For example, a code like:

```js
if (__VU >0) {
   JSON.parse(open("./arr.json"));
}
```

Should return an error.

Closes #1771

Co-authored-by: Mihail Stoykov <[email protected]>
  • Loading branch information
olegbespalov and mstoykov authored Jan 12, 2022
1 parent 23575eb commit 57ec741
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 13 deletions.
4 changes: 4 additions & 0 deletions js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init *
unbindInit()
*init.ctxPtr = nil

if vuID == 0 {
init.allowOnlyOpenedFiles()
}

rt.SetRandSource(common.NewRandSource())

return nil
Expand Down
44 changes: 37 additions & 7 deletions js/initcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"go.k6.io/k6/js/modules/k6/metrics"
"go.k6.io/k6/js/modules/k6/ws"
"go.k6.io/k6/lib"
"go.k6.io/k6/lib/fsext"
"go.k6.io/k6/loader"
)

Expand Down Expand Up @@ -299,13 +300,8 @@ func (i *InitContext) Open(ctx context.Context, filename string, args ...string)
if filename[0:1] != afero.FilePathSeparator {
filename = afero.FilePathSeparator + filename
}
// Workaround for https://github.com/spf13/afero/issues/201
if isDir, err := afero.IsDir(fs, filename); err != nil {
return nil, err
} else if isDir {
return nil, fmt.Errorf("open() can't be used with directories, path: %q", filename)
}
data, err := afero.ReadFile(fs, filename)

data, err := readFile(fs, filename)
if err != nil {
return nil, err
}
Expand All @@ -317,6 +313,40 @@ func (i *InitContext) Open(ctx context.Context, filename string, args ...string)
return i.runtime.ToValue(string(data)), nil
}

func readFile(fileSystem afero.Fs, filename string) (data []byte, err error) {
defer func() {
if errors.Is(err, fsext.ErrPathNeverRequestedBefore) {
// loading different files per VU is not supported, so all files should are going
// to be used inside the scenario should be opened during the init step (without any conditions)
err = fmt.Errorf(
"open() can't be used with files that weren't previously opened during initialization (__VU==0), path: %q",
filename,
)
}
}()

// Workaround for https://github.com/spf13/afero/issues/201
if isDir, err := afero.IsDir(fileSystem, filename); err != nil {
return nil, err
} else if isDir {
return nil, fmt.Errorf("open() can't be used with directories, path: %q", filename)
}

return afero.ReadFile(fileSystem, filename)
}

// allowOnlyOpenedFiles enables seen only files
func (i *InitContext) allowOnlyOpenedFiles() {
fs := i.filesystems["file"]

alreadyOpenedFS, ok := fs.(fsext.OnlyCachedEnabler)
if !ok {
return
}

alreadyOpenedFS.AllowOnlyCached()
}

func getInternalJSModules() map[string]interface{} {
return map[string]interface{}{
"k6": k6.New(),
Expand Down
74 changes: 74 additions & 0 deletions js/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
"go.k6.io/k6/js/modules/k6/ws"
"go.k6.io/k6/lib"
_ "go.k6.io/k6/lib/executor" // TODO: figure out something better
"go.k6.io/k6/lib/fsext"
"go.k6.io/k6/lib/metrics"
"go.k6.io/k6/lib/testutils"
"go.k6.io/k6/lib/testutils/httpmultibin"
Expand Down Expand Up @@ -1157,6 +1158,79 @@ func TestVUIntegrationOpenFunctionErrorWhenSneaky(t *testing.T) {
assert.Contains(t, err.Error(), "only available in the init stage")
}

func TestVUDoesOpenUnderV0Condition(t *testing.T) {
t.Parallel()

baseFS := afero.NewMemMapFs()
data := `
if (__VU == 0) {
let data = open("/home/somebody/test.json");
}
exports.default = function() {
console.log("hey")
}
`
require.NoError(t, afero.WriteFile(baseFS, "/home/somebody/test.json", []byte(`42`), os.ModePerm))
require.NoError(t, afero.WriteFile(baseFS, "/script.js", []byte(data), os.ModePerm))

fs := fsext.NewCacheOnReadFs(baseFS, afero.NewMemMapFs(), 0)

r, err := getSimpleRunner(t, "/script.js", data, fs)
require.NoError(t, err)

_, err = r.NewVU(1, 1, make(chan stats.SampleContainer, 100))
assert.NoError(t, err)
}

func TestVUDoesNotOpenUnderConditions(t *testing.T) {
t.Parallel()

baseFS := afero.NewMemMapFs()
data := `
if (__VU > 0) {
let data = open("/home/somebody/test.json");
}
exports.default = function() {
console.log("hey")
}
`
require.NoError(t, afero.WriteFile(baseFS, "/home/somebody/test.json", []byte(`42`), os.ModePerm))
require.NoError(t, afero.WriteFile(baseFS, "/script.js", []byte(data), os.ModePerm))

fs := fsext.NewCacheOnReadFs(baseFS, afero.NewMemMapFs(), 0)

r, err := getSimpleRunner(t, "/script.js", data, fs)
require.NoError(t, err)

_, err = r.NewVU(1, 1, make(chan stats.SampleContainer, 100))
assert.Error(t, err)
assert.Contains(t, err.Error(), "open() can't be used with files that weren't previously opened during initialization (__VU==0)")
}

func TestVUDoesNonExistingPathnUnderConditions(t *testing.T) {
t.Parallel()

baseFS := afero.NewMemMapFs()
data := `
if (__VU == 1) {
let data = open("/home/nobody");
}
exports.default = function() {
console.log("hey")
}
`
require.NoError(t, afero.WriteFile(baseFS, "/script.js", []byte(data), os.ModePerm))

fs := fsext.NewCacheOnReadFs(baseFS, afero.NewMemMapFs(), 0)

r, err := getSimpleRunner(t, "/script.js", data, fs)
require.NoError(t, err)

_, err = r.NewVU(1, 1, make(chan stats.SampleContainer, 100))
assert.Error(t, err)
assert.Contains(t, err.Error(), "open() can't be used with files that weren't previously opened during initialization (__VU==0)")
}

func TestVUIntegrationCookiesReset(t *testing.T) {
t.Parallel()
tb := httpmultibin.NewHTTPMultiBin(t)
Expand Down
6 changes: 3 additions & 3 deletions lib/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ func (arc *Archive) Write(out io.Writer) error {
normalizeAndAnonymizeURL(metaArc.PwdURL)
metaArc.Filename = getURLtoString(metaArc.FilenameURL)
metaArc.Pwd = getURLtoString(metaArc.PwdURL)
var actualDataPath, err = url.PathUnescape(path.Join(getURLPathOnFs(metaArc.FilenameURL)))
actualDataPath, err := url.PathUnescape(path.Join(getURLPathOnFs(metaArc.FilenameURL)))
if err != nil {
return err
}
Expand Down Expand Up @@ -286,7 +286,7 @@ func (arc *Archive) Write(out io.Writer) error {
if !ok {
continue
}
if cachedfs, ok := filesystem.(fsext.CacheOnReadFs); ok {
if cachedfs, ok := filesystem.(fsext.CacheLayerGetter); ok {
filesystem = cachedfs.GetCachingFs()
}

Expand Down Expand Up @@ -344,7 +344,7 @@ func (arc *Archive) Write(out io.Writer) error {
}

for _, filePath := range paths {
var fullFilePath = path.Clean(path.Join(name, filePath))
fullFilePath := path.Clean(path.Join(name, filePath))
// we either have opaque
if fullFilePath == actualDataPath {
madeLinkToData = true
Expand Down
72 changes: 70 additions & 2 deletions lib/fsext/cacheonread.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,95 @@
package fsext

import (
"errors"
"os"
"sync"
"time"

"github.com/spf13/afero"
)

// ErrPathNeverRequestedBefore represent an error when path never opened/requested before
var ErrPathNeverRequestedBefore = errors.New("path never requested before")

// CacheOnReadFs is wrapper around afero.CacheOnReadFs with the ability to return the filesystem
// that is used as cache
type CacheOnReadFs struct {
afero.Fs
cache afero.Fs

lock *sync.Mutex
cachedOnly bool
cached map[string]bool
}

// OnlyCachedEnabler enables the mode of FS that allows to open
// already opened files (e.g. serve from cache only)
type OnlyCachedEnabler interface {
AllowOnlyCached()
}

// CacheLayerGetter provide a direct access to a cache layer
type CacheLayerGetter interface {
GetCachingFs() afero.Fs
}

// NewCacheOnReadFs returns a new CacheOnReadFs
func NewCacheOnReadFs(base, layer afero.Fs, cacheTime time.Duration) afero.Fs {
return CacheOnReadFs{
return &CacheOnReadFs{
Fs: afero.NewCacheOnReadFs(base, layer, cacheTime),
cache: layer,

lock: &sync.Mutex{},
cachedOnly: false,
cached: make(map[string]bool),
}
}

// GetCachingFs returns the afero.Fs being used for cache
func (c CacheOnReadFs) GetCachingFs() afero.Fs {
func (c *CacheOnReadFs) GetCachingFs() afero.Fs {
return c.cache
}

// AllowOnlyCached enables the cached only mode of the CacheOnReadFs
func (c *CacheOnReadFs) AllowOnlyCached() {
c.lock.Lock()
c.cachedOnly = true
c.lock.Unlock()
}

// Open opens file and track the history of opened files
// if CacheOnReadFs is in the opened only mode it should return
// an error if file wasn't open before
func (c *CacheOnReadFs) Open(name string) (afero.File, error) {
if err := c.checkOrRemember(name); err != nil {
return nil, err
}

return c.Fs.Open(name)
}

// Stat returns a FileInfo describing the named file, or an error, if any
// happens.
// if CacheOnReadFs is in the opened only mode it should return
// an error if path wasn't open before
func (c *CacheOnReadFs) Stat(path string) (os.FileInfo, error) {
if err := c.checkOrRemember(path); err != nil {
return nil, err
}

return c.Fs.Stat(path)
}

func (c *CacheOnReadFs) checkOrRemember(path string) error {
c.lock.Lock()
defer c.lock.Unlock()

if !c.cachedOnly {
c.cached[path] = true
} else if !c.cached[path] {
return ErrPathNeverRequestedBefore
}

return nil
}
2 changes: 1 addition & 1 deletion loader/readsource.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func ReadSource(
return nil, err
}
// TODO: don't do it in this way ...
err = afero.WriteFile(filesystems["file"].(fsext.CacheOnReadFs).GetCachingFs(), "/-", data, 0644)
err = afero.WriteFile(filesystems["file"].(fsext.CacheLayerGetter).GetCachingFs(), "/-", data, 0o644)
if err != nil {
return nil, fmt.Errorf("caching data read from -: %w", err)
}
Expand Down

0 comments on commit 57ec741

Please sign in to comment.