Microsoft describes Azure SQL Managed Instance (MI) as offering near-full SQL Server compatibility with more flexibility than typical PaaS. So naturally, I wondered: how much control does a developer really have?
I set out to find out, not by reading the documentation, but by poking around under the hood. The mission? To see whether SQL MI could support fast, lightweight cloning using the same tricks I'd used before: spinning up parent-child VHDs, mounting them, and leveraging .mdf files, but now from within the SQL MI instance. If SQL MI was just "SQL Server in a VM," it might work. If it was a fully locked-down black box, it definitely wouldn't.
The answer turned out to be neither and more interesting than expected.
My first trick in the bag: xp_cmdshell
We've all leaned on xp_cmdshell at some point, whether it was a quick and dirty backup move, firing off an OS-level check, or just a way to "escape the box." But what happens when that escape hatch is welded shut?
Welcome to SQL Managed Instance (MI), a powerful cloud offering that brings the best of SQL Server to the platform-as-a-service world, but with some surface-level OS access removed. And yes, xp_cmdshell is gone.
But that didn't stop me...
Peeking Behind the PaaS Curtain
So I turned to the only tool still in reach: SQL Agent. With no xp_cmdshell and no RDP, I started inspecting the environment from the inside — one job step at a time. Without xp_cmdshell, you might think your options are limited, but the SQL Agent is surprisingly capable. It can execute both CMD and PowerShell "job step types" and return output right into the SQL Agent job history logs or results table.
I started with some basic commands in my job steps:
Drive Listings
wmic logicaldisk get name Name ---- C: D:
Directory Scans
Get-ChildItem -Path C:\, D:\ | Select-Object FullName FullName -------- C:\03CC4CC8-D99F-4CC0-8946-ED06CD486B84 C:\Dumps C:\Installer C:\Logs C:\Program Files C:\Program Files (x86) C:\ScriptsForContainer C:\SFApplications C:\SFFabricBin C:\SFFabricLog C:\SFPackageRoot C:\Users C:\VCRedist C:\Windows C:\License.txt D:\CollectGuestLogsTemp D:\HDD D:\MonitoringDataDirectory D:\SFVmExtnData D:\WER D:\WFRoot D:\DATALOSS_WARNING_README.txt D:\GcsConfCommt.txt
Local Users
Get-LocalUser Name Enabled Description ---- ------- ----------- Administrator True Built-in account for administering the computer/domain DefaultAccount False A user account managed by the system. Guest False Built-in account for guest access to the computer/domain replAgentUser True replication sqlAgentUser True replication WDAGUtilityAccount False A user account managed and used by the system for Windows Defender Application Guard scena...
Services
Get-Service | Where Status -eq Running Status Name DisplayName ------ ---- ----------- Running cexecsvc Container Execution Agent Running CoreMessagingRe... CoreMessaging Running CryptSvc Cryptographic Services Running DcomLaunch DCOM Server Process Launcher Running Dhcp DHCP Client Running DiagTrack Connected User Experiences and Tele... Running Dnscache DNS Client Running EventLog Windows Event Log Running EventSystem COM+ Event System Running GenevaContainer... Geneva Container Shim Running gpsvc Group Policy Client Running iphlpsvc IP Helper Running KeyIso CNG Key Isolation Running LanmanWorkstation Workstation Running LSM Local Session Manager Running nsi Network Store Interface Service Running ProfSvc User Profile Service Running RpcEptMapper RPC Endpoint Mapper Running RpcSs Remote Procedure Call (RPC) Running SamSs Security Accounts Manager Running Schedule Task Scheduler Running SENS System Event Notification Service Running StateRepository State Repository Service Running SysMain SysMain Running SystemEventsBroker System Events Broker Running TimeBrokerSvc Time Broker Running UserManager User Manager Running UsoSvc Update Orchestrator Service Running WinHttpAutoProx... WinHTTP Web Proxy Auto-Discovery Se... Running Winmgmt Windows Management Instrumentation Running WinRM Windows Remote Management (WS-Manag...
At this point you've might already ask yourself, where are the SQL* Services…but I continued my investigation.
Environment Variables
Get-ChildItem Env: Name Value ---- ----- ALLUSERSPROFILE C:\ProgramData APPDATA C:\Users\ContainerAdministrator\AppData\Roaming CheckConnectivityOnContaine... 1 CommonProgramFiles C:\Program Files\Common Files CommonProgramFiles(x86) C:\Program Files (x86)\Common Files CommonProgramW6432 C:\Program Files\Common Files COMPLUS_MDA InvalidVariant;RaceOnRCWCleanup;InvalidFunctionPointerInDelegate;InvalidMemberDeclarat... COMPUTERNAME CHNKHERPO1VUMRQ ComSpec C:\Windows\system32\cmd.exe DriverData C:\Windows\System32\Drivers\DriverData EnableContainerShim 1 EnableSeLockMemoryPrivilege 1 … USERDOMAIN User Manager USERNAME ContainerAdministrator USERPROFILE C:\Users\ContainerAdministrator windir C:\Windows
As you can see, the username (ContainerAdministrator) and values like EnableContainerShim and CheckConnectivityOnContainerHealth made it clear: this was a containerized environment, not a traditional host.
Full System Info
So, what did the system info tell us?
systeminfo Host Name: CHNKHERPO1VUMRQ OS Name: Microsoft Windows Server 2022 Datacenter OS Version: 10.0.20348 N/A Build 20348 OS Manufacturer: Microsoft Corporation OS Configuration: Standalone Server OS Build Type: Multiprocessor Free Registered Owner: N/A Registered Organization: N/A Product ID: 00454-60000-00001-AA216 Original Install Date: 8/3/2023, 5:02:23 PM System Boot Time: 6/13/2025, 4:02:08 PM System Manufacturer: Microsoft Corporation System Model: Virtual Machine System Type: x64-based PC Processor(s): 1 Processor(s) Installed. [01]: Intel64 Family 6 Model 85 Stepping 7 GenuineIntel ~2594 Mhz BIOS Version: Microsoft Corporation Hyper-V UEFI Release v4.1, 5/13/2024 Windows Directory: C:\Windows System Directory: C:\Windows\system32 Boot Device: \Device\HarddiskVolume1 System Locale: en-us;English (United States) Input Locale: en-us;English (United States) Time Zone: (UTC) Coordinated Universal Time Total Physical Memory: 22,477 MB Available Physical Memory: 11,063 MB Virtual Memory: Max Size: 27,597 MB Virtual Memory: Available: 14,691 MB Virtual Memory: In Use: 12,906 MB Page File Location(s): D:\pagefile.sys Domain: WORKGROUP Logon Server: N/A Hotfix(s): 3 Hotfix(s) Installed. [01]: KB5028956 [02]: KB5029250 [03]: KB5029395 Network Card(s): N/A Hyper-V Requirements: A hypervisor has been detected. Features required for Hyper-V will not be displayed.
Each job gave me a little more insight. This wasn't a stripped-down SQL container on Linux. It was clearly Windows Server 2022 Datacenter, or so I thought.
Installed programs
Then I listed the installed programs.
wmic product get name,version
That's when things got weird.
Name Version ---------------------------------------------- ------------------- Microsoft OLE DB Driver for SQL Server 18.7.4.0 Microsoft OLE DB Driver 19 for SQL Server 19.3.0.0 Microsoft Visual C++ 2022 X64 Minimum Runtime 14.42.34433 Microsoft Visual C++ 2022 X86 Minimum Runtime 14.42.34433 Microsoft Visual C++ 2022 X64 Additional Runtime 14.42.34433 Microsoft Visual C++ 2022 X86 Additional Runtime 14.42.34433
That's it. No full SQL Server installation in sight. And yet, SQL Server was running.
It quickly became clear this wasn't a normal VM (or IaaS in disguise), this was a highly curated container environment, orchestrated by Azure, and built for isolation. Environment variables like Fabric_* hinted at Azure Service Fabric under the hood or was it a preparation for the Fabric?
At this point, my original goal — using VHD-based clone techniques — was starting to feel naïve.
But I had to try anyway.
Trying the Clone Trick Anyway: VHD Mounting Attempts
In a traditional setup, I would create a dynamically expanding VHD, format it, mount it, and use it as a database mount point.
So I tried exactly that, using PowerShell in a SQL Agent job:
New-VHD -Path C:\vhd\parent.vhdx -Dynamic -SizeBytes 2048GB | Mount-VHD -Passthru | Initialize-Disk -Passthru | New-Partition -UseMaximumSize | Format-Volume -FileSystem NTFS -Confirm:$false -Force
Result: ? The Hyper-V Management Tools could not access an expected WMI class.
I had to fell back to diskpart:
diskpart
Result: ? Virtual Disk Service error: The service failed to initialize.
Turns out diskpart depends on the Virtual Disk Service, which simply isn't available in the OS driving SQL MI.
You also can't install it, Install-WindowsFeature, doesn't work either here.
So: no Hyper-V, no VDS, no disk tools. My original plan was dead in the water.
But What Can You Do?
Just as I was ready to give up, I suddenly realized...
My simple command jobs ran successfully and gave me full output. So it turns out: even in a restricted MI environment, SQL Server Agent has enough rights to run native OS commands, both CMD and PowerShell.
That opened the door to a whole new line of questions. For example: Could I list the active processes running inside the container? Could I finally find the sqlservr process itself, the very engine hosting the database? And what would happen if I tried to kill it?
I ran a quick PowerShell job:
Get-Process | Where-Object { $_.ProcessName -like "sqlserv*" } Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName ------- ------ ----- ----- ------ -- -- ----------- 1542 987 1991728 856820 41.23 19832 3 sqlservr
And there it was. Finally, sqlservr was visible for me, alive and well!
Then I took the next (arguably reckless) step:
Stop-Process -Name sqlservr -Force
The job succeeded, my query window died instantly on me…
…and SSMS froze:
The process clearly vanished. But within minutes, SQL MI was automatically respawned by (I assume) the container's orchestration layer, and the sqlservr process was back *:
Get-Process | Where-Object { $_.ProcessName -like "sqlserv*" } Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName ------- ------ ----- ----- ------ -- -- ----------- 1602 970 1753472 851444 16.72 19244 3 sqlservr
*) To be transparent, restarting the agent took a little more than a few minutes. But eventually I came past this message:
The good old sqlservr was back but with a new PID! Well actually, if I looked closer every process seem to have a new PID! Could this be a shiny new container instance?
The implications were clear: MI was more resilient and more containerized than I had imagined. Even killing the SQL Server process didn't break anything (permanently). It simply came back, silently reborn, as if nothing had happened.
Where Does That Leave Us?
This exploration taught me what SQL MI is not:
- Not a general-purpose Windows VM
- Not a Hyper-V host
- Not a clone-friendly IaaS environment
But it also showed what it is:
- A containerized SQL runtime with just enough exposed shell to poke at
- An environment where you can run OS-level queries via Agent jobs
- A great place to experiment, but messing to hard will get you a fresh container ??
What's Next?
In Part 2, I'll try something riskier.
If I can run PowerShell… can I introduce external code?
- No shell exploits.
- No SQLCLR.
- Just documented features and strategic file placement.
I'll explore whether you can upload a binary into a user database, sync it into MI, and convince the container OS to run it, from the inside.
We're not done. We're just getting started!
Bonus: A Handy Tool for PowerShell Execution
As a side project, and to save myself a ton of frustration, I built a stored procedure that lets you run PowerShell via one-time SQL Agent Job and retrieve the output in near real time, even when xp_cmdshell is disabled.
Why? Because experimenting through manually scheduled Agent jobs and digging through their logs is... painful. It's slow, error-prone, and just not fun when you're iterating on commands.
So I wrote dbo.RunPowerShellViaAgent.
CREATE PROCEDURE dbo.RunPowerShellViaAgent @PowerShellCommand NVARCHAR(MAX) AS BEGIN SET NOCOUNT ON; DECLARE @JobName SYSNAME = N'ps_exec_' + CONVERT(VARCHAR(36), NEWID()); DECLARE @StepId INT = 1; DECLARE @JobId UNIQUEIDENTIFIER; DECLARE @output NVARCHAR(MAX) = N''; DECLARE @finalOutput TABLE ( TerminalLine NVARCHAR(MAX) ) BEGIN TRY -- Create the SQL Agent Job EXEC msdb.dbo.sp_add_job @job_name = @JobName, @enabled = 1, @start_step_id = @StepId, @notify_level_eventlog = 0, @job_id = @JobId OUTPUT; -- Add a PowerShell Step EXEC msdb.dbo.sp_add_jobstep @job_id = @JobId, @step_id = 1, @step_name = N'Run PowerShell', @subsystem = N'PowerShell', @command = @PowerShellCommand, @on_success_action = 1, @on_fail_action = 2, @flags = 8; -- Add the job to the server EXEC msdb.dbo.sp_add_jobserver @job_id = @JobId, @server_name = N'(LOCAL)'; -- Start the job synchronously EXEC msdb.dbo.sp_start_job @job_id = @JobId; -- Wait for the job to finish WHILE EXISTS ( SELECT 1 FROM msdb.dbo.sysjobactivity a JOIN msdb.dbo.sysjobs j ON a.job_id = j.job_id WHERE j.name = @JobName AND a.stop_execution_date IS NULL ) BEGIN WAITFOR DELAY '00:00:01'; END -- Get step output SELECT @output = l.log FROM msdb.dbo.sysjobstepslogs l JOIN msdb.dbo.sysjobsteps s ON l.step_uid = s.step_uid JOIN msdb.dbo.sysjobs j ON j.job_id = s.job_id WHERE j.name = @JobName; -- Include original command as first line INSERT INTO @finalOutput ( TerminalLine ) VALUES (@PowerShellCommand); -- Use STRING_SPLIT to avoid XML issues IF @output IS NOT NULL BEGIN INSERT INTO @finalOutput (TerminalLine) SELECT value FROM STRING_SPLIT( REPLACE(cast(@output as nvarchar(max)), CAST( CHAR(13) + CHAR(10) as nvarchar(max)), CAST(CHAR(10) as nvarchar(max))), CHAR(13), CHAR(10)), CHAR(13), CHAR(10)), CHAR(10)); END END TRY BEGIN CATCH -- Capture error details if needed (optional) DECLARE @Err NVARCHAR(MAX) = ERROR_MESSAGE(); INSERT INTO @finalOutput ( TerminalLine ) VALUES ('ERROR: ' + @Err); END CATCH -- Ensure the job is cleaned up BEGIN TRY EXEC msdb.dbo.sp_delete_job @job_name = @JobName, @delete_unused_schedule = 1; END TRY BEGIN CATCH -- Ignore errors during cleanup END CATCH -- Output result SELECT TerminalLine FROM @finalOutput; END;
With this, you can do things like:
EXEC dbo.RunPowerShellViaAgent N'Get-ChildItem C:\ | Out-String'
And get formatted, readable terminal output, right inside SSMS.
It's fast, flexible, and perfect for digging into MI's container OS or really, any SQL Server instance where the Agent has enough (or maybe a bit too much) privilege.