Implement resource limits for regular expression evaluation. Tweaked

to roughly match the Cocoa behaviour, but can be changed through 
the GSRegularExpressionWorkLimit user default.


git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/base/trunk@39872 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
Niels Grewe 2016-06-17 09:04:04 +00:00
parent ac2d08d2a1
commit 0d98f56eca
4 changed files with 156 additions and 8 deletions

View file

@ -1,3 +1,13 @@
2016-06-17 Niels Grewe <niels.grewe@halbordnung.de>
* Headers/Foundation/NSRegularExpression.h
* Source/NSRegularExpression.m
* Tests/base/NSRegularExpression/basic.m:
Implement resource limits for regular expression evaluation. Tweaked
to roughly match the Cocoa behaviour, but can be changed through
the GSRegularExpressionWorkLimit user default.
2016-06-17 Niels Grewe <niels.grewe@halbordnung.de>
* Source/NSRegularExpression.m: Implement -isEqual: and -hash

View file

@ -1,18 +1,19 @@
/* Definition of class NSRegularExpression
Copyright (C) 2011 Free Software Foundation, Inc.
This file is part of the GNUstep Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
@ -72,7 +73,19 @@ DEFINE_BLOCK_TYPE(GSRegexBlock, void, NSTextCheckingResult*,
#ifndef GSREGEXTYPE
# define GSREGEXTYPE void
#endif
/**
* NSRegularExpression is used to inspect and manipulate strings using regular
* expressions. The interface is thread safe: The same NSRegularExpression
* object may be used to concurrently perform matching on multiple threads.
*
* To guard against regular expressions with extremely poor performance, the
* underlying matcher will abort after a certain number of steps. This is
* controlled using the GSRegularExpressionWorkLimit user default. The value of
* this default key represents the number of steps executed by the match engine,
* so it is only indirectly correlated with the time taken to execute the
* pattern, but it usually in the order of milliseconds. The preset 1500,
* setting value to 0 disables the work limit.
*/
@interface NSRegularExpression : NSObject <NSCoding, NSCopying>
{
#if GS_EXPOSE(NSRegularExpression)
@ -153,4 +166,3 @@ DEFINE_BLOCK_TYPE(GSRegexBlock, void, NSTextCheckingResult*,
#endif /* GS_API_MACOSX */
#endif /* _NSRegualrExpression_h_GNUSTEP_BASE_INCLUDE */

View file

@ -46,6 +46,8 @@
#import "Foundation/NSTextCheckingResult.h"
#import "Foundation/NSArray.h"
#import "Foundation/NSCoder.h"
#import "Foundation/NSUserDefaults.h"
#import "Foundation/NSNotification.h"
/**
@ -293,6 +295,48 @@ callback(const void *context, int32_t steps)
return stop;
}
#define DEFAULT_WORK_LIMIT 1500
/**
* The work limit specifies the number of iterations the matcher will do before
* aborting an operation. This ensures that degenerate pattern/input
* combinations don't send the application into what for all intents and
* purposes seems like an infinite loop.
*/
static int32_t _workLimit = DEFAULT_WORK_LIMIT;
+ (void) _defaultsChanged: (NSNotification*)n
{
NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
id value = [defs objectForKey: @"GSRegularExpressionWorkLimit"];
int32_t newLimit = DEFAULT_WORK_LIMIT;
if ([value respondsToSelector: @selector(intValue)])
{
int32_t v = [value intValue];
if (v >= 0)
{
newLimit = v;
}
}
_workLimit = newLimit;
}
+ (void) initialize
{
if (self == [NSRegularExpression class])
{
[[NSNotificationCenter defaultCenter]
addObserver: self
selector: @selector(_defaultsChanged:)
name: NSUserDefaultsDidChangeNotification
object: nil];
[self _defaultsChanged: nil];
}
}
/**
* Sets up a libicu regex object for use. Note: the documentation states that
* NSRegularExpression must be thread safe. To accomplish this, we store a
@ -328,6 +372,7 @@ setupRegex(URegularExpression *regex,
{
uregex_useTransparentBounds(r, TRUE, &s);
}
uregex_setTimeLimit(r, _workLimit, &s);
if (U_FAILURE(s))
{
uregex_close(r);
@ -363,6 +408,7 @@ setupRegex(URegularExpression *regex,
{
uregex_useTransparentBounds(r, TRUE, &s);
}
uregex_setTimeLimit(r, _workLimit, &s);
if (U_FAILURE(s))
{
uregex_close(r);

View file

@ -1,22 +1,102 @@
#import "ObjectTesting.h"
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSRegularExpression.h>
#import <Foundation/NSDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSValue.h>
@interface DegeneratePatternTest : NSObject
{
NSRegularExpression *expression;
NSString* input;
}
@end
@implementation DegeneratePatternTest
- (instancetype) init
{
if (nil == (self = [super init]))
{
return nil;
}
expression =
[[NSRegularExpression alloc] initWithPattern: @"^(([a-z])+.)+[A-Z]([a-z])+$"
options: 0
error: NULL];
ASSIGN(input, @"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!");
return self;
}
- (void) runTest: (id)obj
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
[expression matchesInString: input
options: 0
range: NSMakeRange(0, [input length])];
DESTROY(pool);
}
- (void) dealloc
{
DESTROY(expression);
DESTROY(input);
[super dealloc];
}
@end
int main()
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
# ifdef GNUSTEP
// Ensure that a deterministic limit is set up for this process
NSUserDefaults *dflts = [NSUserDefaults standardUserDefaults];
NSDictionary *domain = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt: 1500], @"GSRegularExpressionWorkLimit", nil];
[dflts setVolatileDomain: domain
forName: @"GSTestDomain"];
# endif
id testObj = [[NSRegularExpression alloc] initWithPattern: @"^a"
options: 0
error: NULL];
test_NSObject(@"NSRegularExpression",
[NSArray arrayWithObject:
[NSArray arrayWithObject:
[[NSRegularExpression alloc] initWithPattern: @"^a"
options: 0
error: NULL]]);
test_NSCopying(@"NSRegularExpression",@"NSRegularExpression",
[NSArray arrayWithObject:testObj],NO,NO);
/* To test whether we correctly bail out of processing degenerate patterns,
* we spin up a new thread and evaluate an expression there. The expectation
* is that the thread should terminate within a few seconds.
*
* NOTE: Since we cannot terminate the thread in case of a failure, this
* test should be run last.
*/
DegeneratePatternTest *test = [DegeneratePatternTest new];
NSThread *thread = [[NSThread alloc] initWithTarget: test
selector: @selector(runTest:)
object: nil];
[thread start];
[thread setName: @"PatternTestRunner"];
NSDate *started = [NSDate date];
NSRunLoop *rl = [NSRunLoop currentRunLoop];
/* We spin the runloop for a bit while we wait for the other thread to bail
* out */
while ([thread isExecuting] && abs([started timeIntervalSinceNow] < 10.0f))
{
[rl runMode: NSDefaultRunLoopMode
beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]];
}
PASS(NO == [thread isExecuting], "Faulty regular expression terminated");
[arp release]; arp = nil;
return 0;
}