Tuesday, January 3, 2012

.Net's Sort Is Not Secure. Don't Use It. Here's a Better One.

.Net's Array.Sort (up to at least version 4.0) has serious weaknesses:

1. It is insecure and using it makes you vulnerable to a malicious attacker. .Net uses an ordinary quicksort with the pivot selected by the median-of-three method. It is easy to provoke quicksort's worst-case (quadratic) behavior and increase running times by multiple orders-of-magnitude. An attacker will be happy to exploit this as an effective denial-of-service attack.

2. It is inflexible. It does not allow you to provide a delegate for the swap function so sorting data structures where data synchronization is required to maintain consistency as items are moved is impossible. Also, you can only sort items on the .Net heap so sorting unmanaged memory is impossible.

3. It is slower than it should be even in the absence of an attacker.

Zimbry.Introsort addresses each of these problems.

1. It is secure. It is based on David Musser's Introsort algorithm. Introsort is essentially a quicksort that, should it fail, falls back to a secure heapsort.

2. It is flexible. Both the compare and swap operations are provided by the user. You can use it to sort anything.

3. It is faster. This wasn't an explicit objective but it's nice that we don't have to trade away performance to get a secure and flexible sort.

Click the links to see the benchmarks:


Let's look at the worst-case of dealing with an adversary.

It takes .Net over 26 minutes to sort one million integers when they are provided by an adversary. Zimbry.Introsort does it in half a second.

Those are the worst-case results. We can disable the adversary and benchmark it again:


Zimbry.Introsort is twice as fast in the average case and rarely less than 13% faster in any case.

(Each test was run only once so the timings for small arrays contain noticeable sampling noise. A more robust benchmark would filter multiple samples.)

I am releasing the source under the MIT license: Click here for the source

Some notes on the source:

You'll find many alternative sort algorithms in the Zimbry.Sort.OtherSorts project. I experimented with these along the way. You can enable them in the benchmark if you have a great deal of patience.

The class in QuicksortAdversary.cs was derived from Doug McIlroy's paper, A Killer Adversary for Quicksort. Be careful. It will beat up quicksort and steal its lunch money.

Zimbry.Introsort contains four sort algorithms layered together:

1. Quicksort with pivot selected by median-of-nine: For large partitions.

2. Quicksort with pivot selected by median-of-five: For small partitions.

3. Heapsort as a fall-back when quicksort recurses too deep: Heapsort is slower than quicksort in the best case but it has no quadratic behavior to exploit so it provides effective protection against an adversary.

4. Insertion sort: For tiny partitions where quicksort is inefficient.

Using these four algorithms lets us enjoy the performance advantage of quicksort for the typical case with protection against a malicious attacker in the worst case.

Both quicksorts use Bentley & McIlroy's "fat-pivot" partitioning method from their paper, Engineering a Sort Function, for better performance. This is a big part of why it performs better than .Net's quicksort in many tests.

While this is an improvement it is far from the last word in sorting. Some ideas to consider:

Better performance may be found with Vladimir Yaroslavskiy's dual-pivot quicksort.

It really needs special versions for handling known data types (avoiding the requirement for using compare and swap delegates in all cases). This would give a significant speed improvement.

There's more room for performance tuning. I tried to leave the code in a fairly readable state and some sacrifices could be made to buy a little more performance.

It would be nice to add support for stable sorting.

74 comments:

  1. Array.Sort has a weekness how about the linq sort? Is this one any safe?

    It should be reported to Microsoft using their public ticket where everyone can vote up. If the released code is under and acceptable license for Microsoft it could probably be event updated in future version of the .NET Framework.

    ReplyDelete
  2. "The class in QuicksortAdversary.cs was derived from David Musser's paper, A Killer Adversary for Quicksort"

    Sorry no. That paper is by Doug McIlroy

    ReplyDelete
  3. Another way to harden quicksort is to select the elements used in the pivot calculation randomly. This makes it nearly impossible for an attacker to cause worse case performance, and doesn't require you to also implement multiple sort algorithms.

    ReplyDelete
  4. Well if it's that good you should definitely report it to Microsoft, maybe they will include it in future version.

    ReplyDelete
  5. Jc, Good question. I don't know if LINQ uses the same underlying sort. It will be easy enough to find out, though. I'll run some tests and post what I learn.

    ReplyDelete
  6. Stephen, Thanks for the catch! I have made the correction.

    ReplyDelete
  7. Eric, That's worthy of a post in itself. If you look in Zimbry.Sort.OtherSorts you'll find two variations of quicksort that choose a pivot at random: QuicksortClassicR1 and QuicksortFatPivotR1. I'll include these in the benchmark, run it, and post what I find.

    ReplyDelete
  8. Array sort might be unsecure, might be inflexible, but not agreed that its slower.

    I created a C# window application and create an array of random generated integers (from 1 to 1 million). What result i got is
    1. For 1 million records it takes 124 milliseconds
    2. For 10 million records it takes 1310 milliseconds
    3. For 100 million records it takes 13979 milliseconds

    So i am 100% disagree that "It takes .Net over 80 minutes to sort one million integers "

    My computer configuration is
    Window 7 64 bit Enterprise, i7, 8GB RAM


    Here is sample code:
    private void StartProcess_Click(object sender, EventArgs e)
    {
    long len = long.Parse(txtLength.Text);

    long[] arrForArraySort;
    arrForArraySort = new long[len];

    Random r = new Random();

    DateTime dt1 = DateTime.Now;
    for (long i = 0; i < len; i++)
    {
    arrForArraySort[i] = r.Next(1, 1000000);
    }

    //Array sort
    DateTime dt2 = DateTime.Now;
    Array.Sort(arrForArraySort);
    DateTime dt3 = DateTime.Now;

    lblFillArray.Text = dt2.Subtract(dt1).TotalMilliseconds.ToString();
    lblArraySort.Text = dt3.Subtract(dt2).TotalMilliseconds.ToString();
    }

    ReplyDelete
  9. Satya, please read it again. It says:

    > It takes .Net over 80 minutes to sort
    > one million integers when they are
    > provided by an adversary.

    The last part is very important. It takes a long time when the integers are supplied by an adversary, not when they are chosen at random.

    .Net's sort is fast when sorting random integers.

    Read this paper by Doug McIlroy:

    A Killer Adversary for Quicksort
    http://www.cs.dartmouth.edu/~doug/mdmspe.pdf

    ReplyDelete
  10. Strange usage of the term "insecure", because some user can invoke a quadratic time response? That's not what term insecure.
    Can't supply as delegate? You supply an object that implements IComparer or Comparision to do specific sorting.

    ReplyDelete
  11. > Strange usage of the term "insecure",
    > because some user can invoke a quadratic
    > time response? That's not what term insecure.

    Yes, it is insecure. It is a resource starvation attack.

    > Can't supply as delegate? You supply an
    > object that implements IComparer or
    > Comparision to do specific sorting.

    Please read it again. You can supply the comparison delegate. It does not provide for supplying the swap delegate.

    ReplyDelete
  12. "It is a resource starvation attack."
    It always possible to define some data to invoke the worst case. It's not insecure but potential inefficient, that's why sorting algorithm declare a worst case. It's even stated in the MSDN documentation that it's O(n^2), which I think for Quicksort is the data starts in reverse order.

    Insecure to me, means you can access sensitive information like storing password as plain text.

    If we use your definition of insecure. Your bank's website is insecure, just means it's slow.

    "... sorting data structures where data synchronization is required to maintain consistency as items are moved is impossible. ..."

    They why are you using array.sort() to do the sort? The data structure (class) itself should provide the sort (if it is required.). An array is just a simple collection of "simple" types.

    What would have be clever if the sort detected the type of the array be pass in, and if it was possible to do a radix / counting sort on it to do that.

    ReplyDelete
  13. > Insecure to me, means you can access
    > sensitive information like storing
    > password as plain text.

    Insecure means it's vulnerable to an attack. You don't have to take my word for it. Here's Wikipedia:

    http://en.wikipedia.org/wiki/Denial-of-service_attack#Asymmetry_of_resource_utilization_in_starvation_attacks

    > They why are you using array.sort() to do
    > the sort? The data structure (class) itself
    > should provide the sort (if it is required.).
    > An array is just a simple collection of
    > "simple" types.

    Yes, you can create a unique sort function for each of your data structures.

    Or, you can use one common sort function on many different kinds of data.

    One sort or many. Which is better?

    ReplyDelete
  14. I wonder if the .net framework uses quicksort vs introsort is that .net framework development had progressed too far for it modified so it could implement it. If they wait a little bit longer it could have generics from the start.

    ReplyDelete
  15. Wow, blast from the past. Where to begin...

    As a general rule, arrays run like crap compared to the generic collections in .NET. The generic, linq enable collection classes only begin performing less than optimally when you start using the sorting and query methods in particular. Even so the interim collection performance makes up for it.

    Why do I bring this up? Because javanubs usually come in, bitter that they're having to switch and begin immediately espousing the glory of simple arrays when they see generics, assuming they're the same as java generics. The rule being "if you don't like the platform, the easiest comparisons to assault are its performance or its security." This smells like a permutation on that attack vector, and is an argument I haven't heard brought up since 2007 when it became largely irrelevant.

    So, the simple answer to your original problem: don't use arrays, save when it's absolutely mandated by a core class.

    ReplyDelete
  16. David its nice artical keep it up

    ReplyDelete
  17. Mallah Diction,

    > So, the simple answer to your original
    > problem: don't use arrays, save when
    > it's absolutely mandated by a core class.

    Have you tested this?

    If the Sort method on the generic List<> collection can also be provoked into quadratic behavior then it is also insecure and vulnerable to an attack.

    I'll run benchmarks overnight and report what I find tomorrow.

    ReplyDelete
  18. Thanks, it's nice that you've provided this. I felt like it would be handy if there were also some "standard" Sort overloads, so that this implementation can be easily swapped with existing .NET Sort methods.

    I've added a couple of them, feel free to use them, modify them, no licence attached:

    public static void Sort<T>
        (IList<T> list, int start, int count, Comparison<T> comparison)
    {
        Compare c = (i, j) => comparison(list[(int)i], list[(int)j]);

        Swap s = (i, j) =>
        {
            T tmp = list[(int)i];
            list[(int)i] = list[(int)j];
            list[(int)j] = tmp;
        };

        Sort(c, s, start, count);
    }

    public static void Sort<T>
        (IList<T> list, int start, int count, IComparer<T> comparer)
    {
        Sort(list, comparer.Compare);
    }

    public static void Sort<T>(IList<T> list, IComparer<T> comparer)
    {
        Sort<T>(list, 0, list.Count, comparer);
    }

    public static void Sort<T>(IList<T> list)
    {
        Sort(list, Comparer<T>.Default);
    }

    public static void Sort<T>(IList<T> list, Comparison<T> comparison)
    {
        Sort(list, 0, list.Count, comparison);
    }

    ------

    With these in place, you can simply write:

    IList<T> list = SomeList();

    // using the default comparer for 'T'
    Introsort.Sort(list);

    // using a delegate
    Introsort.Sort(list, (x, y) => (x - y));

    // using a custom implementation of IComparer<T>
    Introsort.Sort(list, new CustomComparer());

    ReplyDelete
  19. @Mallah Diction: I don't see your point. Array.Sort<T> and List<T>.Sort both use the same QuickSort algorithm. And generic collections all use plain arrays under the hood, which are incredibly fast to allocate and manipulate.

    The only issue with arrays is that their design is poor and feels like ANSI C legacy compared to interface-based collections and syntactic sugar brought by LINQ extension methods. But in sorting context, there is nothing "closer to the metal" than arrays.

    ReplyDelete
  20. Thanks, Groo. What I really need to do is build type-aware versions of the sort. Forcing it to always use delegates is expensive. It's not just due to the delegate call overhead but also because some algorithmic optimizations are not available to a pure comparison sort.

    ReplyDelete
    Replies
    1. I am not sure what you meant by "to always use delegates is expensive". I doubt anyone frequently has a need to sort a plain array of integers? Or did I misunderstand you? Most use cases for sorting will involve around comparison sorts using delegates (if you wanted to implement a radix sort, fine, but you would still use a delegate to project a certain property to an integer).

      Delete
    2. Groo, your question deserves a thoughtful answer. It's going to be longer than I would prefer so please bear with me. There are three parts to the answer:

      Part 1: The way I use delegates isn't the same as the way .Net is using them. .Net lets you provide an IComparer for an object while I simply call a delegate with array indexes. There's a big difference between the two approaches. .Net can store items outside of the collection being sorted, leaving it briefly in an inconsistent state, while I must always work with items within the array itself.

      This has two consequences:

      1. For quicksort: .Net's quicksort can take the pivot item, store it in a local variable (it's just a pointer to an object), and compare it against items in the array. My sort has to first swap it to the beginning of the array, compare against that with an index and then swap it back when the partitioning is complete. Quicksort has to do this often and it's expensive. For heapsort: Floyd's optimization is not available to me which is about a 25% penalty.

      2. In my code every comparison and swap incurs two array reference penalties.

      Part 2: .Net does not provide a delegate to perform swaps which increases performance.

      Part 3: .Net provides special versions of its sort to handle native data types. You wondered how often this would be useful. I don't know but it's probably often enough to be worth handling. Worse, if you don't do it, anyone with a simple benchmark is likely to draw the wrong conclusions.

      Going with the pure delegate approach offers maximum flexibility but it comes at both a mechanical cost (the overhead of indirecting through an array index) and an algorithmic cost (choices that are made unavailable.)

      I can't say for certain what the final performance cost is without building the alternative but I suspect it's significant. There's a lot of performance left laying on the table with the code in its current state.

      Delete
    3. Right, now I get what you wanted to say. I completely forgot that abstracting the swap operation prevents all access to actual elements. When you mentioned delegates, it confused me because calling IComparison<T>.Compare(A,B) or simply the delegate Comparison<T>(A,B) is more or less the same, performance wise, but you were referring to a completely different issue.

      That makes perfect sense, as I was also initially confused by the approach that you decided to take, given that the point of the article seemed to be to provide a replacement for QuickSort (with performance benchmarks).

      Not to mention that the "non-delegate" version would additionally improve benchmark results in your favor.

      Delete
  21. Your test jig has a error in the ForceScrambledOrder method:
    array[i] = swap;
    should be:
    array[i] = array[swap];

    ReplyDelete
  22. I compared this to a .Net implementation of TimSort from http://timsort4net.codeplex.com/releases/view/73969 and sadly, TimSort kicks this guy's butt (though there is an outlier for the All unique, partial ascending order test case -- which is why I discovered the error in the partial shuffling code). If you want the test jig changes, I'll zip it up and you can delve further. Admittedly we're not playing fair because of your extra swapper-features, but it IS a big difference TimSort takes 54% of Introsort on 10,000 entries.

    ReplyDelete
  23. Marc,

    Thank you very much for the bug report! I'll make the correction and rerun the benchmarks.

    Please send me the TimSort code. I would like to give it a try.

    ReplyDelete
  24. @Marc: It is worth noting that TimSort has a O(n) space complexity, compared to O(logN) for QuickSort and O(1) for HeapSort.

    ReplyDelete
  25. I think Mallah's mistake was to assume that C# arrays were non-generic and required boxing/unboxing which would make them very inefficient. But .NET arrays are generic.

    ReplyDelete
  26. This comment has been removed by the author.

    ReplyDelete
  27. This comment has been removed by the author.

    ReplyDelete
  28. Starting with .Net 4.5 Array.Sort IS using introsort itself. .Net 4.5. So your post is not correcy anymore.

    ReplyDelete
  29. Now that the hardware 123.hp.com/setup 9025 of your OfficeJet printer is completed. Let’s move on to the software installation process;

    Navigate to 123.hp.com/setup 9025 web page and specify your printer model there to download the driver software
    Once downloaded, launch the file on your computer
    Follow the on-screen instructions to complete the software installation process

    Do not worry if you face any hindrances during the 123.hp.com/setup 9025 process, call us right away.

    ReplyDelete
  30. Striking of material. I've bookmarked it for future reference. You've made them cripple posts on your blog. Keep up for a stunning Career. Fix HP Envy 4520 Printer Offline by HP experts. Contact our executives to resolve HP Envy Printer 4520 Offline.

    ReplyDelete
  31. Keen as can be clear, man! It's truly what I need!! Much regarded! Appreciative to you. People like Hitler and Mussolini changed into a confusing disfigurement on culture and human amazing new upsetting new staggering sudden unexpected new turn of events. Hi I providing hosting in 80% off. For more information visit my website click here Hostinger Coupon Code India

    ReplyDelete
  32. Your article is so useful. The field of site construction and improvement is associated with making drawing in site that give smooth investigating experience to client. The Epson Printer t3 Error Code 20000010 caused by paper stuck in printer. Don't worry we help you to resolve the issue.

    ReplyDelete
  33. Hazard Analysis Critical Control Points (HACCP) is a food safety system standard that provides requirements to analyze, monitor, and as well as to control the biological, physical, and chemical hazards in food safety. It also specifies critical control points, critical limits, and control measures to inspire the food processors and to continually improve the FSMS performance of the organization. Also, this globally recognized system standard minimizes food safety hazards and eliminates low-quality food. Visit certificazione haccp

    ReplyDelete
  34. Global Assignment help services will provide you with the best solutions for your problem. We are the ones who not only provide you with the assignments but our assignment expert also help how to write a case study assignment. A case Study needs in-depth knowledge and it means to build something new with own ideas and thoughts. We provide three types of case studies like a single instrumental case study, multiple case studies, and intrinsic case study. Each case studies are completely different from the other and each needs complete research and a minute study.Our Service providers avail for all your academic help.

    ReplyDelete
  35. "How to Tomtom Map Update UK, USA ? Guide for Update Tomtom Car sat nav and Add Tomtom GPS Map and Troubleshoot Tomtom Map Update Error Code
    "

    ReplyDelete
  36. "How to Garmin GPS Map Update ? Call for Garmin Map Updates and Garmin lifetime Maps Update, New Add Garmin Map in UK and USA
    "

    ReplyDelete
  37. You made some good points .I did a little research on the topic and found that most people agree with your blog. Thanks.
    microsoft refund request

    ReplyDelete

  38. Marston Holdings
    Your website is excellent , i have been looking for this information everywhere.
    Website

    ReplyDelete
  39. HMRC Chase Tax DebtsThis is my first post. I really like this blog. I'm reading this post from my I-Phone and it looks great!

    ReplyDelete
  40. IVA Help
    Hi, I just stumbled upon your post,a good view point. Hey your post left me quenching for more.

    ReplyDelete
  41. Contact Step Change team has helped many UK natives to reduce their debt amount. We offer the ultimate debt solution which will pay off all your debts. Consult with us before applying for any debt arrangement schemes. We will let you know the eligibility criteria of the selected UK Government scheme. Our experts will state the consequences one has to face for not repaying the money.

    ReplyDelete
  42. It is very good, this is a good platform for chat and got a lot of information after reading comments and got good reviews for social media and bloggers, I am your guest and first time I have come to your page and it is very good and If we want to read more such posts, then stay late, thanks.infalation rate





    ReplyDelete
  43. I just read through the entire article of yours and it was quite good. This is a great article thanks for sharing this information. I will visit your blog regularly for some latest post.
    blink camera login for pc

    ReplyDelete
  44. Thank you very much for publishing a great article. Keep it up
    Many times people could not get bookings in their favorite hotels at hill like Manali.
    However, this kind of problem takes place when they approach during visit season. It is needless to say that thousands of people come to visit Manali for full recreation. This is why speaking to the Manali Tourism service providers can help you to find the rooms at the hotel of your choice.

    ReplyDelete
  45. Thanks for sharing such amazing information. Your content is very well written. Keep it up.
    Getting a Cash App Refund is not easy After making an unwanted payment to another person. If your mind is stuck on how to get your money returned from the cash app then you may call our cash app support team for immediate assistance.

    ReplyDelete
  46. I would like to take this opportunity to thank the author for his incredible work. I believe Customer relationship management is essential for every business to create a loyal customer base. keep doing it. all the best
    top crm software

    ReplyDelete
  47. Dial The Step Change Contact Number for Step Change Debt Charity reduce their debt amount. Stepchange UK offer the ultimate debt solution which will pay off all your debts. Consult with us before applying for any debt arrangement schemes. We will let you know the eligibility criteria of the selected UK Government scheme. Step Change Scotland Contact Number experts will state the consequences one has to face for not repaying the money.

    ReplyDelete
  48. Thanks for sharing this amazaing blog with us. PS3 games on rent

    ReplyDelete
  49. Keep sharing such a great article! Are you stuck with your assignment? GoAssignmentHelp is one of the best Computer Network Assignment Help service providers that provide best engineering assignment help to those students who face these issues and write Business Statistics Assignment Help and score good grades.

    ReplyDelete
  50. The fastest and safest ride to the airport in London.Clean and comfortable cars. Fully licensed taxi services. one of the most trusted taxi services in London. Meet & greet. special airport rates.
    Book in under 60 seconds. Professional drivers.24 hours assistance. Our services include taxi transfers to Gatwick, Heathrow, London, Luton & Stansted airport. we are here to make you comfortable and hassle less for. Friendly and reliable taxi service at competitive prices. Our commitment to you is quality and long-lasting.

    ReplyDelete
  51. 바카라사이트
    I surprised with the research you made to create this actual post incredible. Fantastic job!

    ReplyDelete
  52. Hi,
    It is really a great post by you. I found this so interesting. Keep it up and keep sharing such posts.
    Every student faces many challenges in their academic career. But those Students who go abroad to do their bachelors or masters face more challenges because they have to do a job along with their studies for their survival. And this results in low grades in their college assignments because they don’t have much time to complete their assignments on time. So, we bring a perfect solution for this problem. Get assignment sample from our assignment experts and attain good grades.
    Many different subjects where students mostly seek online assignment help are chcece001 assessment answers Help, Management Assignment Help, Law Assignment Help, etc.
    assignments help australia

    ReplyDelete
  53. HCM Software
    I am impressed by the information that you have on this blog. It shows how well you understand this subject.

    ReplyDelete
  54. You have got some great posts in your blog. Keep up with the good work.https://theluxia.com/

    ReplyDelete
  55. Set up the issue printer such that it prints directly to the printer: After select Settings from the Start menu, click Printers. Click Properties from the menu bar when you right-click the issue printer. Select the Details tab, then select Spool Settings, Print to printer directly, and OK. Visit our website to learn more about the HP Printer Access Denied Issue.
    HP Printer Access Denied Issue

    ReplyDelete
  56. It’s awesome. Article is so helpful. Good work.

    ReplyDelete
  57. Awesome content. I bookmarked it for future reference.

    Data recovery services Sharjah is an exclusive service offered by the company that specializes in recovering data from damaged hard drives, laptops, and other electronic devices. The company offers a wide range of services that includes recovery of data from hard drives, recovery of data from a hard drive, data backup, data recovery, and file recovery. For more information call +97145864033.

    ReplyDelete
  58. This is a very wonderful article, I got to learn a lot from reading this, and I hope that you will keep posting like this in the future also. MacBook Repair Services in Dubai

    ReplyDelete
  59. Your blog is very nice and helpful to me. your writing skill is very good and the way you explain your topic is awesome.
    refrigerator repair in dubai

    ReplyDelete
  60. nice post I've been regularly checking this blog, and I'm impressed! Very useful information, especially the last section. I really appreciate this kind of material. I looked for this precise information for a very long time. Thank you and good luck.Laptop Data Recovery in Dubai

    ReplyDelete


  61. This is such a great resource that you are providing and you give it away for free

    hard disk recovery dubai

    ReplyDelete
  62. I was thrilled to discover this site. I would like to say thank you for this fantastic reading experience! I thoroughly enjoyed every part of it. I've saved your site to go through the latest content you publish.

    laptop repairing

    ReplyDelete
  63. Excellent blog! Such clever work and exposure! Keep up the very good work. Eddie Munson Vest

    ReplyDelete
  64. people in your circle of friends and family for no obvious reasons. pet articles On the other hand you might find it to be very friendly with some strangers.

    ReplyDelete
  65. I recently came across the article discussing the security concerns with .Net's built-in sorting function, and it certainly gave me pause. The author makes a compelling case about the potential vulnerabilities and risks associated with using the default sorting mechanism. It's alarming to think that such a fundamental aspect of programming could have security loopholes.

    The provided alternative solution in the article is a game-changer. It not only addresses the security issues highlighted but also offers a more efficient and reliable sorting method. It's refreshing to see the author not just pointing out a problem but providing a practical and safer alternative.

    As a developer, these insights are invaluable, and I appreciate the effort to share this information with the community. It underscores the importance of staying vigilant about the tools and functions we use regularly. Kudos to the author for shedding light on this issue and offering a better, more secure way forward. Time to update my coding practices! Most students are drawn to these types of articles and information, but they are unable to prepare for their exams, If you have been struggling with your exams and want assistance, students can pay professional test takers for hire - professional test taker and get higher grades on their examinations by providing them with the best available resources, including quality academic services.

    ReplyDelete