ajv-validator / ajv

The fastest JSON schema Validator. Supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927)
https://ajv.js.org
MIT License
13.71k stars 872 forks source link

Crashes when in Docker when using the alpine base #441

Closed daveisfera closed 7 years ago

daveisfera commented 7 years ago

ajv is crashing when running in Docker using the alpine base. See https://github.com/nodejs/docker-node/issues/288 for details.

What version of Ajv are you using? Does the issue happen if you use the latest version? 4.11.5 yes

Ajv options object (see https://github.com/epoberezkin/ajv#options):

{ allErrors: true, extendedRefs: false }

JSON Schema (please make it as small as possible to reproduce the issue):

{ "type": "object",
  "properties": 
   { "s0": { "$ref": "/ColumnType" },
     "s1": { "$ref": "/ColumnType" },
     "s2": { "$ref": "/ColumnType" },
     "s3": { "$ref": "/ColumnType" },
     "s4": { "$ref": "/ColumnType" },
     "s5": { "$ref": "/ColumnType" },
     "s6": { "$ref": "/ColumnType" },
     "s7": { "$ref": "/ColumnType" },
     "s8": { "$ref": "/ColumnType" },
     "s9": { "$ref": "/ColumnType" },
     "s10": { "$ref": "/ColumnType" },
     "s11": { "$ref": "/ColumnType" },
     "s12": { "$ref": "/ColumnType" },
     "s13": { "$ref": "/ColumnType" },
     "s14": { "$ref": "/ColumnType" },
     "s15": { "$ref": "/ColumnType" },
     "s16": { "$ref": "/ColumnType" },
     "s17": { "$ref": "/ColumnType" },
     "s18": { "$ref": "/ColumnType" },
     "s19": { "$ref": "/ColumnType" },
     "s20": { "$ref": "/ColumnType" },
     "s21": { "$ref": "/ColumnType" },
     "s22": { "$ref": "/ColumnType" },
     "s23": { "$ref": "/ColumnType" },
     "s24": { "$ref": "/ColumnType" },
     "s25": { "$ref": "/ColumnType" },
     "s26": { "$ref": "/ColumnType" },
     "s27": { "$ref": "/ColumnType" },
     "s28": { "$ref": "/ColumnType" },
     "s29": { "$ref": "/ColumnType" },
     "s30": { "$ref": "/ColumnType" },
     "s31": { "$ref": "/ColumnType" },
     "s32": { "$ref": "/ColumnType" },
     "s33": { "$ref": "/ColumnType" },
     "s34": { "$ref": "/ColumnType" },
     "s35": { "$ref": "/ColumnType" },
     "s36": { "$ref": "/ColumnType" },
     "s37": { "$ref": "/ColumnType" },
     "s38": { "$ref": "/ColumnType" },
     "s39": { "$ref": "/ColumnType" },
     "s40": { "$ref": "/ColumnType" },
     "s41": { "$ref": "/ColumnType" },
     "s42": { "$ref": "/ColumnType" },
     "s43": { "$ref": "/ColumnType" },
     "s44": { "$ref": "/ColumnType" },
     "s45": { "$ref": "/ColumnType" },
     "s46": { "$ref": "/ColumnType" },
     "s47": { "$ref": "/ColumnType" },
     "s48": { "$ref": "/ColumnType" },
     "s49": { "$ref": "/ColumnType" },
     "s50": { "$ref": "/ColumnType" },
     "s51": { "$ref": "/ColumnType" },
     "s52": { "$ref": "/ColumnType" },
     "s53": { "$ref": "/ColumnType" },
     "s54": { "$ref": "/ColumnType" },
     "s55": { "$ref": "/ColumnType" },
     "s56": { "$ref": "/ColumnType" },
     "s57": { "$ref": "/ColumnType" },
     "s58": { "$ref": "/ColumnType" },
     "s59": { "$ref": "/ColumnType" },
     "s60": { "$ref": "/ColumnType" },
     "s61": { "$ref": "/ColumnType" },
     "s62": { "$ref": "/ColumnType" },
     "s63": { "$ref": "/ColumnType" },
     "s64": { "$ref": "/ColumnType" },
     "s65": { "$ref": "/ColumnType" },
     "s66": { "$ref": "/ColumnType" },
     "s67": { "$ref": "/ColumnType" },
     "s68": { "$ref": "/ColumnType" },
     "s69": { "$ref": "/ColumnType" },
     "s70": { "$ref": "/ColumnType" },
     "s71": { "$ref": "/ColumnType" },
     "s72": { "$ref": "/ColumnType" },
     "s73": { "$ref": "/ColumnType" },
     "s74": { "$ref": "/ColumnType" },
     "s75": { "$ref": "/ColumnType" },
     "s76": { "$ref": "/ColumnType" },
     "s77": { "$ref": "/ColumnType" },
     "s78": { "$ref": "/ColumnType" },
     "s79": { "$ref": "/ColumnType" },
     "s80": { "$ref": "/ColumnType" },
     "s81": { "$ref": "/ColumnType" },
     "s82": { "$ref": "/ColumnType" },
     "s83": { "$ref": "/ColumnType" },
     "s84": { "$ref": "/ColumnType" },
     "s85": { "$ref": "/ColumnType" },
     "s86": { "$ref": "/ColumnType" },
     "s87": { "$ref": "/ColumnType" },
     "s88": { "$ref": "/ColumnType" },
     "s89": { "$ref": "/ColumnType" },
     "s90": { "$ref": "/ColumnType" },
     "s91": { "$ref": "/ColumnType" },
     "s92": { "$ref": "/ColumnType" },
     "s93": { "$ref": "/ColumnType" },
     "s94": { "$ref": "/ColumnType" },
     "s95": { "$ref": "/ColumnType" },
     "s96": { "$ref": "/ColumnType" },
     "s97": { "$ref": "/ColumnType" },
     "s98": { "$ref": "/ColumnType" },
     "s99": { "$ref": "/ColumnType" } } }

Data (please make it as small as posssible to reproduce the issue): Randomly generated by script

Your code (please use options, schema and data as variables):

#!/usr/bin/env node
'use strict';

const Ajv = require('ajv');
const validator = new Ajv({ allErrors: true, extendedRefs: false });

function getRandomIntInclusive(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}       

const MAX_STRING_LENGTH = 10000;

const STRING_KEY = {
    id: '/StringKey',
    type: 'string',
    maxLength: MAX_STRING_LENGTH,
};

const COLUMN_KEY = {
    id: '/ColumnKey',
    oneOf: [
        { $ref: '/StringKey' },
        { type: ['boolean', 'number'] },
    ],  
};          

const COLUMN_ARRAY = {
    type: 'array',
    items: { $ref: '/ColumnKey' },
};          

const COLUMN_TYPE = {
    id: '/ColumnType',
    oneOf: [    
        { $ref: '/ColumnKey' },
        COLUMN_ARRAY,
    ],      
};          

validator.addSchema(STRING_KEY);
validator.addSchema(COLUMN_KEY);
validator.addSchema(COLUMN_TYPE);

const WORDS = ['this', 'works', 'well', 'as', 'a', 'reproducer'];
const NUM_COLUMNS = 100;

let schema = {
    type: 'object',
    properties: {},
};

let c;
for (c=0; c<NUM_COLUMNS; c++) {
    schema.properties[`s${c}`] = { $ref: '/ColumnType'};
}       
const validate = validator.compile(schema);

const NUM_ROWS = parseInt(process.argv[2] || '500');
console.log(`Testing ${NUM_ROWS} rows`);
let r;
let value;
let i;
for (r=0; r<NUM_ROWS; r++) {
    value = {}
    for (c=0; c<NUM_COLUMNS; c++) {
        const n = getRandomIntInclusive(1, 20);
        const cS = `s${c}`;
        value[cS] = '';
        for (i=0; i<n; i++) {
            value[cS] += ` WORDS[${getRandomIntInclusive(0, 5)}]`;
        }
    }

    validate(value);
}

console.log('Done');

Validation result, data AFTER validation, error messages: Prints Done when it doesn't crash.

What results did you expect? Run to completion without crashing.

Are you going to resolve the issue? Wish I could but I'm not sure how to.

epoberezkin commented 7 years ago

@daveisfera please add the error messages when it crashes.

daveisfera commented 7 years ago

It segfaults so there's no output but if you look at dmesg, then this is the output:

[708816.590968] V8 WorkerThread[26964]: segfault at 7f43b54bdff8 ip 000055e3d84c163e sp 00007f43b54be000 error 6 in node[55e3d7bff000+18d6000]
daveisfera commented 7 years ago

I added the backtrace from gdb in the docker-node issue.

epoberezkin commented 7 years ago

@daveisfera I would suggest trying to find a smaller code that causes the crash. You still have too much going on there. Would just creating ajv instance cause it? If so, you can try using option {meta: false, validateSchema: false} (that prevents validating schema against meta-schema).

If the schema size is the issue you can try changing it to use patternProperties keyword instead of properties (the code seems to generate schema with 500 properties and it compiles into a functionthat has size approx. 31kb (it's not recursive though, but can the size of the function be an issue?)

Your schema would look something like this:

{
  "type": "object",
  "patternProperties": {
    "^s\\d+$": { "$ref": "/ColumnType" }
  }
}

You can have more specific pattern to define exact range of allowed numbers and use "additionalProperties": false} to prohibit any other properties if needed.

epoberezkin commented 7 years ago

In general, I think that generating schemas programmatically partially defeats many reasons of using the schemas, so while there are cases when it's the only way, in your case using patternProperties seems a better approach.

daveisfera commented 7 years ago

It's generating a schema with 100 properties and then validates 500 rows of random data with it. I did some testing and it will work fine with 80 properties but crashes with 81. With 81 properties, I can lower the number of rows to 394 and it still crashes. I was using 100 properties and 500 rows before because they were round numbers that made the crash happen but those numbers are the smallest way that I've found to make it crash.

Also, this crash is happening in our actual software where the schema is not so simplistic/contrived, but I can't send you that, so I was just going for the simplest way to reproduce the crash and this was it. It's a completely self contained test in 79 lines with the bulk of that being setting up ajv so I'm not sure how I could make it any simpler.

epoberezkin commented 7 years ago

I did some testing and it will work fine with 80 properties but crashes with 81.

So the function size can be an issue. Try changing schema to reduce its size as I suggested.

It's a completely self contained test in 79 lines with the bulk of that being setting up ajv so I'm not sure how I could make it any simpler.

You don't need a self-contained test. You just need a minimal code snippet that still fails, to isolate the problem - what is the line where it actually crashes. As you don't have logs (maybe you can add them?), removing some code can help to understand the line where it fails. E.g., if it crashes during schema compilation, you can remove call to validate function. etc.

daveisfera commented 7 years ago

I was able to reduce it to using the schema from above with 81 properties and then sending the same value of just empty strings 394 times and it still crashes.

#!/usr/bin/env node
'use strict';

const Ajv = require('ajv');
const validator = new Ajv({ allErrors: true, extendedRefs: false });

const STRING_KEY = {
    id: '/StringKey',
    type: 'string',
    maxLength: 10000,
};

validator.addSchema(STRING_KEY);

let schema = {
    type: 'object',
    properties: {},
};

const NUM_COLUMNS = parseInt(process.argv[2] || '81');
console.log(`Testing with ${NUM_COLUMNS} columns`);
let c;
for (c=0; c<NUM_COLUMNS; c++) {
    schema.properties[`s${c}`] = { $ref: '/StringKey'};
}
console.log('schema:', schema);
const validate = validator.compile(schema);

let value = {};
for (c=0; c<NUM_COLUMNS; c++) {
    const cS = `s${c}`;
    value[cS] = '';
}
console.log('value:', value);

const NUM_ROWS = parseInt(process.argv[3] || '394');
console.log(`Testing with ${NUM_ROWS} rows`);
let r;
for (r=0; r<NUM_ROWS; r++) {
    validate(value);
}

console.log('Done');

Or here's a fixed version that does nothing but define schema, value and call ajv:

#!/usr/bin/env node                    
'use strict';                      

const Ajv = require('ajv');        
const validator = new Ajv({ allErrors: true, extendedRefs: false });

const STRING_KEY = {               
    id: '/StringKey',              
    type: 'string',                
    maxLength: 10000,              
};                                 

validator.addSchema(STRING_KEY);

const schema = { type: 'object',   
  properties:                      
   { s0: { '$ref': '/StringKey' }, 
     s1: { '$ref': '/StringKey' }, 
     s2: { '$ref': '/StringKey' }, 
     s3: { '$ref': '/StringKey' }, 
     s4: { '$ref': '/StringKey' }, 
     s5: { '$ref': '/StringKey' }, 
     s6: { '$ref': '/StringKey' }, 
     s7: { '$ref': '/StringKey' }, 
     s8: { '$ref': '/StringKey' }, 
     s9: { '$ref': '/StringKey' }, 
     s10: { '$ref': '/StringKey' },
     s11: { '$ref': '/StringKey' },
     s12: { '$ref': '/StringKey' },
     s13: { '$ref': '/StringKey' },
     s14: { '$ref': '/StringKey' },
     s15: { '$ref': '/StringKey' },
     s16: { '$ref': '/StringKey' },
     s17: { '$ref': '/StringKey' },
     s18: { '$ref': '/StringKey' },
     s19: { '$ref': '/StringKey' },
     s20: { '$ref': '/StringKey' },
     s21: { '$ref': '/StringKey' },
     s22: { '$ref': '/StringKey' },
     s23: { '$ref': '/StringKey' },
     s24: { '$ref': '/StringKey' },
     s25: { '$ref': '/StringKey' },
     s26: { '$ref': '/StringKey' },
     s27: { '$ref': '/StringKey' },
     s28: { '$ref': '/StringKey' },
     s29: { '$ref': '/StringKey' },
     s30: { '$ref': '/StringKey' },
     s31: { '$ref': '/StringKey' },
     s32: { '$ref': '/StringKey' },    
     s33: { '$ref': '/StringKey' },
     s34: { '$ref': '/StringKey' },        
     s35: { '$ref': '/StringKey' },
     s36: { '$ref': '/StringKey' },
     s37: { '$ref': '/StringKey' },
     s38: { '$ref': '/StringKey' },
     s39: { '$ref': '/StringKey' },
     s40: { '$ref': '/StringKey' },
     s41: { '$ref': '/StringKey' },
     s42: { '$ref': '/StringKey' },
     s43: { '$ref': '/StringKey' },
     s44: { '$ref': '/StringKey' },
     s45: { '$ref': '/StringKey' },
     s46: { '$ref': '/StringKey' },
     s47: { '$ref': '/StringKey' },
     s48: { '$ref': '/StringKey' },
     s49: { '$ref': '/StringKey' },
     s50: { '$ref': '/StringKey' },
     s51: { '$ref': '/StringKey' },
     s52: { '$ref': '/StringKey' },
     s53: { '$ref': '/StringKey' },
     s54: { '$ref': '/StringKey' },
     s55: { '$ref': '/StringKey' },
     s56: { '$ref': '/StringKey' },
     s57: { '$ref': '/StringKey' },
     s58: { '$ref': '/StringKey' },
     s59: { '$ref': '/StringKey' },
     s60: { '$ref': '/StringKey' },
     s61: { '$ref': '/StringKey' },
     s62: { '$ref': '/StringKey' },
     s63: { '$ref': '/StringKey' },
     s64: { '$ref': '/StringKey' },
     s65: { '$ref': '/StringKey' },
     s66: { '$ref': '/StringKey' },
     s67: { '$ref': '/StringKey' },
     s68: { '$ref': '/StringKey' },
     s69: { '$ref': '/StringKey' },
     s70: { '$ref': '/StringKey' },
     s71: { '$ref': '/StringKey' },
     s72: { '$ref': '/StringKey' },
     s73: { '$ref': '/StringKey' },
     s74: { '$ref': '/StringKey' },
     s75: { '$ref': '/StringKey' },
     s76: { '$ref': '/StringKey' },
     s77: { '$ref': '/StringKey' },
     s78: { '$ref': '/StringKey' },
     s79: { '$ref': '/StringKey' },
     s80: { '$ref': '/StringKey' } } };

const validate = validator.compile(schema);

const value = { s0: '',            
  s1: '',                          
  s2: '',                          
  s3: '',                          
  s4: '',                          
  s5: '',                          
  s6: '',                          
  s7: '',                          
  s8: '',                          
  s9: '',                          
  s10: '',                         
  s11: '',                         
  s12: '',                         
  s13: '',                         
  s14: '',                         
  s15: '',                         
  s16: '',                         
  s17: '',                         
  s18: '',                         
  s19: '',                         
  s20: '',                         
  s21: '',                         
  s22: '',                         
  s23: '',                         
  s24: '',                         
  s25: '',                         
  s26: '',                         
  s27: '',                         
  s28: '',                         
  s29: '',                         
  s30: '',                         
  s31: '',                         
  s32: '',                         
  s33: '',                         
  s34: '',                         
  s35: '',                         
  s36: '',                         
  s37: '',                             
  s38: '',                         
  s39: '',                                 
  s40: '',                         
  s41: '',                         
  s42: '',                         
  s43: '',                         
  s44: '',                         
  s45: '',                         
  s46: '',                         
  s47: '',                         
  s48: '',                         
  s49: '',                         
  s50: '',                         
  s51: '',                         
  s52: '',                         
  s53: '',                         
  s54: '',                         
  s55: '',                         
  s56: '',                         
  s57: '',                         
  s58: '',                         
  s59: '',                         
  s60: '',                         
  s61: '',                         
  s62: '',                         
  s63: '',                         
  s64: '',                         
  s65: '',                         
  s66: '',                         
  s67: '',                         
  s68: '',                         
  s69: '',                         
  s70: '',                         
  s71: '',                         
  s72: '',                         
  s73: '',                         
  s74: '',                         
  s75: '',                         
  s76: '',                         
  s77: '',                         
  s78: '',                         
  s79: '',                         
  s80: '' };                       

for (let r=0; r<394; r++) {        
    validate(value);               
}                                  

console.log('Done');
epoberezkin commented 7 years ago

Can you try removing data and call to validate inside the loop in the end? Would it still crash?

epoberezkin commented 7 years ago

Also, after that, can you try using option inlineRefs: false?

daveisfera commented 7 years ago

Can you try removing data and call to validate inside the loop in the end? Would it still crash?

Does not crash if call to validate() is commented out.

Also, after that, can you try using option inlineRefs: false?

Still crashes.

epoberezkin commented 7 years ago

So crash is caused by call to validate, not by compiling schema.

Would it crash if you call validate only once? Or you need to call it 394 times to crash and 393 is not enough?

Did you try using patternProperties in schema instead of properties as I suggested earlier?

You really need to experiment more with the code to find the minimal amount of code that still crashes but when ANY further reduction stops it from crashing. From what you say it is not minimal yet...

epoberezkin commented 7 years ago

Also, the option is not extendedRefs, it's extendRefs and in your case it does nothing.

epoberezkin commented 7 years ago

Also try changing ID to id: 'StringKey', not sure why you need this "/" in front. Unlikely it's what's causing the issue, but just in case.

Also, does switching to ajv 5.0.4-beta change anything?

daveisfera commented 7 years ago

So crash is caused by call to validate, not by compiling schema.

We already knew that because the log after the schema being compiled was printed since the start of this issue.

Would it crash if you call validate only once? Or you need to call it 394 times to crash and 393 is not enough?

No, 394 is minimum number of times for it to crash. If you run it 393 times or with less than 81 properties, then it completes without issue.

Did you try using patternProperties in schema instead of properties as I suggested earlier?

This wouldn't help me because our actual code can't use patternProperties.

You really need to experiment more with the code to find the minimal amount of code that still crashes but when ANY further reduction stops it from crashing. From what you say it is not minimal yet...

I've found it. 81 properties with 394 calls.

Also, the option is not extendedRefs, it's extendRefs and in your case it does nothing.

Ok, I removed it.

Also try changing ID to id: 'StringKey', not sure why you need this "/" in front. Unlikely it's what's causing the issue, but just in case.

Still crashes.

Also, does switching to ajv 5.0.4-beta change anything?

Still crashes.

epoberezkin commented 7 years ago

This wouldn't help me because our actual code can't use patternProperties.

This would help understanding the issue. But I guess it should work given that it works with smaller schemas...

epoberezkin commented 7 years ago

Ok, I have a theory... Can you try bigger number of properties with a smaller number of calls (e.g. 82 properties with 1+ call etc.)? Opposite too (395 calls with 1+ properties etc.)?

Maybe the environment has some limit on the time spent in the execution block ("tick")? Maybe if you make it async it will work? I mean not trying to validate everything in one go and instead use setTimeout to break it into multiple blocks?

daveisfera commented 7 years ago

The issue was with the smaller stack available in Apline Linux. You can see details here.

daveisfera commented 7 years ago

@epoberezkin Could this be added to the tests to verify that ajv works with very large schemas?